diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/bun.js/bindings/BunJSCModule.cpp | 1 | ||||
-rw-r--r-- | src/bun.js/bindings/BunPlugin.cpp | 505 | ||||
-rw-r--r-- | src/bun.js/bindings/BunPlugin.h | 85 | ||||
-rw-r--r-- | src/bun.js/bindings/ZigGlobalObject.cpp | 44 | ||||
-rw-r--r-- | src/bun.js/bindings/ZigGlobalObject.h | 5 | ||||
-rw-r--r-- | src/bun.js/bindings/bindings.zig | 46 | ||||
-rw-r--r-- | src/bun.js/bindings/exports.zig | 2 | ||||
-rw-r--r-- | src/bun.js/bindings/headers-handwritten.h | 8 | ||||
-rw-r--r-- | src/bun.js/builtins/BunBuiltinNames.h | 3 | ||||
-rw-r--r-- | src/bun.js/javascript.zig | 183 | ||||
-rw-r--r-- | src/bun.js/modules/ObjectModule.h | 32 | ||||
-rw-r--r-- | src/bundler.zig | 208 | ||||
-rw-r--r-- | src/import_record.zig | 6 | ||||
-rw-r--r-- | src/js_printer.zig | 46 | ||||
-rw-r--r-- | src/linker.zig | 60 |
15 files changed, 1206 insertions, 28 deletions
diff --git a/src/bun.js/bindings/BunJSCModule.cpp b/src/bun.js/bindings/BunJSCModule.cpp index f6054d7f3..39e20d346 100644 --- a/src/bun.js/bindings/BunJSCModule.cpp +++ b/src/bun.js/bindings/BunJSCModule.cpp @@ -1,5 +1,4 @@ #include "root.h" -#include "JavaScriptCore/JSCInlines.h" #include "JavaScriptCore/JavaScript.h" #include "wtf/FileSystem.h" 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); +} diff --git a/src/bun.js/bindings/BunPlugin.h b/src/bun.js/bindings/BunPlugin.h new file mode 100644 index 000000000..600705500 --- /dev/null +++ b/src/bun.js/bindings/BunPlugin.h @@ -0,0 +1,85 @@ +#pragma once + +#include "root.h" +#include "headers-handwritten.h" +#include "JavaScriptCore/JSGlobalObject.h" +#include "JavaScriptCore/Strong.h" +#include "helpers.h" + +extern "C" JSC_DECLARE_HOST_FUNCTION(jsFunctionBunPlugin); +extern "C" JSC_DECLARE_HOST_FUNCTION(jsFunctionBunPluginClear); + +namespace Zig { + +using namespace JSC; + +class BunPlugin { +public: + // This is a list of pairs of regexps and functions to match against + class Group { + + public: + // JavaScriptCore/RegularExpression does exist however it does not JIT + // We want JIT! + // TODO: evaluate if using JSInternalFieldImpl(2) is faster + Vector<JSC::Strong<JSC::RegExp>> filters = {}; + Vector<JSC::Strong<JSC::JSFunction>> callbacks = {}; + BunPluginTarget target { BunPluginTargetBun }; + + void append(JSC::VM& vm, JSC::RegExp* filter, JSC::JSFunction* func); + JSFunction* find(JSC::JSGlobalObject* globalObj, String& path); + void clear() + { + filters.clear(); + callbacks.clear(); + } + }; + + class Base { + public: + Group fileNamespace = {}; + Vector<String> namespaces = {}; + Vector<Group> groups = {}; + + Group* group(const String& namespaceStr) + { + if (namespaceStr.isEmpty()) { + return &fileNamespace; + } + + for (size_t i = 0; i < namespaces.size(); i++) { + if (namespaces[i] == namespaceStr) { + return &groups[i]; + } + } + + return nullptr; + } + + void append(JSC::VM& vm, JSC::RegExp* filter, JSC::JSFunction* func, String& namespaceString); + }; + + class OnLoad final : public Base { + + public: + OnLoad() + : Base() + { + } + + EncodedJSValue run(JSC::JSGlobalObject* globalObject, ZigString* namespaceString, ZigString* path); + }; + + class OnResolve final : public Base { + + public: + OnResolve() + : Base() + { + } + + EncodedJSValue run(JSC::JSGlobalObject* globalObject, ZigString* namespaceString, ZigString* path, ZigString* importer); + }; +}; + +} // namespace Zig
\ No newline at end of file diff --git a/src/bun.js/bindings/ZigGlobalObject.cpp b/src/bun.js/bindings/ZigGlobalObject.cpp index 855e212f9..d378a0f4c 100644 --- a/src/bun.js/bindings/ZigGlobalObject.cpp +++ b/src/bun.js/bindings/ZigGlobalObject.cpp @@ -106,6 +106,8 @@ #include "ZigGeneratedClasses.h" +#include "BunPlugin.h" + #if ENABLE(REMOTE_INSPECTOR) #include "JavaScriptCore/RemoteInspectorServer.h" #endif @@ -162,6 +164,7 @@ using JSBuffer = WebCore::JSBuffer; #include "../modules/EventsModule.h" #include "../modules/ProcessModule.h" #include "../modules/StringDecoderModule.h" +#include "../modules/ObjectModule.h" // #include <iostream> static bool has_loaded_jsc = false; @@ -2400,6 +2403,14 @@ void GlobalObject::installAPIGlobals(JSClassRef* globals, int count, JSC::VM& vm JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontDelete | 0); } + { + JSC::Identifier identifier = JSC::Identifier::fromString(vm, "plugin"_s); + JSFunction* pluginFunction = JSFunction::create(vm, this, 1, String("plugin"_s), jsFunctionBunPlugin, ImplementationVisibility::Public, NoIntrinsic); + pluginFunction->putDirectNativeFunction(vm, this, JSC::Identifier::fromString(vm, "clearAll"_s), 1, jsFunctionBunPluginClear, ImplementationVisibility::Public, NoIntrinsic, + JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontDelete | 0); + object->putDirect(vm, PropertyName(identifier), pluginFunction, JSC::PropertyAttribute::Function | JSC::PropertyAttribute::DontDelete | 0); + } + extraStaticGlobals.uncheckedAppend( GlobalPropertyInfo { builtinNames.BunPublicName(), JSC::JSValue(object), JSC::PropertyAttribute::DontDelete | 0 }); @@ -2655,6 +2666,22 @@ static JSC_DEFINE_HOST_FUNCTION(functionFulfillModuleSync, RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(JSC::jsUndefined())); RELEASE_AND_RETURN(scope, JSValue::encode(JSC::jsUndefined())); } + case SyntheticModuleType::ObjectModule: { + JSC::EncodedJSValue encodedValue = reinterpret_cast<JSC::EncodedJSValue>( + bitwise_cast<int64_t>(reinterpret_cast<size_t>(res.result.value.source_code.ptr))); + JSC::JSObject* object = JSC::JSValue::decode(encodedValue).getObject(); + auto function = generateObjectModuleSourceCode( + globalObject, + object); + auto source = JSC::SourceCode( + JSC::SyntheticSourceProvider::create(WTFMove(function), + JSC::SourceOrigin(), WTFMove(moduleKey))); + + RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(JSC::jsUndefined())); + globalObject->moduleLoader()->provideFetch(globalObject, key, WTFMove(source)); + RETURN_IF_EXCEPTION(scope, JSC::JSValue::encode(JSC::jsUndefined())); + RELEASE_AND_RETURN(scope, JSValue::encode(JSC::jsUndefined())); + } case SyntheticModuleType::Process: { auto source = JSC::SourceCode( JSC::SyntheticSourceProvider::create( @@ -2746,6 +2773,23 @@ JSC::JSInternalPromise* GlobalObject::moduleLoaderFetch(JSGlobalObject* globalOb globalObject->vm().drainMicrotasks(); return promise; } + case SyntheticModuleType::ObjectModule: { + JSC::EncodedJSValue encodedValue = reinterpret_cast<JSC::EncodedJSValue>( + bitwise_cast<int64_t>(reinterpret_cast<size_t>(res.result.value.source_code.ptr))); + JSC::JSObject* object = JSC::JSValue::decode(encodedValue).getObject(); + auto source = JSC::SourceCode( + JSC::SyntheticSourceProvider::create(generateObjectModuleSourceCode( + globalObject, + object), + JSC::SourceOrigin(), WTFMove(moduleKey))); + + auto sourceCode = JSSourceCode::create(vm, WTFMove(source)); + RETURN_IF_EXCEPTION(scope, promise->rejectWithCaughtException(globalObject, scope)); + + promise->resolve(globalObject, sourceCode); + scope.release(); + return promise; + } case SyntheticModuleType::Buffer: { auto source = JSC::SourceCode( JSC::SyntheticSourceProvider::create(generateBufferSourceCode, diff --git a/src/bun.js/bindings/ZigGlobalObject.h b/src/bun.js/bindings/ZigGlobalObject.h index 88149434c..144e43bd8 100644 --- a/src/bun.js/bindings/ZigGlobalObject.h +++ b/src/bun.js/bindings/ZigGlobalObject.h @@ -32,6 +32,7 @@ class EventLoopTask; #include "DOMConstructors.h" #include "DOMWrapperWorld-class.h" #include "DOMIsoSubspaces.h" +#include "BunPlugin.h" // #include "EventTarget.h" // namespace WebCore { @@ -303,6 +304,10 @@ public: this->m_ffiFunctions.append(JSC::Strong<JSC::JSFunction> { vm(), function }); } + BunPlugin::OnLoad onLoadPlugins[BunPluginTargetMax + 1] {}; + BunPlugin::OnResolve onResolvePlugins[BunPluginTargetMax + 1] {}; + BunPluginTarget defaultBunPluginTarget = BunPluginTargetBun; + #include "ZigGeneratedClasses+lazyStructureHeader.h" private: diff --git a/src/bun.js/bindings/bindings.zig b/src/bun.js/bindings/bindings.zig index ea29c99ab..d997bc2cd 100644 --- a/src/bun.js/bindings/bindings.zig +++ b/src/bun.js/bindings/bindings.zig @@ -101,6 +101,23 @@ pub const ZigString = extern struct { return this; } + pub fn substring(this: ZigString, offset: usize) ZigString { + if (this.is16Bit()) { + return ZigString.from16Slice(this.utf16SliceAligned()[@minimum(this.len, offset)..]); + } + + var out = ZigString.init(this.slice()[@minimum(this.len, offset)..]); + if (this.isUTF8()) { + out.markUTF8(); + } + + if (this.isGloballyAllocated()) { + out.mark(); + } + + return out; + } + pub fn utf8ByteLength(this: ZigString) usize { if (this.isUTF8()) { return this.len; @@ -1773,6 +1790,35 @@ pub const JSGlobalObject = extern struct { this.vm().throwError(this, err); } + pub const BunPluginTarget = enum(u8) { + bun = 0, + node = 1, + browser = 2, + }; + extern fn Bun__runOnLoadPlugins(*JSC.JSGlobalObject, ?*const ZigString, *const ZigString, BunPluginTarget) JSValue; + extern fn Bun__runOnResolvePlugins(*JSC.JSGlobalObject, ?*const ZigString, *const ZigString, *const ZigString, BunPluginTarget) JSValue; + + pub fn runOnLoadPlugins(this: *JSGlobalObject, namespace_: ZigString, path: ZigString, target: BunPluginTarget) ?JSValue { + JSC.markBinding(); + const result = Bun__runOnLoadPlugins(this, if (namespace_.len > 0) &namespace_ else null, &path, target); + if (result.isEmptyOrUndefinedOrNull()) { + return null; + } + + return result; + } + + pub fn runOnResolvePlugins(this: *JSGlobalObject, namespace_: ZigString, path: ZigString, source: ZigString, target: BunPluginTarget) ?JSValue { + JSC.markBinding(); + + const result = Bun__runOnResolvePlugins(this, if (namespace_.len > 0) &namespace_ else null, &path, &source, target); + if (result.isEmptyOrUndefinedOrNull()) { + return null; + } + + return result; + } + pub fn createSyntheticModule_(this: *JSGlobalObject, export_names: [*]const ZigString, export_len: usize, value_ptrs: [*]const JSValue, values_len: usize) void { shim.cppFn("createSyntheticModule_", .{ this, export_names, export_len, value_ptrs, values_len }); } diff --git a/src/bun.js/bindings/exports.zig b/src/bun.js/bindings/exports.zig index 2f10e75a4..20bc1195b 100644 --- a/src/bun.js/bindings/exports.zig +++ b/src/bun.js/bindings/exports.zig @@ -243,6 +243,8 @@ pub const ResolvedSource = extern struct { pub const Tag = enum(u64) { javascript = 0, wasm = 1, + object = 2, + file = 3, @"node:buffer" = 1024, @"node:process" = 1025, diff --git a/src/bun.js/bindings/headers-handwritten.h b/src/bun.js/bindings/headers-handwritten.h index c9ac33e21..69b4934e9 100644 --- a/src/bun.js/bindings/headers-handwritten.h +++ b/src/bun.js/bindings/headers-handwritten.h @@ -46,6 +46,12 @@ typedef struct SystemError { typedef void* ArrayBufferSink; +typedef uint8_t BunPluginTarget; +const BunPluginTarget BunPluginTargetBun = 0; +const BunPluginTarget BunPluginTargetBrowser = 1; +const BunPluginTarget BunPluginTargetNode = 2; +const BunPluginTarget BunPluginTargetMax = BunPluginTargetNode; + typedef uint8_t ZigStackFrameCode; const ZigStackFrameCode ZigStackFrameCodeNone = 0; const ZigStackFrameCode ZigStackFrameCodeEval = 1; @@ -178,6 +184,8 @@ typedef struct { } Bun__ArrayBuffer; enum SyntheticModuleType : uint64_t { + ObjectModule = 2, + Buffer = 1024, Process = 1025, Events = 1026, diff --git a/src/bun.js/builtins/BunBuiltinNames.h b/src/bun.js/builtins/BunBuiltinNames.h index 16f7096a6..c8a14a260 100644 --- a/src/bun.js/builtins/BunBuiltinNames.h +++ b/src/bun.js/builtins/BunBuiltinNames.h @@ -99,6 +99,7 @@ using namespace JSC; macro(file) \ macro(filePath) \ macro(fillFromJS) \ + macro(filter) \ macro(finishConsumingStream) \ macro(flush) \ macro(flushAlgorithm) \ @@ -115,6 +116,7 @@ using namespace JSC; macro(hostname) \ macro(href) \ macro(ignoreBOM) \ + macro(importer) \ macro(inFlightCloseRequest) \ macro(inFlightWriteRequest) \ macro(initializeWith) \ @@ -192,6 +194,7 @@ using namespace JSC; macro(sep) \ macro(setBody) \ macro(setStatus) \ + macro(setup) \ macro(sink) \ macro(size) \ macro(start) \ diff --git a/src/bun.js/javascript.zig b/src/bun.js/javascript.zig index 546ab36aa..6da062b23 100644 --- a/src/bun.js/javascript.zig +++ b/src/bun.js/javascript.zig @@ -30,6 +30,7 @@ const logger = @import("../logger.zig"); const Api = @import("../api/schema.zig").Api; const options = @import("../options.zig"); const Bundler = @import("../bundler.zig").Bundler; +const PluginRunner = @import("../bundler.zig").PluginRunner; const ServerEntryPoint = @import("../bundler.zig").ServerEntryPoint; const js_printer = @import("../js_printer.zig"); const js_parser = @import("../js_parser.zig"); @@ -270,6 +271,7 @@ comptime { _ = Bun__queueMicrotask; _ = Bun__handleRejectedPromise; _ = Bun__readOriginTimer; + _ = Bun__onDidAppendPlugin; } } @@ -282,6 +284,18 @@ pub export fn Bun__handleRejectedPromise(global: *JSGlobalObject, promise: *JSC. global.bunVM().runErrorHandler(result, null); } +pub export fn Bun__onDidAppendPlugin(jsc_vm: *VirtualMachine, globalObject: *JSGlobalObject) void { + if (jsc_vm.plugin_runner != null) { + return; + } + + jsc_vm.plugin_runner = PluginRunner{ + .global_object = globalObject, + .allocator = jsc_vm.allocator, + }; + jsc_vm.bundler.linker.plugin_runner = &jsc_vm.plugin_runner.?; +} + // If you read JavascriptCore/API/JSVirtualMachine.mm - https://github.com/WebKit/WebKit/blob/acff93fb303baa670c055cb24c2bad08691a01a0/Source/JavaScriptCore/API/JSVirtualMachine.mm#L101 // We can see that it's sort of like std.mem.Allocator but for JSGlobalContextRef, to support Automatic Reference Counting // Its unavailable on Linux @@ -311,6 +325,8 @@ pub const VirtualMachine = struct { timer: Bun.Timer = Bun.Timer{}, uws_event_loop: ?*uws.Loop = null, + plugin_runner: ?PluginRunner = null, + /// Do not access this field directly /// It exists in the VirtualMachine struct so that /// we don't accidentally make a stack copy of it @@ -671,7 +687,7 @@ pub const VirtualMachine = struct { }; fn _fetch( jsc_vm: *VirtualMachine, - _: *JSGlobalObject, + globalObject: *JSGlobalObject, _specifier: string, _: string, log: *logger.Log, @@ -1054,18 +1070,153 @@ pub const VirtualMachine = struct { } } - const specifier = normalizeSpecifier(_specifier); - - std.debug.assert(std.fs.path.isAbsolute(specifier)); // if this crashes, it means the resolver was skipped. - - const path = Fs.Path.init(specifier); - const loader = jsc_vm.bundler.options.loaders.get(path.name.ext) orelse brk: { + var specifier = normalizeSpecifier(_specifier); + var path = Fs.Path.init(specifier); + const default_loader = jsc_vm.bundler.options.loaders.get(path.name.ext) orelse brk: { if (strings.eqlLong(specifier, jsc_vm.main, true)) { break :brk options.Loader.js; } break :brk options.Loader.file; }; + var loader = default_loader; + var virtual_source: logger.Source = undefined; + var has_virtual_source = false; + var source_code_slice: ZigString.Slice = ZigString.Slice.empty; + defer source_code_slice.deinit(); + + if (jsc_vm.plugin_runner != null) { + const namespace = PluginRunner.extractNamespace(_specifier); + const after_namespace = if (namespace.len == 0) + specifier + else + _specifier[@minimum(namespace.len + 1, _specifier.len)..]; + + if (PluginRunner.couldBePlugin(_specifier)) { + if (globalObject.runOnLoadPlugins(ZigString.init(namespace), ZigString.init(after_namespace), .bun)) |plugin_result| { + if (plugin_result.isException(globalObject.vm()) or plugin_result.isAnyError(globalObject)) { + jsc_vm.runErrorHandler(plugin_result, null); + log.addError(null, logger.Loc.Empty, "Failed to run plugin") catch unreachable; + return error.PluginError; + } + + if (comptime Environment.allow_assert) + std.debug.assert(plugin_result.isObject()); + + if (plugin_result.get(globalObject, "loader")) |loader_value| { + if (!loader_value.isUndefinedOrNull()) { + const loader_string = loader_value.getZigString(globalObject); + if (comptime Environment.allow_assert) + std.debug.assert(loader_string.len > 0); + + if (loader_string.eqlComptime("js")) { + loader = options.Loader.js; + } else if (loader_string.eqlComptime("jsx")) { + loader = options.Loader.jsx; + } else if (loader_string.eqlComptime("tsx")) { + loader = options.Loader.tsx; + } else if (loader_string.eqlComptime("ts")) { + loader = options.Loader.ts; + } else if (loader_string.eqlComptime("json")) { + loader = options.Loader.json; + } else if (loader_string.eqlComptime("toml")) { + loader = options.Loader.toml; + } else if (loader_string.eqlComptime("object")) { + const exports_object: JSValue = @as(?JSValue, brk: { + const exports_value = plugin_result.get(globalObject, "exports") orelse break :brk null; + if (!exports_value.isObject()) { + break :brk null; + } + break :brk exports_value; + }) orelse { + log.addError(null, logger.Loc.Empty, "Expected object loader to return an \"exports\" object") catch unreachable; + return error.PluginError; + }; + return ResolvedSource{ + .allocator = null, + .source_code = ZigString{ + .ptr = @ptrCast([*]const u8, exports_object.asVoid()), + .len = 0, + }, + .specifier = ZigString.init(_specifier), + .source_url = ZigString.init(_specifier), + .hash = 0, + .tag = .object, + }; + } else { + log.addErrorFmt( + null, + logger.Loc.Empty, + jsc_vm.allocator, + "Expected onLoad() plugin \"loader\" to be one of \"js\", \"jsx\", \"tsx\", \"ts\", \"json\", or \"toml\" but received \"{any}\"", + .{loader_string}, + ) catch unreachable; + return error.PluginError; + } + } + } + + if (plugin_result.get(globalObject, "contents")) |code| { + if (code.asArrayBuffer(globalObject)) |array_buffer| { + virtual_source = .{ + .path = path, + .key_path = path, + .contents = array_buffer.byteSlice(), + }; + has_virtual_source = true; + } else if (code.isString()) { + source_code_slice = code.toSlice(globalObject, jsc_vm.allocator); + if (!source_code_slice.allocated) { + if (!strings.isAllASCII(source_code_slice.slice())) { + var allocated = try strings.allocateLatin1IntoUTF8(jsc_vm.allocator, []const u8, source_code_slice.slice()); + source_code_slice.ptr = allocated.ptr; + source_code_slice.len = @truncate(u32, allocated.len); + source_code_slice.allocated = true; + source_code_slice.allocator = jsc_vm.allocator; + } + } + + virtual_source = .{ + .path = path, + .key_path = path, + .contents = source_code_slice.slice(), + }; + has_virtual_source = true; + } + } + + if (!has_virtual_source) { + log.addError(null, logger.Loc.Empty, "Expected onLoad() plugin to return \"contents\" as a string or ArrayBufferView") catch unreachable; + return error.PluginError; + } + } else { + std.debug.assert(std.fs.path.isAbsolute(specifier)); // if this crashes, it means the resolver was skipped. + } + } + } + + const transpiled_result = transpileSourceCode( + jsc_vm, + specifier, + path, + loader, + log, + if (has_virtual_source) &virtual_source else null, + flags, + ); + return transpiled_result; + } + + fn transpileSourceCode( + jsc_vm: *VirtualMachine, + specifier: string, + path: Fs.Path, + loader: options.Loader, + log: *logger.Log, + virtual_source: ?*const logger.Source, + comptime flags: FetchFlags, + ) !ResolvedSource { + const disable_transpilying = comptime flags.disableTranspiling(); switch (loader) { .js, .jsx, .ts, .tsx, .json, .toml => { @@ -1116,6 +1267,7 @@ pub const VirtualMachine = struct { .file_hash = hash, .macro_remappings = macro_remappings, .jsx = jsc_vm.bundler.options.jsx, + .virtual_source = virtual_source, }; if (is_node_override) { @@ -1391,12 +1543,27 @@ pub const VirtualMachine = struct { pub fn resolveMaybeNeedsTrailingSlash(res: *ErrorableZigString, global: *JSGlobalObject, specifier: ZigString, source: ZigString, comptime is_a_file_path: bool, comptime realpath: bool) void { var result = ResolveFunctionResult{ .path = "", .result = null }; + var jsc_vm = vm; + if (jsc_vm.plugin_runner) |plugin_runner| { + if (PluginRunner.couldBePlugin(specifier.slice())) { + const namespace = PluginRunner.extractNamespace(specifier.slice()); + const after_namespace = if (namespace.len == 0) + specifier + else + specifier.substring(namespace.len + 1); + + if (plugin_runner.onResolveJSC(ZigString.init(namespace), after_namespace, source, .bun)) |resolved_path| { + res.* = resolved_path; + return; + } + } + } _resolve(&result, global, specifier.slice(), source.slice(), is_a_file_path, realpath) catch |err| { // This should almost always just apply to dynamic imports const printed = ResolveError.fmt( - vm.allocator, + jsc_vm.allocator, specifier.slice(), source.slice(), err, diff --git a/src/bun.js/modules/ObjectModule.h b/src/bun.js/modules/ObjectModule.h new file mode 100644 index 000000000..42ab4dfa5 --- /dev/null +++ b/src/bun.js/modules/ObjectModule.h @@ -0,0 +1,32 @@ +#include "../bindings/ZigGlobalObject.h" +#include "JavaScriptCore/JSGlobalObject.h" + +namespace Zig { +JSC::SyntheticSourceProvider::SyntheticSourceGenerator +generateObjectModuleSourceCode(JSC::JSGlobalObject *globalObject, + JSC::JSObject *object) { + JSC::VM &vm = globalObject->vm(); + + return [strongObject = JSC::Strong<JSC::JSObject>(vm, object)]( + JSC::JSGlobalObject *lexicalGlobalObject, + JSC::Identifier moduleKey, Vector<JSC::Identifier, 4> &exportNames, + JSC::MarkedArgumentBuffer &exportValues) -> void { + JSC::VM &vm = lexicalGlobalObject->vm(); + GlobalObject *globalObject = + reinterpret_cast<GlobalObject *>(lexicalGlobalObject); + JSC::JSObject *object = strongObject.get(); + + PropertyNameArray properties(vm, PropertyNameMode::Strings, + PrivateSymbolMode::Exclude); + object->getPropertyNames(globalObject, properties, + DontEnumPropertiesMode::Exclude); + + for (auto &entry : properties) { + exportNames.append(entry); + exportValues.append(object->get(globalObject, entry)); + } + strongObject.clear(); + }; +} + +} // namespace Zig
\ No newline at end of file diff --git a/src/bundler.zig b/src/bundler.zig index 0a5f892ad..c5b593507 100644 --- a/src/bundler.zig +++ b/src/bundler.zig @@ -127,6 +127,214 @@ pub const ParseResult = struct { const cache_files = false; +pub const PluginRunner = struct { + global_object: *JSC.JSGlobalObject, + allocator: std.mem.Allocator, + + pub fn extractNamespace(specifier: string) string { + const colon = strings.indexOfChar(specifier, ':') orelse return ""; + return specifier[0..colon]; + } + + pub fn couldBePlugin(specifier: string) bool { + if (strings.lastIndexOfChar(specifier, '.')) |last_dor| { + const ext = specifier[last_dor + 1 ..]; + // '.' followed by either a letter or a non-ascii character + // maybe there are non-ascii file extensions? + // we mostly want to cheaply rule out "../" and ".." and "./" + return ext.len > 0 and ((ext[0] >= 'a' and ext[0] <= 'z') or (ext[0] >= 'A' and ext[0] <= 'Z') or ext[0] > 127); + } + return (!std.fs.path.isAbsolute(specifier) and strings.containsChar(specifier, ':')); + } + + pub fn onResolve( + this: *PluginRunner, + specifier: []const u8, + importer: []const u8, + log: *logger.Log, + loc: logger.Loc, + target: JSC.JSGlobalObject.BunPluginTarget, + ) ?Fs.Path { + var global = this.global_object; + const namespace_slice = extractNamespace(specifier); + const namespace = if (namespace_slice.len > 0 and !strings.eqlComptime(namespace_slice, "file")) + JSC.ZigString.init(namespace_slice) + else + JSC.ZigString.init(""); + const on_resolve_plugin = global.runOnResolvePlugins( + namespace, + JSC.ZigString.init(specifier).substring(if (namespace.len > 0) namespace.len + 1 else 0), + JSC.ZigString.init(importer), + target, + ) orelse return null; + const path_value = on_resolve_plugin.get(global, "path") orelse return null; + if (path_value.isEmptyOrUndefinedOrNull()) return null; + if (!path_value.isString()) { + log.addError(null, loc, "Expected \"path\" to be a string") catch unreachable; + return null; + } + + var file_path = path_value.getZigString(global); + + if (file_path.len == 0) { + log.addError( + null, + loc, + "Expected \"path\" to be a non-empty string in onResolve plugin", + ) catch unreachable; + return null; + } else if + // TODO: validate this better + (file_path.eqlComptime(".") or + file_path.eqlComptime("..") or + file_path.eqlComptime("...") or + file_path.eqlComptime(" ")) + { + log.addError( + null, + loc, + "Invalid file path from onResolve plugin", + ) catch unreachable; + return null; + } + var static_namespace = true; + const user_namespace: JSC.ZigString = brk: { + if (on_resolve_plugin.get(global, "namespace")) |namespace_value| { + if (!namespace_value.isString()) { + log.addError(null, loc, "Expected \"namespace\" to be a string") catch unreachable; + return null; + } + + const namespace_str = namespace_value.getZigString(global); + if (namespace_str.len == 0) { + break :brk JSC.ZigString.init("file"); + } + + if (namespace_str.eqlComptime("file")) { + break :brk JSC.ZigString.init("file"); + } + + if (namespace_str.eqlComptime("bun")) { + break :brk JSC.ZigString.init("bun"); + } + + if (namespace_str.eqlComptime("node")) { + break :brk JSC.ZigString.init("node"); + } + + static_namespace = false; + + break :brk namespace_str; + } + + break :brk JSC.ZigString.init("file"); + }; + + if (static_namespace) { + return Fs.Path.initWithNamespace( + std.fmt.allocPrint(this.allocator, "{any}", .{file_path}) catch unreachable, + user_namespace.slice(), + ); + } else { + return Fs.Path.initWithNamespace( + std.fmt.allocPrint(this.allocator, "{any}", .{file_path}) catch unreachable, + std.fmt.allocPrint(this.allocator, "{any}", .{user_namespace}) catch unreachable, + ); + } + } + + pub fn onResolveJSC( + this: *const PluginRunner, + namespace: JSC.ZigString, + specifier: JSC.ZigString, + importer: JSC.ZigString, + target: JSC.JSGlobalObject.BunPluginTarget, + ) ?JSC.ErrorableZigString { + var global = this.global_object; + const on_resolve_plugin = global.runOnResolvePlugins( + if (namespace.len > 0 and !namespace.eqlComptime("file")) + namespace + else + JSC.ZigString.init(""), + specifier, + importer, + target, + ) orelse return null; + const path_value = on_resolve_plugin.get(global, "path") orelse return null; + if (path_value.isEmptyOrUndefinedOrNull()) return null; + if (!path_value.isString()) { + return JSC.ErrorableZigString.err( + error.JSErrorObject, + JSC.ZigString.init("Expected \"path\" to be a string in onResolve plugin").toErrorInstance(this.global_object).asVoid(), + ); + } + + const file_path = path_value.getZigString(global); + + if (file_path.len == 0) { + return JSC.ErrorableZigString.err( + error.JSErrorObject, + JSC.ZigString.init("Expected \"path\" to be a non-empty string in onResolve plugin").toErrorInstance(this.global_object).asVoid(), + ); + } else if + // TODO: validate this better + (file_path.eqlComptime(".") or + file_path.eqlComptime("..") or + file_path.eqlComptime("...") or + file_path.eqlComptime(" ")) + { + return JSC.ErrorableZigString.err( + error.JSErrorObject, + JSC.ZigString.init("\"path\" is invalid in onResolve plugin").toErrorInstance(this.global_object).asVoid(), + ); + } + var static_namespace = true; + const user_namespace: JSC.ZigString = brk: { + if (on_resolve_plugin.get(global, "namespace")) |namespace_value| { + if (!namespace_value.isString()) { + return JSC.ErrorableZigString.err( + error.JSErrorObject, + JSC.ZigString.init("Expected \"namespace\" to be a string").toErrorInstance(this.global_object).asVoid(), + ); + } + + const namespace_str = namespace_value.getZigString(global); + if (namespace_str.len == 0) { + break :brk JSC.ZigString.init("file"); + } + + if (namespace_str.eqlComptime("file")) { + break :brk JSC.ZigString.init("file"); + } + + if (namespace_str.eqlComptime("bun")) { + break :brk JSC.ZigString.init("bun"); + } + + if (namespace_str.eqlComptime("node")) { + break :brk JSC.ZigString.init("node"); + } + + static_namespace = false; + + break :brk namespace_str; + } + + break :brk JSC.ZigString.init("file"); + }; + + // Our super slow way of cloning the string into memory owned by JSC + var combined_string = std.fmt.allocPrint( + this.allocator, + "{any}:{any}", + .{ user_namespace, file_path }, + ) catch unreachable; + const out = JSC.ZigString.init(combined_string).toValueGC(this.global_object).getZigString(this.global_object); + this.allocator.free(combined_string); + return JSC.ErrorableZigString.ok(out); + } +}; + pub const Bundler = struct { const ThisBundler = @This(); diff --git a/src/import_record.zig b/src/import_record.zig index a933df2fb..fcf5b1c46 100644 --- a/src/import_record.zig +++ b/src/import_record.zig @@ -144,6 +144,12 @@ pub const ImportRecord = struct { tag: Tag = Tag.none, + /// Tell the printer to print the record as "foo:my-path" instead of "path" + /// where "foo" is the namespace + /// + /// Used to prevent running resolve plugins multiple times for the same path + print_namespace_in_path: bool = false, + pub const Tag = enum { none, react_refresh, diff --git a/src/js_printer.zig b/src/js_printer.zig index e6f31aa85..716da988d 100644 --- a/src/js_printer.zig +++ b/src/js_printer.zig @@ -1424,7 +1424,7 @@ pub fn NewPrinter( } p.print("("); - p.printQuotedUTF8(record.path.text, true); + p.printImportRecordPath(&record); p.print(")"); return; } @@ -1442,7 +1442,7 @@ pub fn NewPrinter( // Allow it to fail at runtime, if it should p.print("import("); - p.printQuotedUTF8(record.path.text, true); + p.printImportRecordPath(&record); p.print(")"); if (leading_interior_comments.len > 0) { @@ -3132,7 +3132,7 @@ pub fn NewPrinter( } p.print("from"); p.printSpace(); - p.printQuotedUTF8(p.import_records[s.import_record_index].path.text, false); + p.printImportRecordPath(&p.import_records[s.import_record_index]); p.printSemicolonAfterStatement(); }, .s_export_clause => |s| { @@ -3377,7 +3377,7 @@ pub fn NewPrinter( } p.print("}=import.meta.require("); - p.printQuotedUTF8(import_record.path.text, true); + p.printImportRecordPath(&import_record); p.print(")"); p.printSemicolonAfterStatement(); p.print("export {"); @@ -3445,7 +3445,7 @@ pub fn NewPrinter( p.printSpace(); p.print("from"); p.printSpace(); - p.printQuotedUTF8(import_record.path.text, false); + p.printImportRecordPath(&import_record); p.printSemicolonAfterStatement(); }, .s_local => |s| { @@ -3708,14 +3708,18 @@ pub fn NewPrinter( }, .import_path => { if (s.default_name) |name| { - const quotes = p.bestQuoteCharForString(p.import_records[s.import_record_index].path.text, true); - p.print("var "); p.printSymbol(name.ref.?); p.print(" = "); - p.print(quotes); - p.printUTF8StringEscapedQuotes(p.import_records[s.import_record_index].path.text, quotes); - p.print(quotes); + p.printImportRecordPath(&p.import_records[s.import_record_index]); + p.printSemicolonAfterStatement(); + } else if (p.import_records[s.import_record_index].contains_import_star) { + // this case is particularly important for running files without an extension in bun's runtime + p.print("var "); + p.printSymbol(s.namespace_ref); + p.print(" = {default:"); + p.printImportRecordPath(&p.import_records[s.import_record_index]); + p.print("}"); p.printSemicolonAfterStatement(); } return; @@ -3726,13 +3730,10 @@ pub fn NewPrinter( if (import_record.print_mode == .napi_module) { p.printIndent(); - const quotes = p.bestQuoteCharForString(import_record.path.text, true); p.print("var "); p.printSymbol(s.namespace_ref); p.print(" = import.meta.require("); - p.print(quotes); - p.printUTF8StringEscapedQuotes(import_record.path.text, quotes); - p.print(quotes); + p.printImportRecordPath(import_record); p.print(")"); p.printSemicolonAfterStatement(); } @@ -3960,7 +3961,7 @@ pub fn NewPrinter( p.printSpace(); } - p.printQuotedUTF8(p.import_records[s.import_record_index].path.text, false); + p.printImportRecordPath(&p.import_records[s.import_record_index]); p.printSemicolonAfterStatement(); }, .s_block => |s| { @@ -4047,6 +4048,21 @@ pub fn NewPrinter( p.print("module.exports"); } + pub fn printImportRecordPath(p: *Printer, import_record: *const ImportRecord) void { + const quote = p.bestQuoteCharForString(import_record.path.text, false); + if (import_record.print_namespace_in_path and import_record.path.namespace.len > 0 and !strings.eqlComptime(import_record.path.namespace, "file")) { + p.print(quote); + p.print(import_record.path.namespace); + p.print(":"); + p.print(import_record.path.text); + p.print(quote); + } else { + p.print(quote); + p.print(import_record.path.text); + p.print(quote); + } + } + pub fn printBundledImport(p: *Printer, record: importRecord.ImportRecord, s: *S.Import) void { if (record.is_internal) { return; diff --git a/src/linker.zig b/src/linker.zig index e193cd53a..7a0a66a7e 100644 --- a/src/linker.zig +++ b/src/linker.zig @@ -37,6 +37,7 @@ const ResolverType = Resolver.Resolver; const Runtime = @import("./runtime.zig").Runtime; const URL = @import("url.zig").URL; const JSC = @import("javascript_core"); +const PluginRunner = @import("./bundler.zig").PluginRunner; pub const CSSResolveError = error{ResolveError}; pub const OnImportCallback = fn (resolve_result: *const Resolver.Result, import_record: *ImportRecord, origin: URL) void; @@ -57,6 +58,8 @@ pub const Linker = struct { import_counter: usize = 0, tagged_resolutions: TaggedResolution = TaggedResolution{}, + plugin_runner: ?*PluginRunner = null, + onImportCSS: ?OnImportCallback = null, pub const runtime_source_path = "bun:wrap"; @@ -315,6 +318,34 @@ pub const Linker = struct { } } + if (linker.plugin_runner) |runner| { + if (PluginRunner.couldBePlugin(import_record.path.text)) { + if (runner.onResolve( + import_record.path.text, + file_path.text, + linker.log, + import_record.range.loc, + if (is_bun) + JSC.JSGlobalObject.BunPluginTarget.bun + else if (linker.options.platform == .browser) + JSC.JSGlobalObject.BunPluginTarget.browser + else + JSC.JSGlobalObject.BunPluginTarget.node, + )) |path| { + import_record.path = try linker.generateImportPath( + source_dir, + path.text, + false, + path.namespace, + origin, + import_path_format, + ); + import_record.print_namespace_in_path = true; + continue; + } + } + } + if (comptime allow_import_from_bundle) { if (linker.options.node_modules_bundle) |node_modules_bundle| { if (Resolver.isPackagePath(import_record.path.text)) { @@ -507,6 +538,13 @@ pub const Linker = struct { continue; } + if (comptime is_bun) { + // make these happen at runtime + if (import_record.kind == .require or import_record.kind == .require_resolve) { + continue; + } + } + had_resolve_errors = true; if (import_record.path.text.len > 0 and Resolver.isPackagePath(import_record.path.text)) { @@ -639,9 +677,12 @@ pub const Linker = struct { return Fs.Path.initWithNamespace(source_path, "node"); } - var relative_name = linker.fs.relative(source_dir, source_path); - - return Fs.Path.initWithPretty(source_path, relative_name); + if (strings.eqlComptime(namespace, "bun") or strings.eqlComptime(namespace, "file") or namespace.len == 0) { + var relative_name = linker.fs.relative(source_dir, source_path); + return Fs.Path.initWithPretty(source_path, relative_name); + } else { + return Fs.Path.initWithNamespace(source_path, namespace); + } }, .relative => { var relative_name = linker.fs.relative(source_dir, source_path); @@ -771,9 +812,20 @@ pub const Linker = struct { .napi => { import_record.print_mode = .napi_module; }, - .wasm, .file => { + .wasm => { import_record.print_mode = .import_path; }, + .file => { + + // if we're building for web/node, always print as import path + // if we're building for bun + // it's more complicated + // loader plugins could be executed between when this is called and the import is evaluated + // but we want to preserve the semantics of "file" returning import paths for compatibiltiy with frontend frameworkss + if (!linker.options.platform.isBun()) { + import_record.print_mode = .import_path; + } + }, else => {}, } |