Jacob Deichert


A Super Hacky Alternative to import.meta.url

I've been diving deep into ES modules lately... mostly due to my frustrations with the state of existing bundlers and the complexity and configuration they require. Two weeks ago I released svelvet, a cli svelte compiler and file watcher that builds components and dependencies into individual esm files. Since then, I've been thinking a lot about ways to ship even less js to the client. My next project is a svelte static site prerenderer that eliminates static component trees and only hydrates the interactive parts of the DOM. And that's where import.meta.url comes in!

What is import.meta.url?

It's a proposal to add the ability for ES modules to figure out what their file name or full path is. This behaves similarly to __dirname in Node which prints out the file path to the current module. According to caniuse, most browsers already support it (including the latest Chromium Edge).

Here's a simple example. Create an esm file at dist/components/init.js with this:

export function main() {
    console.log(import.meta.url);
}

Next, create an html file at dist/index.html:

<html>
    <body>
        <script type="module">
            import { main } from '/components/init.js';
            main();
        </script>
    </body>
</html>

And finally, serve that dist directory. Check your console and you should see this printed:

http://localhost:8080/components/init.js

Why would I need an alternative if it's widely supported?

So, there's two parts to this...

#1 Svelte can't compile import.meta.url yet

I reported an issue to svelte two days ago showing that the compiler fails to parse import.meta.url. Due to it being a stage 3 proposal and not official yet, it's not being handled by the current js parser setup they're using it seems.

#2 import.meta.url can only be referenced from inside the file it's defined in

With the prerenderer project I'm currently working on, I wanted the api for marking interactive trees to be as simple as possible.

As seen in that svelte issue I logged, here's how marking a component could behave.

<script>
    import { hydrate } from '/HydratableTreeProofOfConcept';
</script>

<div use:hydrate={[import.meta.url, $$props]}>
    some component
</div>

This requires that the user always passes import.meta.url to use:hydrate. It's just boilerplate I'd rather not make people type all the time.

These two issues are what led me to look for an alternative...

The super hacky alternative... using an error's stack trace

When I got this to work I literally laughed out loud 😂. It might be the most hacky solution to a problem I've found yet... and so here's the disclaimer: DO NOT USE THIS IN PRODUCTION.

We've all seen error stack traces, right?

These things, but often much longer...

Uncaught Error
    at HTMLFormElement.handleSubmit (Form.js:704)
    at HTMLFormElement.<anonymous> (main.js:207)

You may also know that if you have an Error object, you can access the .stack property of it. This is super useful for error logging and monitoring.

// File: dist/components/index.js
export function main() {
    console.log(new Error('uh oh!').stack);
}

After refreshing the browser you'll see something like this in the console:

// Firefox
main@http://localhost:8080/components/index.js:2:15
@http://localhost:8080/components/index.js:4:1

// Chrome
Error: uh oh!
    at main (index.js:2)
    at index.js:4

At this point, it dawned on me that I could potentially use a stack trace to get the file's url!

Notice how Firefox shows the full path to index.js but Chrome doesn't? That threw me off for a little bit... until I realized that Chrome's console does some extra formatting on top of that stack trace. What we're seeing here isn't reality.

If we change the code a bit, we'll see the true stringified version in Chrome's console.

// File: dist/components/index.js
export function main() {
    const stackTraceFrames = String(new Error('uh oh!').stack)
        .replace(/^Error.*\n/, '') // Removes Chrome's first line
        .split('\n'); // Separate each stack frame
    console.log(stackTraceFrames);
}

// Chrome outputs this:
[
    "    at main (http://localhost:8080/components/index.js:2:34)",
    "    at http://localhost:8080/components/index.js:7:1"
];
// Firefox outputs this:
[
    "main@http://localhost:8080/components/index.js:2:34",
    "@http://localhost:8080/components/index.js:7:1"
];
// Safari outputs this:
[
    "main@http://localhost:8080/components/index.js:2:43",
    "global code@http://localhost:8080/components/index.js:7:18"
];

Now that we have all the browers outputting the full script paths, we can extract the same url that import.meta.url would give us.

If we make a helper function called getFileUrl, we can call this from our ES module to get its file path.

// File: dist/importMetaUrl.js
export function getFileUrl() {
    const stackTraceFrames = String(new Error().stack)
        .replace(/^Error.*\n/, '')
        .split('\n');
    // 0 = this getFileUrl frame (because the Error is created here)
    // 1 = the caller of getFileUrl (the file path we want to grab)
    const callerFrame = stackTraceFrames[1];
    // Extract the script's complete url
    const url = callerFrame.match(/http.*\.js/)[0];
    return url;
}

Then, update our esm script to use getFileUrl():

// File: dist/components/index.js
import { getFileUrl } from '/importMetaUrl.js';

export function main() {
    console.log(getFileUrl());
}

You'll see http://localhost:8080/components/index.js logged in the console, successfully behaving like import.meta.url does!

How does this solve my problem?

Well, now my prerenderer hydration marker api can look like this instead:

<script>
    import { hydrate } from '/HydratableTreeProofOfConcept';
</script>

<!-- User does not need to type import.meta.url anymore! -->
<!-- And this works around the svelte compiler error too -->
<div use:hydrate={$$props}>
    some component
</div>

The hydrate function internally calls getFileUrl() which allows me to pick out this component's stack frame and extract its url!

What could go wrong?

So many things. Chrome, Firefox or Safari could change their stack trace formatting at any point. Then there's dozens of other browsers I can't even test!

My use case is different though. I'll only be using this inside of headless chrome for a prerender phase to output my static html files. I'm NOT shipping this to production by any means 😅.

Hope you enjoyed my dumb hack!