Modules

Obfuscation and import maps

An import map is a block of JSON in your HTML telling the browser where bare module specifiers resolve. It is markup, not code, so no protection step touches it and it stays exactly as readable as you wrote it. That produces two separate questions which usually arrive fused together, and they have different answers.

Two questions that get mixed together

The first is a compatibility question: if my modules are protected, do the specifiers still match what the map declares? The answer is yes, reliably, and for a structural reason worth understanding rather than taking on faith.

The second is a disclosure question: if the map is sitting in my HTML in plain text, what does it tell someone about an application whose code I just spent effort protecting? The answer there is more interesting, and it is the half most teams have not thought about.

Why specifiers survive, structurally

When the parser meets an import or export statement it does not decompose it into the kind of expression tree that renaming and string transforms operate on. It reads the statement through as a raw module statement, reassembling it token by token and carrying the text forward. The specifier is never converted into something a transform could rewrite, so it comes out the other side exactly as written.

That is the right design rather than a convenient accident, because the language leaves no alternative. An import declaration's specifier has to be a static string literal that a browser can read before any of your code runs, which is precisely what makes the module graph statically analysable. If a transform replaced it with a call into a string table assembled at run time, the module would no longer parse, let alone resolve. Handling the statement as raw text makes that class of mistake structurally unavailable rather than a rule someone has to remember to apply.

The same path carries export statements, which is why exported names survive too. That is the constraint the ES modules article works through in detail, and it is the same fact seen from the other end: what crosses a module boundary is a contract, and contracts are not renamed.

Dynamic import() is the exception worth naming. It takes an expression rather than a literal, so a specifier there is an ordinary string and is treated like any other. The value is preserved so resolution still works, but the path is no longer sitting in the file as readable text, which is occasionally useful and occasionally surprising when you go looking for it.

The map is a public index of your module graph

Nothing in a protection pipeline reads your HTML, and even if it did, mangling an import map would break resolution rather than protect anything. So the map ships as written and anyone can read it with view-source. What it contains is worth looking at with fresh eyes.

It lists the bare names your code imports and the URLs they resolve to. From that a reader gets your third-party dependency list, frequently with versions in the filenames. If you map internal prefixes, they get the shape of your internal module layout and your naming conventions along with it. And because the map is a declaration rather than a record of what was used, it can name modules that no path through your entry point ever reaches, which is a hint about what exists rather than about what runs.

This is the same class of exposure as a published source map or a bundle that names your unreleased features: the metadata is more legible than the code, and it is legible without any analysis at all. The roadmap article makes the general version of this argument, and an import map is a particularly clean example because it is literally a labelled index.

Keep the exposure in proportion. A dependency list is not a secret in most applications, and someone determined to enumerate your dependencies could do it from the bundle anyway. The point is that the map turns a small analysis job into a glance, and that it silently escapes any protection decision you made about the code.

What to actually do about it

Do not attempt to obfuscate the map. It is an input the browser parses before your code runs, it has to be readable for resolution to work, and there is nowhere to hide it. Any scheme that appears to protect it either breaks module loading or relies on rewriting the map at run time, which requires the code doing the rewriting to be readable first.

Keep the map minimal. Map only the specifiers that genuinely need a bare name. Every extra entry is a line in a public index, and a map that mirrors your whole internal tree because it was generated from a directory listing is disclosing structure for no benefit.

Treat version strings in the target URLs as the disclosure decision they are. Naming exact versions of third-party modules tells a reader which known issues to check for without touching your code at all, and that is the same decision you already make when you put versions in filenames. If you would not publish a dependency manifest, do not publish one here by accident.

If the module graph itself is the sensitive part, bundling is the control, not protection. A bundler resolves specifiers at build time and emits a single artefact with no map, so the graph stops being a published document and becomes an internal detail of a file. That is a build-architecture decision with real trade-offs against caching and code splitting, which the code-splitting article covers, but it is the honest answer to the question people are actually asking.

Where you keep unbundled modules, pair the map with integrity checking on the files themselves rather than trying to protect the map. Subresource integrity and obfuscation covers the ordering constraint that makes this work, which is that the digest has to be computed after protection rather than before.

The short version

Import maps and protected modules coexist without special handling, because import and export statements are carried through as raw text and their specifiers are never candidates for renaming or string packing. Dynamic import is the one place a specifier becomes an ordinary string, and its value is preserved.

The question worth your attention is the other one. The map is JSON in your HTML, no build step touches it, and it is a labelled index of your dependencies, their versions and often your internal module layout. Keep it minimal, treat its contents as published, and if the graph is what you were protecting, bundle it away rather than looking for a way to obscure the map.

Frequently asked questions

Does obfuscation break import maps?

No. Import and export statements are handled as raw module statements rather than being decomposed into expressions, so the specifier text is carried through unchanged and still matches what your map declares. This is structural rather than a special case, because the language requires an import specifier to be a static string literal that the browser can read before your code runs. A transform that replaced it with anything computed would produce a module that no longer parses, so that path is not available in the first place.

Can a protection step rename or encrypt my module specifiers?

Not for static import and export declarations, for the reason above. Dynamic import is different: it accepts an expression rather than a literal, so a specifier passed to it is an ordinary string and is handled like any other string in your code. The value is preserved so resolution still works at run time, but the path will no longer appear in the file as plain readable text. That distinction occasionally matters when you are searching a built file for a path and cannot find it.

Is my import map protected along with my JavaScript?

No, and it cannot be. The map is JSON inside a script tag in your HTML, and the browser must parse it before any of your code executes in order to resolve anything. Protection tools operate on JavaScript rather than on your markup, so the map ships exactly as written. Anyone can read it with view-source, which is worth remembering when deciding what to put in it.

What does an import map disclose about my application?

More than most teams expect. It gives a reader the list of bare names your code imports and the URLs they resolve to, which usually means your third-party dependency list and frequently the exact versions if those appear in filenames. If you map internal prefixes it also exposes your internal module layout and naming conventions. Because a map declares what is available rather than recording what is used, it can also name modules that nothing in your entry point actually reaches.

How do I stop my module graph being visible?

Bundle it. A bundler resolves specifiers at build time and emits an artefact with no map attached, so the graph becomes an internal detail of a file rather than a published document. There is no way to achieve the same thing while keeping the map, because the map exists to be read by the browser before your code runs. This is a build-architecture decision with real trade-offs against long-lived caching and code splitting, so weigh it rather than treating it as a free win.

Should I add integrity hashes to modules loaded through an import map?

Where your tooling and target browsers support it, integrity checking on the module files is the more useful control, because it addresses substitution of the files themselves rather than the readability of the map. The ordering constraint is the thing to get right: the digest has to be computed from the final protected file, so integrity generation belongs after protection in your pipeline. Computing it earlier produces hashes that will not match what you ship and the modules will be blocked.

Related reading