Skip to content
143 changes: 87 additions & 56 deletions src/js/builtins/BundlerPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,79 +403,110 @@ export function runOnResolvePlugins(this: BundlerPlugin, specifier, inputNamespa

var promiseResult: any = (async (inputPath, inputNamespace, importer, kind) => {
var { onResolve, onLoad } = this;
var results = onResolve.$get(inputNamespace);
if (!results) {
this.onResolveAsync(internalID, null, null, null);
return null;
}

for (let [filter, callback] of results) {
if (filter.test(inputPath)) {
var result = callback({
path: inputPath,
importer,
namespace: inputNamespace,
resolveDir: inputNamespace === "file" ? require("node:path").dirname(importer) : undefined,
kind,
// pluginData
});
const tryNamespace = async (matchNamespace: string, matchPath: string) => {
var results = onResolve.$get(matchNamespace);
if (!results) {
return false;
}

while (result && $isPromise(result) && $peekPromiseStatus(result) === 1) {
result = $peekPromiseSettledValue(result);
}
for (let [filter, callback] of results) {
if (filter.test(matchPath)) {
var result = callback({
path: matchPath,
importer,
namespace: matchNamespace,
resolveDir: inputNamespace === "file" ? require("node:path").dirname(importer) : undefined,
kind,
// pluginData
});

while (result && $isPromise(result) && $peekPromiseStatus(result) === 1) {
result = $peekPromiseSettledValue(result);
}

if (result && $isPromise(result)) {
result = await result;
}
if (result && $isPromise(result)) {
result = await result;
}

if (!result || !$isObject(result)) {
continue;
}
if (!result || !$isObject(result)) {
continue;
}

var { path, namespace: userNamespace = inputNamespace, external } = result;
if (path !== undefined && typeof path !== "string") {
throw new TypeError("onResolve plugins 'path' field must be a string if provided");
}
var { path, namespace: userNamespace = inputNamespace, external } = result;
if (path !== undefined && typeof path !== "string") {
throw new TypeError("onResolve plugins 'path' field must be a string if provided");
}

if (result.namespace !== undefined && typeof result.namespace !== "string") {
throw new TypeError("onResolve plugins 'namespace' field must be a string if provided");
}
if (result.namespace !== undefined && typeof result.namespace !== "string") {
throw new TypeError("onResolve plugins 'namespace' field must be a string if provided");
}

if (!path) {
continue;
}
if (!path) {
continue;
}

if (!userNamespace) {
userNamespace = inputNamespace;
}
if (typeof external !== "boolean" && !$isUndefinedOrNull(external)) {
throw new TypeError('onResolve plugins "external" field must be boolean or unspecified');
}
if (!userNamespace) {
userNamespace = inputNamespace;
}
if (typeof external !== "boolean" && !$isUndefinedOrNull(external)) {
throw new TypeError('onResolve plugins "external" field must be boolean or unspecified');
}

if (!external) {
if (userNamespace === "file") {
if (process.platform !== "win32") {
if (path[0] !== "/" || path.includes("..")) {
throw new TypeError('onResolve plugin "path" must be absolute when the namespace is "file"');
if (!external) {
if (userNamespace === "file") {
if (process.platform !== "win32") {
if (path[0] !== "/" || path.includes("..")) {
throw new TypeError('onResolve plugin "path" must be absolute when the namespace is "file"');
}
} else {
if (require("node:path").isAbsolute(path) === false || path.includes("..")) {
throw new TypeError('onResolve plugin "path" must be absolute when the namespace is "file"');
}
}
} else {
if (require("node:path").isAbsolute(path) === false || path.includes("..")) {
throw new TypeError('onResolve plugin "path" must be absolute when the namespace is "file"');
}
if (userNamespace === "dataurl") {
if (!path.startsWith("data:")) {
throw new TypeError('onResolve plugin "path" must start with "data:" when the namespace is "dataurl"');
}
}
}
if (userNamespace === "dataurl") {
if (!path.startsWith("data:")) {
throw new TypeError('onResolve plugin "path" must start with "data:" when the namespace is "dataurl"');

if (userNamespace && userNamespace !== "file" && (!onLoad || !onLoad.$has(userNamespace))) {
throw new TypeError(`Expected onLoad plugin for namespace ${userNamespace} to exist`);
}
}
this.onResolveAsync(internalID, path, userNamespace, external);
return true;
}
}

if (userNamespace && userNamespace !== "file" && (!onLoad || !onLoad.$has(userNamespace))) {
throw new TypeError(`Expected onLoad plugin for namespace ${userNamespace} to exist`);
return false;
};

// Peek before awaiting so a synchronous callback stays on the synchronous path.
var matched = tryNamespace(inputNamespace, inputPath);
if ($peekPromiseStatus(matched) === 1 ? $peekPromiseSettledValue(matched) : await matched) {
return null;
}

// Also offer "ns:rest" to onResolve({ namespace: "ns" }) with the stripped path.
if (inputNamespace === "file") {
var colon = inputPath.indexOf(":");
if (colon > 0) {
var prefix = inputPath.slice(0, colon);
var isDriveLetter =
process.platform === "win32" &&
colon === 1 &&
inputPath.length > 2 &&
(inputPath.charCodeAt(0) | 0x20) >= 97 &&
(inputPath.charCodeAt(0) | 0x20) <= 122 &&
(inputPath.charCodeAt(2) === 47 || inputPath.charCodeAt(2) === 92);
if (!isDriveLetter && prefix !== "file") {
matched = tryNamespace(prefix, inputPath.slice(colon + 1));
if ($peekPromiseStatus(matched) === 1 ? $peekPromiseSettledValue(matched) : await matched) {
return null;
}
}
this.onResolveAsync(internalID, path, userNamespace, external);
return null;
}
}

Expand Down
37 changes: 27 additions & 10 deletions src/jsc/bindings/BunPlugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -797,13 +797,8 @@ std::optional<String> BunPlugin::OnLoad::resolveVirtualModule(const String& path
return virtualModules->contains(path) ? std::optional<String> { path } : std::nullopt;
}

EncodedJSValue BunPlugin::OnResolve::run(JSC::JSGlobalObject* globalObject, const BunString* namespaceString, const BunString* path, const BunString* importer)
static EncodedJSValue runOnResolveGroup(JSC::JSGlobalObject* globalObject, BunPlugin::Group& group, const WTF::String& pathString, const BunString* importer)
{
Group* groupPtr = this->group(namespaceString ? namespaceString->toWTFString(BunString::ZeroCopy) : String());
if (groupPtr == nullptr) {
return JSValue::encode(jsUndefined());
}
Group& group = *groupPtr;
auto& filters = group.filters;

if (filters.size() == 0) {
Expand All @@ -813,7 +808,6 @@ EncodedJSValue BunPlugin::OnResolve::run(JSC::JSGlobalObject* globalObject, cons
auto& callbacks = group.callbacks;
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
WTF::String pathString = path->toWTFString(BunString::ZeroCopy);

JSC::MarkedArgumentBuffer matchedCallbacks;
matchedCallbacks.ensureCapacity(filters.size());
Expand Down Expand Up @@ -845,11 +839,9 @@ EncodedJSValue BunPlugin::OnResolve::run(JSC::JSGlobalObject* globalObject, cons

JSC::JSObject* paramsObject = JSC::constructEmptyObject(globalObject, globalObject->objectPrototype(), 2);
const auto& builtinNames = WebCore::builtinNames(vm);
auto* pathJS = Bun::toJS(globalObject, *path);
RETURN_IF_EXCEPTION(scope, {});
paramsObject->putDirect(
vm, builtinNames.pathPublicName(),
pathJS);
jsString(vm, pathString));
auto* importerJS = Bun::toJS(globalObject, *importer);
RETURN_IF_EXCEPTION(scope, {});
paramsObject->putDirect(
Expand Down Expand Up @@ -898,6 +890,31 @@ EncodedJSValue BunPlugin::OnResolve::run(JSC::JSGlobalObject* globalObject, cons
return JSValue::encode(JSC::jsUndefined());
}

EncodedJSValue BunPlugin::OnResolve::run(JSC::JSGlobalObject* globalObject, const BunString* namespaceString, const BunString* path, const BunString* importer)
{
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);

WTF::String nsString = namespaceString ? namespaceString->toWTFString(BunString::ZeroCopy) : String();
WTF::String pathString = path->toWTFString(BunString::ZeroCopy);

if (Group* groupPtr = this->group(nsString)) {
EncodedJSValue result = runOnResolveGroup(globalObject, *groupPtr, pathString, importer);
RETURN_IF_EXCEPTION(scope, {});
if (!JSValue::decode(result).isUndefined()) {
RELEASE_AND_RETURN(scope, result);
}
}

// Also offer the full "ns:path" to onResolve({ filter: /^ns:/ }) like Bun.build does.
if (!nsString.isEmpty() && !this->fileNamespace.filters.isEmpty()) {
WTF::String fullSpecifier = makeString(nsString, ":"_s, pathString);
RELEASE_AND_RETURN(scope, runOnResolveGroup(globalObject, this->fileNamespace, fullSpecifier, importer));
}

return JSValue::encode(JSC::jsUndefined());
}

} // namespace Zig

extern "C" JSC::EncodedJSValue Bun__runOnResolvePlugins(Zig::GlobalObject* globalObject, const BunString* namespaceString, const BunString* path, const BunString* from, BunPluginTarget target)
Expand Down
36 changes: 25 additions & 11 deletions src/jsc/bindings/JSBundlerPlugin.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,8 @@ void BundlerPlugin::NamespaceList::append(JSC::VM& vm, JSC::RegExp* filter, Stri
nsGroup->append(WTF::move(filter_regexp));
}

static bool anyMatchesForNamespace(JSC::VM& vm, BundlerPlugin::NamespaceList& list, BunString* namespaceStr, BunString* path)
static bool anyMatchesForNamespace(JSC::VM& vm, BundlerPlugin::NamespaceList& list, const String& namespaceString, const String& pathString)
{
auto namespaceString = namespaceStr ? namespaceStr->transferToWTFString() : String();
auto pathString = path->transferToWTFString();

if (list.fileNamespace.isEmpty() && list.namespaces.isEmpty())
return false;

unsigned index = 0;
auto* group = list.group(namespaceString, index);
if (group == nullptr) {
Expand All @@ -102,11 +96,31 @@ static bool anyMatchesForNamespace(JSC::VM& vm, BundlerPlugin::NamespaceList& li
}
bool BundlerPlugin::anyMatchesCrossThread(JSC::VM& vm, BunString* namespaceStr, BunString* path, bool isOnLoad)
{
if (isOnLoad) {
return anyMatchesForNamespace(vm, this->onLoad, namespaceStr, path);
} else {
return anyMatchesForNamespace(vm, this->onResolve, namespaceStr, path);
auto namespaceString = namespaceStr ? namespaceStr->transferToWTFString() : String();
auto pathString = path->transferToWTFString();

auto& list = isOnLoad ? this->onLoad : this->onResolve;
if (list.fileNamespace.isEmpty() && list.namespaces.isEmpty())
return false;

if (anyMatchesForNamespace(vm, list, namespaceString, pathString))
return true;

// onResolve: also offer "ns:rest" to the "ns" group with the stripped path.
if (!isOnLoad && (namespaceString.isEmpty() || namespaceString == "file"_s) && !list.namespaces.isEmpty()) {
if (auto colon = pathString.find(':'); colon != WTF::notFound && colon != 0) {
#if OS(WINDOWS)
if (colon == 1 && pathString.length() > 2 && isASCIIAlpha(pathString[0]) && (pathString[2] == '/' || pathString[2] == '\\'))
return false;
#endif
auto prefixNamespace = pathString.left(colon);
auto afterNamespace = pathString.substring(colon + 1);
if (anyMatchesForNamespace(vm, list, prefixNamespace, afterNamespace))
return true;
}
}

return false;
}

static const HashTableValue JSBundlerPluginHashTable[] = {
Expand Down
Loading