I am working on a project where I’d like to run the Typst compiler in the browser as a wasm module and I used a package called typst.ts.
The project I was testing it out on was using Bun with React and when I tried to import the React component for the package, I got an error saying Could not resolve: "./typst.css?inline". Understanding the solution was pretty interesting so here’s everything I found out and how to fix it.
Understanding the issue.
What the component is trying to do here, is get the css as a string value using the ?inline syntax. That syntax is specific to Vite and bun doesn’t support it.
The fix
Bun allows you to hook into different parts of the bundling process and run code.
create a plugins/inline-css.ts file
first, we register the bun plugin
import { BunPlugin } from 'bun'
const inlineCssPlugin: BunPlugin = {
name: 'inline-css'
}
The next step is to hook inside the build process
import { BunPlugin } from 'bun'
const inlineCssPlugin: BunPlugin = {
name: 'inline-css',
setup(build) {
...
}
}
The first hook we need to regiser is on onResolve
import { BunPlugin } from 'bun'
const inlineCssPlugin: BunPlugin = {
name: 'inline-css',
setup(build) {
build.onResolve({ filter: /\.css\?inline$/ }, (args) => {
const path = args.path.replace(/\?inline$/, "");
const resolved = Bun.resolveSync(path, args.importer);
return { path: resolved, namespace: "css-inline" };
});
}
}
args.path contains the raw import string
args.importer is the file that you try to import it from
- We tell bun to run this hook on any line that end with
.css/?inline - We get the import statement and remove the inline part to get only the path
- We change the relative path to an absolute path on disk
- We return the absolute path with a special tag for the next hook.
Speaking of the next hook, here is it.
import { BunPlugin } from 'bun'
const inlineCssPlugin: BunPlugin = {
name: 'inline-css',
setup(build) {
//...
build.onLoad({ filter: /.*/, namespace: "css-inline" }, async (args) => {
const contents = await Bun.file(args.path).text();
return {
contents: `export default ${JSON.stringify(contents)};`,
loader: "js",
};
});
}
}
We now tell bun to run this hook on any import load that matches the criteria. In this case, we run it for any file in the css-inline namespace.
We get the content of the file using args.path which is not the absolute path from the previous hook.
We return it as a JSON object and we need to specificy the loader : "js" otherwise Bun uses the file extension as parser.
Bun then ends up returning a fake js module that might look something like this
export default "body { color: red; }";