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...