mirror of
https://github.com/oven-sh/bun
synced 2026-02-02 15:08:46 +00:00
Plugin API (#1199)
* Plugin API * Fix the bugs * Implement `"object"` loader Co-authored-by: Jarred Sumner <709451+Jarred-Sumner@users.noreply.github.com>
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
"he": "^1.2.0",
|
||||
"html-entities": "^2.3.3",
|
||||
"prettier": "^2.4.1",
|
||||
"svelte": "^3.50.0",
|
||||
"typescript": "4.6.3"
|
||||
},
|
||||
"version": "0.0.0"
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
#include "root.h"
|
||||
#include "JavaScriptCore/JSCInlines.h"
|
||||
|
||||
#include "JavaScriptCore/JavaScript.h"
|
||||
#include "wtf/FileSystem.h"
|
||||
|
||||
505
src/bun.js/bindings/BunPlugin.cpp
Normal file
505
src/bun.js/bindings/BunPlugin.cpp
Normal file
@@ -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);
|
||||
}
|
||||
85
src/bun.js/bindings/BunPlugin.h
Normal file
85
src/bun.js/bindings/BunPlugin.h
Normal file
@@ -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
|
||||
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) \
|
||||
|
||||
@@ -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,
|
||||
|
||||
32
src/bun.js/modules/ObjectModule.h
Normal file
32
src/bun.js/modules/ObjectModule.h
Normal file
@@ -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
|
||||
208
src/bundler.zig
208
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();
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 => {},
|
||||
}
|
||||
|
||||
5
test/bun.js/hello.svelte
Normal file
5
test/bun.js/hello.svelte
Normal file
@@ -0,0 +1,5 @@
|
||||
<script>
|
||||
let name = "world";
|
||||
</script>
|
||||
|
||||
<h1>Hello {name}!</h1>
|
||||
5
test/bun.js/hello2.svelte
Normal file
5
test/bun.js/hello2.svelte
Normal file
@@ -0,0 +1,5 @@
|
||||
<script>
|
||||
let name = "world";
|
||||
</script>
|
||||
|
||||
<h1>Hello {name}!</h1>
|
||||
212
test/bun.js/plugins.test.ts
Normal file
212
test/bun.js/plugins.test.ts
Normal file
@@ -0,0 +1,212 @@
|
||||
import { it, expect, describe, afterAll } from "bun:test";
|
||||
import { resolve } from "path";
|
||||
|
||||
Bun.plugin({
|
||||
name: "boop beep beep",
|
||||
setup(builder) {
|
||||
builder.onResolve({ filter: /boop/, namespace: "beep" }, () => ({
|
||||
path: "boop",
|
||||
namespace: "beep",
|
||||
}));
|
||||
|
||||
builder.onLoad({ filter: /boop/, namespace: "beep" }, () => ({
|
||||
contents: `export default 42;`,
|
||||
loader: "js",
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
var objectModuleResult = {
|
||||
hello: "world",
|
||||
};
|
||||
Bun.plugin({
|
||||
name: "an object module",
|
||||
setup(builder) {
|
||||
builder.onResolve({ filter: /.*/, namespace: "obj" }, ({ path }) => ({
|
||||
path,
|
||||
namespace: "obj",
|
||||
}));
|
||||
|
||||
builder.onLoad({ filter: /.*/, namespace: "obj" }, () => ({
|
||||
exports: objectModuleResult,
|
||||
loader: "object",
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
Bun.plugin({
|
||||
name: "svelte loader",
|
||||
setup(builder) {
|
||||
var { compile } = require("svelte/compiler");
|
||||
var { readFileSync } = require("fs");
|
||||
builder.onLoad({ filter: /\.svelte$/ }, ({ path }) => ({
|
||||
contents: compile(readFileSync(path, "utf8"), {
|
||||
filename: path,
|
||||
generate: "ssr",
|
||||
}).js.code,
|
||||
loader: "js",
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
var failingObject;
|
||||
Bun.plugin({
|
||||
name: "failing loader",
|
||||
setup(builder) {
|
||||
builder.onResolve({ filter: /.*/, namespace: "fail" }, ({ path }) => ({
|
||||
path,
|
||||
namespace: "fail",
|
||||
}));
|
||||
builder.onLoad({ filter: /.*/, namespace: "fail" }, () => failingObject);
|
||||
},
|
||||
});
|
||||
|
||||
var laterCode = "";
|
||||
|
||||
Bun.plugin({
|
||||
name: "delayed loader",
|
||||
setup(builder) {
|
||||
builder.onResolve({ filter: /.*/, namespace: "delay" }, ({ path }) => ({
|
||||
namespace: "delay",
|
||||
path,
|
||||
}));
|
||||
|
||||
builder.onLoad({ filter: /.*/, namespace: "delay" }, ({ path }) => ({
|
||||
contents: laterCode,
|
||||
loader: "js",
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
describe("require", () => {
|
||||
it("SSRs `<h1>Hello world!</h1>` with Svelte", () => {
|
||||
const { default: App } = require("./hello.svelte");
|
||||
const { html } = App.render();
|
||||
|
||||
expect(html).toBe("<h1>Hello world!</h1>");
|
||||
});
|
||||
|
||||
it("beep:boop returns 42", () => {
|
||||
const result = require("beep:boop");
|
||||
expect(result.default).toBe(42);
|
||||
});
|
||||
|
||||
it("object module works", () => {
|
||||
const result = require("obj:boop");
|
||||
expect(result.hello).toBe(objectModuleResult.hello);
|
||||
objectModuleResult.there = true;
|
||||
const result2 = require("obj:boop2");
|
||||
expect(result.there).toBe(undefined);
|
||||
expect(result2.there).toBe(objectModuleResult.there);
|
||||
expect(result2.there).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("dynamic import", () => {
|
||||
it("SSRs `<h1>Hello world!</h1>` with Svelte", async () => {
|
||||
const { default: App } = await import("./hello.svelte");
|
||||
const { html } = App.render();
|
||||
|
||||
expect(html).toBe("<h1>Hello world!</h1>");
|
||||
});
|
||||
|
||||
it("beep:boop returns 42", async () => {
|
||||
const result = await import("beep:boop");
|
||||
expect(result.default).toBe(42);
|
||||
});
|
||||
});
|
||||
|
||||
describe("import statement", () => {
|
||||
it("SSRs `<h1>Hello world!</h1>` with Svelte", async () => {
|
||||
laterCode = `
|
||||
import Hello from "${resolve(import.meta.dir, "hello2.svelte")}";
|
||||
export default Hello;
|
||||
`;
|
||||
const { default: SvelteApp } = await import("delay:hello2.svelte");
|
||||
const { html } = SvelteApp.render();
|
||||
|
||||
expect(html).toBe("<h1>Hello world!</h1>");
|
||||
});
|
||||
});
|
||||
|
||||
describe("errors", () => {
|
||||
it("valid loaders work", () => {
|
||||
const validLoaders = ["js", "jsx", "ts", "tsx"];
|
||||
const inputs = [
|
||||
"export default 'hi';",
|
||||
"export default 'hi';",
|
||||
"export default 'hi';",
|
||||
"export default 'hi';",
|
||||
];
|
||||
for (let i = 0; i < validLoaders.length; i++) {
|
||||
const loader = validLoaders[i];
|
||||
const input = inputs[i];
|
||||
failingObject = { contents: input, loader };
|
||||
expect(require(`fail:my-file-${loader}`).default).toBe("hi");
|
||||
}
|
||||
});
|
||||
|
||||
it("invalid loaders throw", () => {
|
||||
const invalidLoaders = ["blah", "blah2", "blah3", "blah4"];
|
||||
const inputs = [
|
||||
"body { background: red; }",
|
||||
"<h1>hi</h1>",
|
||||
'{"hi": "there"}',
|
||||
"hi",
|
||||
];
|
||||
for (let i = 0; i < invalidLoaders.length; i++) {
|
||||
const loader = invalidLoaders[i];
|
||||
const input = inputs[i];
|
||||
failingObject = { contents: input, loader };
|
||||
try {
|
||||
require(`fail:my-file-${loader}`);
|
||||
throw -1;
|
||||
} catch (e) {
|
||||
if (e === -1) {
|
||||
throw new Error("Expected error");
|
||||
}
|
||||
expect(e.message.length > 0).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("transpiler errors work", () => {
|
||||
const invalidLoaders = ["ts"];
|
||||
const inputs = ["const x: string = -NaNAn../!!;"];
|
||||
for (let i = 0; i < invalidLoaders.length; i++) {
|
||||
const loader = invalidLoaders[i];
|
||||
const input = inputs[i];
|
||||
failingObject = { contents: input, loader };
|
||||
try {
|
||||
require(`fail:my-file-${loader}-3`);
|
||||
throw -1;
|
||||
} catch (e) {
|
||||
if (e === -1) {
|
||||
throw new Error("Expected error");
|
||||
}
|
||||
expect(e.message.length > 0).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("invalid onLoad objects throw", () => {
|
||||
const invalidOnLoadObjects = [
|
||||
{},
|
||||
{ contents: -1 },
|
||||
{ contents: "", loader: -1 },
|
||||
{ contents: "", loader: "klz", resolveDir: -1 },
|
||||
];
|
||||
for (let i = 0; i < invalidOnLoadObjects.length; i++) {
|
||||
failingObject = invalidOnLoadObjects[i];
|
||||
try {
|
||||
require(`fail:my-file-${i}-2`);
|
||||
throw -1;
|
||||
} catch (e) {
|
||||
if (e === -1) {
|
||||
throw new Error("Expected error");
|
||||
}
|
||||
expect(e.message.length > 0).toBe(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user