cf.completefrontendCode editorOpen lab
THE JAVASCRIPT FIELD GUIDE

Module resolution & import maps

Learn how JavaScript module specifiers become URLs in the browser, how import maps give bare names addresses, how module scripts load, and why JSON modules need import attributes.

By the end, you can
  • 01
    Resolve specifiersClassify relative, absolute, and bare imports and predict their resolved URLs.
  • 02
    Read import mapsUse exact keys, prefix keys, and scopes without treating them like a package manager.
  • 03
    Run browser modules safelyObserve real module scripts and JSON modules inside sandboxed iframes.

From a string to a module file

Every import statement contains a small string called a module specifier. Before JavaScript can run your code, the host has to answer one practical question: “What file or URL does this string mean?” That answer is module resolution.

Browser modules are deliberately URL-shaped. If the specifier looks like directions from the current file, such as ./math.js, or a full address, such as https://cdn.example/lib.js, the browser can resolve it directly. If it is just a package-like name, such as react or greeting, the browser needs an import map or a build step to translate the name into a URL first.

The short definition

Module resolution is the host’s process for turning an import specifier into the exact URL or file that should be loaded.

Real-life analogyDirections vs an address book

If I say “go next door,” that instruction depends on where you are standing. If I say “go to the library,” you need an address book or local knowledge. Imports work the same way: some strings are directions, some are addresses, and some are names that need a map.

In real life: “Next door” or “up one floor”
In JavaScript: Relative specifiers like ./utils.js and ../data.js
In real life: “The library”
In JavaScript: Bare specifiers like greeting or react
In real life: A street address
In JavaScript: Absolute URL specifiers like https://cdn.example/app.js
In real life: An address book
In JavaScript: An import map that says which URL a name means

Where the analogy stops: Directions only make sense from where you are standing. Bare names only make sense after the host has an address book, a package resolver, or a bundler rewrite.

In this lesson you will resolve specifiers by hand, run real modules inside sandboxed iframes, compare classic scripts with module scripts, import JSON with an attribute, and practice deciding when a name needs the browser, an import map, Node, or a bundler.

Relative, absolute, and bare specifiers

INTERACTIVE

Browsers use three broad buckets. A relative specifier starts with ./, ../, or /. It is resolved with the same URL rules you practiced in the URL Objects lesson: new URL(specifier, referrer). An absolute URL already names the address. A bare specifier is everything else: a name with no built-in browser address.

Resolver lab: turn a specifier into a URL
Simplified resolver inputsPop out in the code editor (opens in a new tab)JavaScript
const referrer = "https://app.example/src/main.js";resolve("./utils/math.js", referrer);resolve("https://cdn.example/lib.js", referrer);resolve("greeting", referrer);resolve("lodash/fp/map.js", referrer);
Resolution resultbare
statusresolved
kindbare
urldata:text/javascript,export%20const%20message%20%3D%20%22Hello%20from%20a%20mapped%20module%22%3B
map importsgreeting, lodash/, app
scopehttps://app.example/admin/
Try it yourself

The import map's imports table maps the bare name to a real URL.

This is a tested, simplified browser resolver: URL-like specifiers resolve directly; bare specifiers look in the import map. Try ./local.js, /assets/app.js, lodash/fp/map.js, and react.

Try ./utils.js first. The result stays next to the importing module. Try /assets/app.js next: it jumps to the origin root. Then try greeting with the import map off. The failure is not about exporting; the browser cannot even find a URL to fetch.

Sort the specifier
  • ./utils/math.js
  • ../shared/log.js
  • /assets/app.js
  • https://cdn.example/preact.js
  • data:text/javascript,export const ok = true
  • greeting
  • lodash/fp/map.js
  • react
  • @scope/pkg/button
Try it yourself
0 of 9 correct

Place each import specifier where a browser or tool would handle it. Each answer explains why.

Choose a category for every card. You can change an answer at any time; Reset clears them all.

Import maps: the browser’s address book

STEP THROUGH

An import map is JSON inside <script type="importmap">. Its imports object maps bare names to real URLs. A key ending in / is a prefix key: lodash/ can match lodash/fp/map.js and keep the fp/map.js tail. A scopes object can override entries for modules imported from a particular URL neighborhood.

Real-life analogyYour contacts and the office directory

Your phone can use one contact list, while work uses its own directory. Import-map scopes work the same way: the importing module’s URL decides which directory to check first.

In real life: Your contacts list
In JavaScript: Top-level imports map greeting to a shared URL
In real life: The office directory
In JavaScript: A scopes entry maps greeting differently for /admin/ modules
In real life: Names beginning with the same word
In JavaScript: A prefix key maps every matching subpath

Where the analogy stops: Import maps do not install packages or scan folders. They only translate strings the browser sees.

<script type="importmap">{  "imports": {    "greeting": "./modules/greeting.js",    "lodash/": "https://cdn.example/lodash-es/"  },  "scopes": {    "/admin/": {      "greeting": "./admin/greeting.js"    }  }}</script>
Step through a tiny module graph
Step 0 of 4Ready
Your turn: follow the blue line

Toggle the import map and step through how the same source either links or fails.

Running in
  1. script
Next: line 1
Click the blue line to take the next stepPop out in the code editor (opens in a new tab)JavaScript
import { add } from "./math.js";import { greeting } from "greeting";console.log(greeting, add(2, 3));
CallStoreChangeResultRun = next line. Ran = already executed.
Recent returnsNothing yet. Start with the blue line.
Import map setting
A guided replay recorded from real JavaScript calls, not an engine debugger. Step follows executed statements; Back reviews a snapshot. Reset starts a fresh run.

The replay is a model of resolution, not an engine debugger. It uses the same pure resolver as the lab above, and the tests prove the displayed URLs match real URL behavior. The important order is real: if any static import in a module graph cannot resolve, the graph fails to link before the entry module runs.

Bare specifier lab: real browser module import
Frame HTML patternPop out in the code editor (opens in a new tab)HTML
<script type="importmap">{"imports":{"greeting":"data:..."}}</script><script type="module">import { text } from "greeting";parent.postMessage({ text }, "*");</script>
Sandboxed iframenot built
  • Choose a mode to build the sandboxed frame.
Try it yourself

The module code is real, but it runs only inside a sandboxed srcdoc iframe. The parent accepts messages only from that frame. Import maps must be parsed before the module script that needs them.

The mapped module is a predefined data: URL module. No learner code is evaluated, and the lesson page itself never calls dynamic import.
Import maps must come first

The iframe lab runs a real type="module" script. With no import map, import "greeting" fails with the browser’s “Failed to resolve module specifier” TypeError. With the map before the module, it succeeds. With the map after the module, it is too late for that import.

Module scripts in the browser

INTERACTIVE

Browser module scripts are not just classic scripts with imports. They are deferred by default, always strict, keep their top-level declarations out of the global object, fetch dependencies as a graph, and run each module once per URL. Cross-origin module scripts and imports use CORS; the server must allow the module response. The CORS lesson explains the network rule in detail.

Classic scripts vs module scripts
FeatureClassic scriptModule script
Default timingInline classic scripts run when the parser reaches them unless defer or async is added.Modules are deferred by default: the document is parsed before they execute.
Strict modeSloppy unless the file says "use strict".Always strict, even without a directive.
Top-level namesTop-level var can become a global property.Top-level declarations stay in the module's own scope.
FetchingCross-origin classic scripts have older, looser rules.Cross-origin module scripts use CORS, so the server must allow the fetch.
FallbackBrowsers that understand modules ignore nomodule scripts.Older browsers run nomodule fallbacks because they ignore type="module".
Module script lab: deferred, strict, scoped, cached
Module script behaviorHTML
<script>classic code runs while parsing</script><script type="module">import { count as first } from "data:...";import { count as second } from "data:...";undeclaredModuleValue = 1; // throws in strict modulesparent.postMessage({ first, second }, "*");</script><script nomodule>fallback for old browsers</script><p id="target">parsed after scripts</p>
Frame reportnormal
  1. Waiting for the frame to report.
Try it yourself

The classic inline script runs immediately and cannot see the later paragraph. The module is deferred, strict, has its own scope, and importing the same URL twice reuses one module instance. async lets a module run as soon as its graph is ready.

Cross-origin module scripts are fetched with CORS; this lab avoids the network by using data: URL modules. Modern browsers also skip the nomodule fallback.

The lab uses an iframe because it needs a real HTML parser, real module execution, and real nomodule behavior without touching this lesson page’s own module graph. The parent checks that messages come from the iframe it created, just like the sandboxed frame patterns in the Windows, iframes & postMessage lesson.

JSON modules and import attributes

INTERACTIVE

A JSON module imports a JSON resource and exposes the parsed value as its default export. Modern browsers require an import attribute: with { type: "json" }. The attribute is a label on the package saying what is inside, so the browser can reject a response that does not match the expected module type.

Real-life analogyA package label for the loader

A delivery label tells the receiver what handling rules to apply. Import attributes do the same for module loading: this import expects JSON, not ordinary JavaScript.

In real life: A box labeled “glass”
In JavaScript: with { type: "json" } says the import expects JSON
In real life: The receiver refuses a mislabeled package
In JavaScript: The browser refuses a JSON module imported without the right attribute
In real life: Opening the box reveals the item
In JavaScript: The default export is the parsed JSON value

Where the analogy stops: The label does not convert JavaScript into JSON. The fetched resource still has to be valid JSON, and support should be checked in the browsers you ship to.

JSON modules lab: import attributes
JSON module importPop out in the code editor (opens in a new tab)JavaScript
import data from "data:application/json,%7B%22course%22%3A%22JavaScript%22%2C%22lesson%22%3A%22Module%20resolution%22%7D" with { type: "json" };console.log(data.course);console.log(data.lesson);
Frame reportwith
  • Waiting for the JSON module.
Try it yourself

The with { type: "json" } attribute is a label: this import expects JSON. Modern Chrome refuses the same data: JSON module without that label. Check support in the browsers you target.

The JSON file is a predefined data: URL. It exports the parsed object as the default export when the attribute is present.

This connects to the earlier JSON lesson: JSON is data, not code. A JSON module gives you the parsed data value without calling JSON.parse yourself, but the module loader still controls fetching, CORS, and type checks.

Where you’ll use this

You will see module resolution in three daily places. In small demos, you can write browser-native modules directly and use an import map for a few friendly names. In production apps, a bundler often rewrites bare package specifiers into hashed URLs. In Node, bare package names are resolved through node_modules and package.json exports; the next lesson covers CommonJS and ES module interop there.

The same imports in three hostsPop out in the code editor (opens in a new tab)JavaScript
import React from "react";import button from "@company/design-system/button"; // Browser: needs import map entries or bundled URLs.// Node: searches node_modules and package.json "exports".// Bundler: rewrites these imports during the build.
  • Browser without tools: use relative URLs, absolute URLs, and explicit import map entries.
  • Bundled app: write package names like react; the bundler resolves and rewrites them before the browser sees the code.
  • Node scripts: Node applies its package-resolution rules. That is powerful, but it is not what browsers do natively.

Common misconceptions

“A slash anywhere means relative.”

No. lodash/fp has a slash but is still bare because it does not start with ./, ../, /, or a URL scheme.

“Import maps install packages.”

They only map strings to URLs. You still need files at those URLs, and the browser still fetches them with normal module rules.

“Scopes depend on the imported package name.”

Scopes depend on the importing module’s URL. The same bare name can resolve differently from /admin/ than from /public/.

“Module scripts run exactly like classic scripts.”

Modules are deferred, strict, scoped, fetched as a graph, and cached per URL. Classic-script habits can mislead you.

“JSON imports are just JSON.parse with nicer syntax.”

The result is parsed data, but the browser’s module loader also checks the import attribute and module type.

Practice: resolve before you run

5 EXERCISES
Exercise 1 · Warm-upResolve a relative import

Predict the full URL printed by this program.

Starter codePop out in the code editor (opens in a new tab)JavaScript
const referrer = "https://app.example/src/pages/home.js";
console.log(new URL("../shared/card.js", referrer).href);

Answer, then press Check. Spacing and letter case don’t matter.

    Exercise 2 · PracticeFix the bare-name iframe

    A page has <script type="module">import "greeting"</script> and then an import map below it. What must move?

    Answer, then press Check. Spacing and letter case don’t matter.

      Exercise 3 · PracticeRead a JSON module’s default value

      What does this JSON-module-shaped value print?

      Starter codePop out in the code editor (opens in a new tab)JavaScript
      const data = { course: "JavaScript", lesson: "Module resolution" };
      console.log(data.course + ": " + data.lesson);

      Answer, then press Check. Spacing and letter case don’t matter.

        Exercise 4 · PracticeFind the scoped mapping

        Which mapping would an /admin/ module use?

        Starter codePop out in the code editor (opens in a new tab)JavaScript
        const scope = "https://app.example/admin/";
        const referrer = "https://app.example/admin/panel.js";
        const chosen = referrer.startsWith(scope) ? "admin greeting" : "regular greeting";
        console.log(chosen);

        Answer, then press Check. Spacing and letter case don’t matter.

          Exercise 5 · ChallengeReview an import list

          In your editor, label each import in a file as “browser URL-like” or “needs a map/tool.” Explain one decision to a teammate.

            Quiz: check your understanding

            7 QUESTIONS

            These questions mix classification, resolution, import maps, module scripts, and JSON import attributes.

            Lesson quiz · 7 questionsScore: first tries count
            1. Question 1 of 7Which specifier can a browser resolve directly without an import map?

              Choose an answer to see the explanation.

            2. Question 2 of 7What URL does this relative module specifier produce?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const referrer = "https://app.example/src/main.js";
              console.log(new URL("../shared/log.js", referrer).href);

              Choose an answer to see the explanation.

            3. Question 3 of 7In an import map, what is special about a key ending in /?

              Choose an answer to see the explanation.

            4. Question 4 of 7What does the scoped import map entry depend on?

              Choose an answer to see the explanation.

            5. Question 5 of 7Which statement about browser module scripts is true?

              Choose an answer to see the explanation.

            6. Question 6 of 7What does this JSON-module-like object log?

              Read the code, then predictPop out in the code editor (opens in a new tab)JavaScript
              const data = { course: "JavaScript", lesson: "Module resolution" };
              console.log(data.course);

              Choose an answer to see the explanation.

            7. Question 7 of 7Why write with { type: "json" } on a JSON module import?

              Choose an answer to see the explanation.

            Key takeaways

            • Relative specifiers are directions from the importing module URL.
            • Absolute URL specifiers already name the module address.
            • Bare specifiers need an import map in browsers, or a bundler/Node-style resolver before the browser sees them.
            • Import-map keys ending in / match prefixes; scopes are chosen by the importing module’s URL.
            • Module scripts are deferred, strict, scoped, CORS-fetched across origins, and run once per URL.
            • JSON modules need with { type: "json" } and export parsed JSON as the default value.

            Remember the one-liner.
            Module resolution is how an import string becomes the exact module URL or file the host loads.

            Up next: CommonJS & ES module interop, where Node’s require, type: "module", and package boundaries enter the story.

            CompleteFrontend Clear concepts. Working examples.