Q1What happens if you load the default export Cart with import { Cart } from "./cart.js"?
Modules — import and export
Covers export, which makes values in a file public, and import, which loads them. Learn named vs. default exports, type="module" in the browser, and ESM vs. CommonJS in Node.js.
If you keep adding the code for an order screen to a single file, tax calculations, the cart, and checkout logic end up mixed together over hundreds of lines, and finding a function takes a while. Even if you split the code into files and list several <script> tags (the tag that loads a JavaScript file) in the HTML, the code that uses a function can't tell which file it lives in.
This article covers export and import, which pass values between files.
You Can't Use import in the Console
The exercise console runs your code by placing it inside a single function. import and export can only be written at the top level of a file, so they cause a SyntaxError there. The code in this article is meant to run in Node.js or the browser as separate files, and the results are shown in comments at the end of each line.
Sharing Functions Between Files — Named Exports
Suppose both cart-page.js (the cart screen) and checkout.js (the checkout screen) need the same tax calculation. If you copy the function into both files, a tax rate change means fixing two places, and if you miss one, the screens show different amounts.
Variables and functions declared in a module (a JavaScript file that passes values with import and export; how to load one is covered in a later section) aren't visible to other files. Putting export in front of a declaration makes it public; this is called a named export. The file that uses it writes the same name inside braces: import { withTax } from "./price.js";.
// ---- price.js ----
const TAX_RATE = 0.1; // No export: used only inside price.js
export function withTax(price) { // With export: other files can import it
return Math.floor(price * (1 + TAX_RATE));
}
export function formatYen(price) {
return `${price} yen`;
}
// ---- cart-page.js ----
import { withTax, formatYen } from "./price.js"; // List the names you'll use inside the braces
console.log(formatYen(withTax(4800))); // 5280 yen
// If you write a name that isn't exported in the import
// import { TAX_RATE } from "./price.js";
// SyntaxError: The requested module './price.js' does not provide an export named 'TAX_RATE'
TAX_RATE is read by withTax inside price.js, so cart-page.js gets the price with tax without knowing the tax rate. If the rate changes, you only fix the one TAX_RATE line in price.js, and every file that imports it picks up the change.
Making a Single Class Public — Default Exports
If cart.js holds the cart class, the main thing it exports is a single Cart. You'll often see import Cart from "./cart.js"; in other people's code, but adding braces as in the previous section gives a SyntaxError.
A default export (at most one per file; the importing file doesn't need to match its name) is written as export default class Cart { ... }. The importing file writes it without braces and can use any name it likes in place of Cart.
// ---- cart.js ----
import { withTax } from "./price.js";
export const MAX_ITEMS = 20; // Named exports can live in the same file
export default class Cart { // One default export per file
#prices = [];
add(price) { this.#prices.push(price); }
get total() { return withTax(this.#prices.reduce((sum, price) => sum + price, 0)); }
}
// ---- checkout.js ----
import Cart, { MAX_ITEMS } from "./cart.js"; // Default outside the braces, named inside
import ShoppingCart from "./cart.js"; // A default can be imported under another name
const cart = new ShoppingCart();
cart.add(1200); cart.add(3600);
console.log(cart.total, MAX_ITEMS); // 5280 20
console.log(Cart === ShoppingCart); // true (both are the same class)
// With braces, it looks for a named export called Cart
// import { Cart } from "./cart.js";
// SyntaxError: The requested module './cart.js' does not provide an export named 'Cart'
The 'Cart' at the end of the error message is the name of the named export it looked for and couldn't find. The message doesn't mention that a default Cart exists, so check whether you used braces. The table below sums up the import forms used in this article.
| import form | What you get | Name on the importing side |
|---|---|---|
| import { withTax } from … | Named export | Same name as the export |
| import Cart from … | Default export | Any name you like |
| import Cart, { MAX_ITEMS } from … | Default and named | Cart and MAX_ITEMS (default first) |
Letting the Browser Follow Imports — type="module"
Next, the checkout page's HTML needs to load checkout.js. With a plain <script src="./checkout.js"></script>, Chrome reports SyntaxError: Cannot use import statement outside a module at the import on line 1, and none of the file runs.
With type="module" (a <script> setting that runs the loaded file as a module), the browser follows the imports in the entry file and fetches the other files too. These import connections are called the dependency graph (which files import which).
// ---- index.html: list only the entry file, checkout.js ----
// <script type="module" src="./checkout.js"></script>
// ---- price.js ----
console.log("Running price.js"); // Printed once, even though 2 files import it
const TAX_RATE = 0.1;
export function withTax(price) { return Math.floor(price * (1 + TAX_RATE)); }
// ---- cart.js ----
import { withTax } from "./price.js";
console.log("Running cart.js");
export const cartTotal = withTax(4800);
// ---- checkout.js ----
import { cartTotal } from "./cart.js";
import { withTax } from "./price.js"; // Loads the same price.js as cart.js
console.log(`Total due: ${cartTotal + withTax(500)} yen`);
// Console output order: Running price.js → Running cart.js → Total due: 5830 yen
A module runs the files it imports before it runs itself. The entry file, checkout.js, waits until the cart.js and price.js it imports have finished, so it runs last. Since the order is decided by the imports, you don't need to think about the order of files in the HTML.
It Doesn't Work in HTML Opened via file://
If you double-click an HTML file and open it as file://, Chrome treats even files in the same folder as coming from a different origin (the source a file is fetched from), and CORS (the mechanism that restricts loading from other origins) blocks the type="module" load. Start a web server, for example with the Live Server extension for VS Code, and open the page from there.
Telling the Formats Apart in Node.js — ESM and CommonJS
In code written for Node.js, you'll come across files that use const { reserve } = require("./stock"); instead of import. If you copy a line like that into a file that uses import, it throws a ReferenceError at runtime.
Besides ESM (short for ES Modules, the standard format that uses import and export), there's CommonJS (a Node.js-specific format that loads with require and makes values public with module.exports). Node.js decides which one a file uses based on its extension and the "type" field in package.json (the project's configuration file).
// ---- shop/package.json (the file with a Node.js project's settings) ----
// { "type": "module" }
// ---- shop/stock.cjs: its extension is .cjs, so it's loaded as CommonJS ----
function reserve(count) { return `Reserved: ${count}`; }
module.exports = { reserve: reserve, LIMIT: 3 }; // Put all the public values in one object
// ---- shop/report.cjs: a CommonJS file loads with require ----
const { reserve, LIMIT } = require("./stock.cjs");
console.log(reserve(2), LIMIT); // Reserved: 2 3
// ---- shop/checkout.js: "type": "module", so it's loaded as ESM ----
import stock from "./stock.cjs"; // No braces: gets the value of module.exports
console.log(stock.reserve(1)); // Reserved: 1
const { LIMIT } = require("./stock.cjs");
// ReferenceError: require is not defined in ES module scope, you can use import instead
- Files ending in
.jsare loaded as ESM
- Can load it with
import stock from "./stock.cjs" - Writing
requirethrows aReferenceError
stock.cjs— makes values public withmodule.exportsreport.cjs— can load withrequire
import stock has no braces, so it works the same way as importing a default export: it gets the module.exports object. Don't mix import and require in one file; stick to one or the other. The table below shows how to write each format and the rules that decide a file's format.
| Item | ESM | CommonJS |
|---|---|---|
| Loading | import { reserve } from "./stock.js" | const { reserve } = require("./stock.js") |
| Making public | export function reserve() { ... } | module.exports = { reserve: reserve } |
| Extension | Can't be omitted in Node.js or browsers ("./stock" isn't found) | Can be omitted ("./stock" finds stock.js) |
| Files loaded in this format | .mjs, and .js under "type": "module" | .cjs, and .js under "commonjs" or no "type" (with no "type", a .js that uses import is re-read as ESM) |
| Browser | Loads with type="module" | Not supported directly |
Knowledge Check
Answer each question one by one.
Q2price.js is imported from two places. How many times does the console.log at its top print?
Q3What happens if you call require in checkout.js under "type": "module"?