The "i-prefer-import-meta-url-over-using-esm-import-to-get-a-relative-file-url" plugin ;-)
In my components library, I have SVG images.
I used them with <img>
tags in my components and to get the proper relative URL, I use new URL('../assets/image.svg', import.meta.url').href
.
To make this work in a rollup build, you need to:
- Preserve relative file tree structure of your JavaScript files
- Copy files with
rollup-plugin-copy
and keep same relative tree structure
I had to rework my tree structure to have all components in src/foo/component-foo.js
and all assets in src/assets/image.svg
.
I also had to make sure my components end up in src/dist/component-foo.js
and copied assets in dist/assets/image.svg
.
It works "OK" in my situation but it has a few limitations:
- The rules on the tree structure are too strict
- When I did a test to emit just one bundle instead of preserving modules, I had to emit it in a subdir so the
../assets
still work. - If a relative path does not exist, the build does not fail.
- You need the copy plugin.
"Everyone is doing it", especially with Webpack loaders but it's not standard. It requires specific bundler config to work before serving the source files to a browser.
With import.meta.url
it works as is in the browser before the bundling.
This plugin idea is "just a way" to help rollup understand this pattern better and rewrite stuffs if the relative tree structure is changed.
A tranform plugin (with transform()
hook) that:
- detects
new URL('../path/to.svg', import.meta.url)
- emits file with
this.emitFile({type:'asset', /* .... */})
- replaces the
new URL('../path/to.svg', import.meta.url)
withimport.meta.ROLLUP_FILE_URL_referenceId
so rollup can replace it with the correct relative path (and maybe the hash).
Here's the untested/dirty/hacky proof of concept:
// rollup.config.js
export default {
// ...
plugins: [
// ...
{
transform(code, id) {
const rgx = /new URL\('(.*)', import\.meta\.url\)\.href/g;
const matches = Array.from(code.matchAll(rgx));
const { dir } = path.parse(id);
let newCode = code;
for (const [all, m] of matches) {
const fileName = path.relative(process.cwd(), path.resolve(dir, m))
.replace('src/', '');
const ref = this.emitFile({ type: 'asset', fileName, source: `contents of ${m}` });
newCode = newCode.replace(all, 'import.meta.ROLLUP_FILE_URL_' + ref);
}
return {
code: newCode,
map: { mappings: '' },
};
}
}
]
}
What does it do?
Well if you have this:
src
├── assets
│ ├── eye-closed.svg
│ └── eye-open.svg
├── atoms
│ └── cc-input-text.js
With a cc-input-text.js
component using this to get the relative URLs:
const eyeClosedSvg = new URL('../assets/eye-closed.svg', import.meta.url).href;
const eyeOpenSvg = new URL('../assets/eye-open.svg', import.meta.url).href;
After a build, it would end up like this:
dist
├── my-bundle.js
├── eye-closed.svg
└── eye-open.svg
With my-bundle.js
containing the code of cc-input-text.js
and rewritten relative URLs like this:
const eyeClosedSvg = new URL('./eye-closed.svg', import.meta.url).href;
const eyeOpenSvg = new URL('./eye-open.svg', import.meta.url).href;
- It should be more agnostic to where your files are
- It should be more agnostic to where your files go
- It should allow you to hash your asset names and get your source code modified to reflect that
- It will fail if a relative import is not found
- No need for copy plugin anymore
- Read the file instead of this fake content
- Use Rollup's
this.parse()
instead of a regex - Rewrite the
fileName
so it's generic - Implement sourcemap
- Investigate how it works if we hash JS files
- Investigate how it works if we hash assets files
- Add the classic include/exclude options
- Proper filtering of other files in the transform hook (non js, non source...)
And of course:
- a name
- tests
- docs
- What do we detect?
.href
or not at the end? - If I want to optimize my SVG, should it be a transform option of this plugin or can this be done properly outside of this?
- Could rollup-plugin-visualizer be able to also display the size of my assets?
- Should this plugin focus only on this, or could it also rewrite the
import.meta.url
expression to something else?
About (4):
- I think rewriting
import.meta.url
into something else that work in situations that don't understand this is a different problem. - I'm not a great fan of data URI inlining etc but maybe it could be an option...
Right now, my use case is to run this on my component sources to prepare the npm package but when I will use my components in my other projects, I would need this plugin to run over my dependencies. I'm not using rollup on those projects so I'll need a similar trick for webpack 😄
I'll defer that to an improvement but yes it should be addressed.
I'm pretty sure we won't be able to ask Lukas to provide a config to remove the href so yes, maybe we need to compose it like your example. It's ugly but it works ™️
Good point, I'm using it in my webpack build to make my assets work.
I have a transform callback in my POC and I wired svgo. Works great.