Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bun runtime plugin onResolve doesn't filter non-file: protocol imports #9863

Open
TomasHubelbauer opened this issue Apr 2, 2024 · 33 comments
Labels
bug Something isn't working

Comments

@TomasHubelbauer
Copy link

What version of Bun is running?

1.1.0+5903a6141

What platform is your computer?

Darwin 23.4.0 arm64 arm

What steps can reproduce the bug?

See my repro repo: https://github.com/TomasHubelbauer/bun-runtime-plugin-onResolve-custom-protocol

Make a runtime plugin with an onResolve hook:

import { plugin } from "bun";

await plugin({
  name: "test",
  async setup(build) {
    console.log("Plugin 1 loaded!", build);

    build.onResolve({ filter: /\.demo$/ }, (args) => {
      console.log("onResolve1", args);

      return {
        path: './demo1.ts'
      };
    });
  },
});

Register it in bunfig.toml: preload = ["./plugin.ts"].

Run a script with an import that should get captured by the plugin: bun index.ts:

import demo1 from '1.demo';

console.log('demo 1', demo1);

So far everything works as expected:

Plugin 1 loaded! {
  target: "bun",
  onLoad: [Function: onLoad],
  onResolve: [Function: onResolve],
  module: [Function: module],
}
onResolve1 {
  path: "1.demo",
  importer: "/Users/tom/Desktop/bun-runtime-plugin-onResolve/index.ts",
}
demo 1 Hello, 1!

Change the plugin to filter for a custom protocol:

import { plugin } from "bun";

await plugin({
  name: "test",
  async setup(build) {
    console.log("Plugin 2 loaded!", build);

    build.onResolve({ filter: /^demo:/ }, (args) => {
      console.log("onResolve2", args);

      return {
        path: './demo2.ts'
      };
    });
  },
});

Change the import and run the script again:

import demo2 from 'demo:2';

console.log('demo 2', demo2);

Now we see a problem:

Plugin 2 loaded! {
  target: "bun",
  onLoad: [Function: onLoad],
  onResolve: [Function: onResolve],
  module: [Function: module],
}
error: Cannot find package "demo:2" from "/…/index.ts"

Notice that onResolve2 was never even called.

This might be a design decision, not a real bug, if I learn as much, I will contribute a documentation change explaining this.

What is the expected behavior?

Plugin 2 loaded! {
  target: "bun",
  onLoad: [Function: onLoad],
  onResolve: [Function: onResolve],
  module: [Function: module],
}
onResolve2 {
  path: "demo:2",
  importer: "/Users/tom/Desktop/bun-runtime-plugin-onResolve/index.ts",
}
demo 2 Hello, 2!

What do you see instead?

A runtime error in module resolution not preceded by the custom onResolve hook call.

Additional information

This might not be a bug but a design decision. I ended up trying this, because build.module doesn't accept a regex for the module name (so that I could virtualize all modules on a custom protocol) so I ended up looking at onResolve. Ultimately I would prefer a change to build.module that allows for regexes, but this might also be worth supporting IMO.

@TomasHubelbauer TomasHubelbauer added the bug Something isn't working label Apr 2, 2024
@guest271314

This comment was marked as off-topic.

@TomasHubelbauer
Copy link
Author

That's interesting. I found #4458 talking about adding import map support to the bundler. But I wonder how one would define import maps for the runtime? Bunfig?

@TomasHubelbauer
Copy link
Author

Ultimately my interest lies in being able to make up a module from thin air when using a custom protocol in the import. Ideally build.module would accept a regex not just a string which would allow me to do that. I ended up looking at onResolve and onBuild as a hacky combination to achieve the same. My logic was that onResolve could catch these imports and drop a temporary file and redirect to it under a different extension and onLoad would pick up that and return the file as { contents } and delete it (I don't want it to hand around on the disk - this would just be a hack in lieu of build.module regex support) so import maps wouldn't help in this specific case.

@guest271314

This comment was marked as off-topic.

@TomasHubelbauer
Copy link
Author

TomasHubelbauer commented Apr 3, 2024

This is related to bun build, but what about the bun index.ts case - no build step, just Bun running a file at runtime with some plugins in the Bunfig preload. I would expect import map support would include the ability to specify (and mutate) the import map at runtime the same way that is possible in the browser.

Edit: turns out mutating import maps at runtime is not possible in browsers, I mis-remembered: WICG/import-maps#92.

@guest271314

This comment was marked as off-topic.

@TomasHubelbauer
Copy link
Author

I am not attempting to create a custom protocol, I have an existing custom protocol which I am looking to bridge Bun into supporting. It is possible that runtime plugins are intentionally locked into the file: protocol only, but if that design decision is not strongly held, I would definitely like runtime plugins to support any protocol / filter against any import specifier, not just the ones with implicit file: protocol.

Node has experimental support for custom loaders which I believe are roughly equivalent to Bun's runtime plugins and AFAICT it should be possible to create a custom loader which matches the URL by protocol, here demonstrated on a rudimentary HTTP/HTTPS loader:
https://nodejs.org/api/module.html#import-from-https

Whether it is by build.module module virtualization with regex module name matching or extending onResolve and onLoad to support non-file protocol, this would enable something similar to what's possible there.

@guest271314

This comment was marked as off-topic.

@TomasHubelbauer
Copy link
Author

Whatever is on the other side of the import is dynamic, I can't bake it in or use a static data: protocol import.

@guest271314

This comment was marked as off-topic.

@guest271314

This comment was marked as off-topic.

@guest271314

This comment was marked as off-topic.

@guest271314

This comment was marked as off-topic.

@guest271314

This comment was marked as off-topic.

@guest271314

This comment was marked as off-topic.

@TomasHubelbauer
Copy link
Author

Thanks for the PR, I noted in the repro repo README and in my initial post in this issue that I can't use build.module because it doesn't take a regex. demo:2 is not meant to be a module name, the plugin is meant to be able to handle any resolution starting with demo:.

As far as import maps go, I was just pondering how it could look, I am aware Bun doesn't support import maps at the moment, but should it, I would hope for an implementation where the import map can be passed to bun index.ts via the CLI or Bunfig or some other means, not only baked into a build via bun build.

I am aware of the Node loader support, I've linked it previously. This is something I am hoping to do an equivalent of in Bun, because it seems like it can support custom protocols as the HTTP/HTTPS loader demonstrates. I believe if build.module took a regex as an option in addition to a string, it would be possible to add this type of support to Bun as well.

I've been able to build Bun locally and while I am not at al proficient in Zig, I think I've gathered enough context by now to maybe be able to contribute regex support to build.module. I'm not very good at debugging Zig yet, though, so it's a lot of wading through log statements.

@guest271314

This comment was marked as off-topic.

@TomasHubelbauer
Copy link
Author

I cannot write out all the specifiers I am expecting as the specifiers are dynamic, not known at compile time.

@guest271314

This comment was marked as off-topic.

@TomasHubelbauer
Copy link
Author

With build.module being able to return virtual modules, there is no need to map every module specifier to a path on the disk, especially if the module isn't backed by a physical path. The Node HTTP loader is a good example of this.

https://bun.sh/docs/runtime/plugins#virtual-modules

@guest271314

This comment was marked as off-topic.

@guest271314

This comment was marked as off-topic.

@TomasHubelbauer
Copy link
Author

Here's the simplest case I can think of to demonstrate build.module with a regex:

import { plugin } from "bun";

await plugin({
  name: "test",
  async setup(build) {
    console.log("Plugin loaded!", build);

    build.build(/^demo:/, (specifier) => {
      return {
        contents: `export default "Hi there, I am the '${specifier}' import!";`,
        loader: 'ts'
      };
    });
  },
});
console.log(await import('demo:alice')); // "Hi there, I am the 'alice' import!"
console.log(await import('demo:bob')); // "Hi there, I am the 'bob' import!"

@guest271314

This comment was marked as off-topic.

@guest271314

This comment was marked as off-topic.

@guest271314

This comment was marked as off-topic.

@guest271314

This comment was marked as off-topic.

@guest271314

This comment was marked as off-topic.

@guest271314

This comment was marked as off-topic.

@guest271314

This comment was marked as off-topic.

@guest271314

This comment was marked as off-topic.

@guest271314

This comment was marked as off-topic.

@guest271314

This comment was marked as off-topic.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
bug Something isn't working
Projects
None yet
Development

No branches or pull requests

2 participants