Bundling Non-JavaScript Resources with Custom Imports and the new URL() Syntax
This article explains how to import and bundle non‑JavaScript assets such as images, CSS, fonts, WebAssembly modules and workers using custom URL schemes and the new URL(import.meta.url) pattern, discusses tool support, and outlines future import proposals.
When developing a web application you often need to handle resources beyond JavaScript modules, such as Web Workers, images, CSS, fonts, and WebAssembly modules.
Instead of referencing these assets directly in HTML, you can import them from JavaScript modules so that they are loaded dynamically together with the component that needs them.
Build systems cannot execute code to discover dynamic URLs, so custom import schemes are used to make static assets visible to the bundler.
Custom imports in bundlers
Bundlers can detect static import syntax and, with plugins, recognize custom URL schemes like asset-url: and js-url: to add the referenced resources to the build graph.
// regular JavaScript import
import { loadImg } from './utils.js';
// special "URL import" for static assets
import imageUrl from 'asset-url:./image.png';
import wasmUrl from 'asset-url:./module.wasm';
import workerUrl from 'js-url:./worker.js';
loadImg(imageUrl);
WebAssembly.instantiateStreaming(fetch(wasmUrl));
new Worker(workerUrl);The bundler copies the assets, optimizes them, and returns a final URL for runtime use. This approach reuses JavaScript import syntax and static relative paths, making dependency detection easy for the bundler.
However, browsers do not understand these custom schemes, so the code only works when processed by a bundler.
Universal import syntax for browsers and bundlers
Using new URL('./relative-path', import.meta.url) provides a static expression that works both in the browser and can be statically detected by bundlers.
new URL('./relative-path', import.meta.url)Rewriting the earlier example with this syntax:
// regular JavaScript import
import { loadImg } from './utils.js';
loadImg(new URL('./image.png', import.meta.url));
WebAssembly.instantiateStreaming(
fetch(new URL('./module.wasm', import.meta.url)),
{ /* … */ }
);
new Worker(new URL('./worker.js', import.meta.url));The new URL(...) constructor resolves the first argument relative to the module’s URL (import.meta.url), ensuring correct paths for both bundlers and browsers.
Ambiguous relative URLs
Dynamic requests like fetch('./module.wasm') are resolved relative to the document, not the JavaScript module, which can cause loading failures.
Wrapping the relative URL with new URL('./module.wasm', import.meta.url) guarantees the correct resolution before any loader processes it.
Toolchain support
Bundlers
The following bundlers support the new URL syntax:
Webpack v5
Rollup (via @web/rollup-plugin-import-meta-assets; workers via @surma/rollup-plugin-off-main-thread)
Parcel v2 (beta)
Vite
WebAssembly
When using WebAssembly, toolchains can emit JavaScript glue code that uses new URL(..., import.meta.url) to locate the .wasm file.
Emscripten (C/C++)
$ emcc input.cpp -o output.mjs
## If you don’t want the .mjs extension:
$ emcc input.cpp -o output.js -s EXPORT_ES6Adding -pthread also enables threading support.
$ emcc input.cpp -o output.mjs -pthread
## If you don’t want the .mjs extension:
$ emcc input.cpp -o output.js -s EXPORT_ES6 -pthreadwasm-pack / wasm-bindgen (Rust)
By default wasm-pack outputs a module that depends on the WebAssembly ESM integration proposal; for browser‑compatible output use:
$ wasm-pack build --target webThe generated code also uses new URL(..., import.meta.url) , allowing bundlers to discover the .wasm asset.
Future import mechanisms
import.meta.resolve
An experimental proposal import.meta.resolve(...) would let modules resolve relative URLs without an extra base argument.
// current syntax
new URL('...', import.meta.url);
// future syntax
await import.meta.resolve('...');It integrates with import maps and provides a static signal for bundlers.
Import assertions
Import assertions allow importing non‑JavaScript types (currently only JSON) with syntax like:
{ "answer": 42 } import json from './foo.json' assert { type: 'json' };
console.log(json.answer); // 42Future support may extend to CSS modules and other asset types.
Conclusion
Various methods exist to include non‑JavaScript resources in web projects, each with trade‑offs; the new URL(..., import.meta.url) pattern is currently the most widely supported solution across browsers, bundlers, and WebAssembly toolchains.
Until newer proposals become stable, using new URL(..., import.meta.url) remains the recommended approach.
ByteDance Web Infra
ByteDance Web Infra team, focused on delivering excellent technical solutions, building an open tech ecosystem, and advancing front-end technology within the company and the industry | The best way to predict the future is to create it
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.