I’m linking to issues in standards repos in the interest of transparency. Please don’t spam them, even with supportive comments (that’s what reactions are for). Also keep in mind that the vast majority of import map improvements are meant for tooling authors and infrastructure providers — nobody expects regular web developers to author import maps by hand.
Reading List
The most recent articles from a list of feeds I subscribe to.
Examples for the tcpdump and dig man pages
Hello! My big takeaway from last month’s musings about man pages was that examples in man pages are really great, so I worked on adding (or improving) examples to two of my favourite tools’ man pages.
Here they are:
- the dig man page (now with examples)
- the tcpdump man page examples (this one is an update to the previous examples)
the goal: include the most basic examples
The goal here was really just to give the absolute most basic examples of how to use the tool, for people who use tcpdump or dig infrequently (or have never used it before!) and don’t remember how it works.
So far saying “hey, I want to write an examples section for beginners and infrequent users of this tools” has been working really well. It’s easy to explain, I think it makes sense from everything I’ve heard from users about what they want from a man page, and maintainers seem to find it compelling.
Thanks to Denis Ovsienko, Guy Harris, Ondřej Surý, and everyone else who reviewed the docs changes, it was a good experience and left me motivated to do a little more work on man pages.
why improve the man pages?
I’m interested in working on tools’ official documentation right now because:
- Man pages can actually have close to 100% accurate information! Going through a review process to make sure that the information is actually true has a lot of value.
- Even with basic questions “what are the most commonly used tcpdump flags”,
often maintainers are aware of useful features that I’m not! For
example I learned by working on these tcpdump examples that if you’re saving
packets to a file with
tcpdump -w out.pcap, it’s useful to pass-vto print a live summary of how many packets have been captured so far. That’s really useful, I didn’t know it, and I don’t think I ever would have noticed it on my own.
It’s kind of a weird place for me to be because honestly I always kind of assume documentation is going to be hard to read, and I usually just skip it and read a blog post or Stack Overflow comment or ask a friend instead. But right now I’m feeling optimistic, like maybe the documentation doesn’t have to be bad? Maybe it could be just as good as reading a really great blog post, but with the benefit of also being actually correct? I’ve been using the Django documentation recently, and it’s really good! We’ll see.
on avoiding writing the man page language
The tcpdump project tool’s man page is
written in the roff language,
which is kind of hard to use and that I really did not feel like learning it.
I handled this by writing a very basic markdown-to-roff script to convert Markdown to roff, using similar conventions to what the man page was already using. I could maybe have just used pandoc, but the output pandoc produced seemed pretty different, so I thought it might be better to write my own script instead. Who knows.
I did think it was cool to be able to just use an existing Markdown library’s ability to parse the Markdown AST and then implement my own code-emitting methods to format things in a way that seemed to make sense in this context.
man pages are complicated
I went on a whole rabbit hole learning about the history of roff, how it’s
evolved since the 70s, and who’s working on it today, inspired by learning about
the mandoc project that BSD systems (and some Linux
systems, and I think Mac OS) use for formatting man pages. I won’t say more
about that today though, maybe another time.
In general it seems like there’s a technical and cultural divide in how documentation works on BSD and on Linux that I still haven’t really understood, but I have been feeling curious about what’s going on in the BSD world.
Propellant.
We cannot separate the everyday use of “AI” platforms from their use in death and war.
LinkedIn should punish the “comment X to get access” bait spam
External import maps, today!
A few weeks ago, I posted Web dependencies are broken. Can we fix them?. Today’s post is a little less gloomy: Turns out that the major limitation that would allow centralized set-it-and-forget-it import map management can be lifted today, with excellent browser support!
The core idea is that you can use DOM methods to inject an import map dynamically, by literally creating an <script type="importmap"> element in a classic (blocking) script and appending it after the injector script. 💡
This is a gamechanger. It makes external import maps nice-to-have sugar instead of the only way to have centralized import map management decoupled from HTML generation.
All we need to do is build a little injector script, no need for tightly coupled workflows that take over everything.
Once you have that, it takes a single line of HTML to include it anywhere.
If you’re already using a templating system, great! You could add <script src="importmap.js"></script> to your <head> template for every page.
But you don’t need a templating system: even if you’re rawdogging HTML (e.g. for a simple SPA), it’s no big deal to just include a <script src="importmap.js"></script> in there manually.
This is not even new: when the injector is a classic (non-module) script placed before any modules are fetched, it works in every import map implementation, all the way back to Chrome 89, Safari 16.4+, and Firefox 108+!
Turns out, JSPM made the same discovery: JSPM v4 uses the same technique. It is unclear why it took all of us so long to discover it but I’m glad we got there.
How does it work?
Basic import map injector script
First, while there is some progress around making import maps more resilient, your best bet for maximum compatibility is for the injector script to be a good ol’ blocking <script> that comes before everything else.
This means no type="module", no async, no defer — you want to get it in before any modules start loading or many browsers will ignore it.
Then, you literally use DOM methods to create a <script type="importmap"> and append it after the script that is injecting the import map (which you can get via document.currentScript).
This is a minimal example:
(()=>{
const map = {
/* elided */
};
const script = Object.assign(document.createElement("script"), {
type: "importmap",
textContent: JSON.stringify(map)
});
document.currentScript.after(script);
})();
Fixing relative URLs
Remember, this literally injects inline import maps in your page. This means that any relative URLs will be interpreted relative to the current page!
If you’re building an SPA or your URLs are all absolute or root-relative, that’s no biggie. But if these are relative URLs, they will not work as expected across pages. You need to compute the absolute URL for each mapped URL and use that instead. This sounds complicated, but it only adds about 5 more lines of code:
(()=>{
const map = {
/* elided */
};
const mapUrl = document.currentScript.src;
const rebase = m => { for (let k in m) m[k] = new URL(m[k], mapUrl).href; return m; };
rebase(map.imports);
for (let scope in (map.scopes ?? {})) {
rebase(map.scopes[scope]);
}
const script = Object.assign(document.createElement("script"), {
type: "importmap",
textContent: JSON.stringify(map)
});
document.currentScript?.after(script);
})();
Error handling
Note that document.currentScript is null in module scripts, since the same module can be loaded from different places and different scripts.
Once it becomes possible to inject import maps from a module script, you could use import.meta.url to get the URL of the current module.
Until then, you can use a bit of error handling to catch mistakes:
(()=>{
const map = {
/* elided */
};
const mapUrl = document.currentScript?.src;
if (!mapUrl) {
throw new Error("Import map injector script must be a classic (non-module) script");
}
const rebase = m => { for (let k in m) m[k] = new URL(m[k], mapUrl).href; return m; };
rebase(map.imports);
for (let scope in (map.scopes ?? {})) {
rebase(map.scopes[scope]);
}
const script = Object.assign(document.createElement("script"), {
type: "importmap",
textContent: JSON.stringify(map)
});
document.currentScript?.after(script);
})();
This is the minimum, since the script literally breaks if document.currentScript is null.
You could get more elaborate and warn about async/defer attributes, or if type="module" scripts are present before the current script.
These are left as an exercise for the reader.
Do we still need external import maps?
While this alleviates the immediate need for external import maps, the DX and footguns make it a bit gnarly, so having first-class external import map support would still be a welcome improvement.
But even if we could do <script type="importmap" src="..."> today, the unfortunate coupling with HTML is still at the receiving end of all this, and creates certain limitations,
such as specifiers not working in worker scripts.
My position remains that HTML being the only way to include import maps is a hack. I’m not saying this pejoratively. Hacks are often okay — even necessary! — in the short term. This particular hack allowed us to get import maps out the door and shipped quickly, without getting bogged down into architecture astronaut style discussions that can be non-terminating.
But it also created architectural debt.
These types of issues can always be patched ad hoc, but that increases complexity, both for implementers and web developers.
Ultimately, we need deeper integration of specifiers and import maps across the platform.
<script type="importmap"> (with or without an src attribute) should become a shortcut, not the only way mappings can be specified.
In my earlier post, I outlined a few ideas that could help get us closer to that goal and make import maps ubiquitous and mindless. Since they were well received, I opened issues for them:
- Linking to import maps via an HTTP header (
Link?) specifier:URLs to bridge the gap between specifiers and URLs- Since import maps as an import attribute proved out to be tricky, I also filed another proposal for a synchronous import map API.
The hope is also that better platform-wide integration can pave the way for satisfying the (many!) requests to expand specifiers beyond JS imports. Currently, the platform has no good story for importing non-JS resources from a package, such as styles, images, icons, etc.
But even without any further improvement, simply the fact that injector scripts are possible opens up so many possibilities! The moment I found out about this I started working on making the tool I wished had existed to facilitate end-to-end dependency management without a build process (piggybacking on the excellent JSPM Generator for the heavy lifting), which I will announce in a separate post very soon [1]. Stay tuned!
But if you’re particularly curious and driven, you can find it even before then, both the repo and npm package are already public 😉🤫 ↩︎