ES6 Modules, npm, and Webpack

Modern JavaScript

ES6 Modules, npm & Webpack

CSCI 4513

Week 4, Lecture 7

Today's Learning Objectives

Norse Mythology Connection

The Dwarven Forges

Specialized workshops collaborated to create great artifacts, just as ES6 modules combine specialized code into powerful applications.

The Tale:

Beneath Svartalfheim's mountains, dwarves maintained countless specialized forgesβ€”some for metalwork, others for enchantments, gemcraft, or mechanics. Great artifacts flowed through multiple workshops, each contributing expertise while maintaining independence.

The greatest works like Mjolnir weren't created in a single forge. Ore refiners purified materials, master smiths shaped forms, enchanters wove magic, and assemblers combined everything. Each workshop kept its techniques secret, passing only finished components to the next stage.

Specialized Workshops

// metalwork.js - specialized module
export function forgeBase(material, shape) {
    return { material, shape, durability: 100 };
}

// enchantment.js - specialized module
export function addMagic(item, type) {
    return { ...item, magic: type, power: 50 };
}

// assembly.js - combines modules
import { forgeBase } from './metalwork.js';
import { addMagic } from './enchantment.js';

export function createArtifact(specs) {
    let artifact = forgeBase(specs.material, specs.shape);
    artifact = addMagic(artifact, specs.enchantment);
    return artifact;
}

export - Makes a function available for other files to import

...item - Spread operator copies all existing properties into the new object

import { fn } from - Pull specific named exports from another module

Pattern - Each file is a specialized module. assembly.js coordinates by importing from both.

Each module does one thing well β€” single responsibility principle!

Reflection Questions

Before ES6: The Global Scope Problem

<script src="one.js" defer></script>
<script src="two.js" defer></script>
// one.js
const greeting = "Hello!";
// two.js
console.log(greeting);

defer - Loads scripts after HTML is parsed, but they still share global scope

The problem - greeting defined in one.js is visible in two.js because both scripts share the window's global scope.

⚠️ Danger - Any script can overwrite another script's variables! No isolation, no protection.

Imagine 10+ scripts all sharing one scope β€” name collisions are inevitable!

Old Solution: IIFE (Module Pattern)

// one.js - wrap in IIFE
const greeting = (() => {
  const greetingString = "Hello, Odinite!";
  const farewellString = "Bye bye, Odinite!";
  return greetingString;  // Expose only this
})();

// two.js
console.log(greeting);       // Works!
console.log(farewellString); // Error - not exposed

(() => { ... })() - IIFE: defines and immediately calls a function, creating a private scope

return - Only what's returned is exposed to the outside

Private by default - farewellString stays hidden inside the IIFE. Only greetingString is returned.

This was the pre-ES6 workaround β€” ES6 modules make this pattern built-in!

ES6 Modules (ESM)

Real Modules, Finally!

πŸ’‘ Key Point: Module scope is NOT the global scope!

Named Exports

// one.js - Method 1: Inline export
export const greeting = "Hello, Odinite!";
export const farewell = "Bye bye, Odinite!";

// one.js - Method 2: Separate export
const greeting = "Hello, Odinite!";
const farewell = "Bye bye, Odinite!";
export { greeting, farewell };

export const - Inline: export at declaration time

export { } - Separate: declare first, export at the bottom

Both are identical - Choose whichever style you prefer. Separate export is nice when you want all exports listed in one place.

Named exports keep their exact name β€” importers must use the same name!

Named Imports

// two.js - Import specific named exports
import { greeting, farewell } from "./one.js";

console.log(greeting);  // "Hello, Odinite!"
console.log(farewell);  // "Bye bye, Odinite!"

// Or import only what you need
import { greeting } from "./one.js";
console.log(greeting);  // Works!

import { name } - Curly braces pull specific named exports from a module

from "./one.js" - Relative path to the module file (must include extension in browsers)

Selective imports - You only import what you need. Unused exports stay in their module.

⚠️ Not objects! { } is special import/export syntax, not destructuring!

Default Exports

One Default Export Per File

// one.js - Method 1: Inline default export
export default "Hello, Odinite!";

// one.js - Method 2: Separate default export
const greeting = "Hello, Odinite!";
export default greeting;

export default - Marks the "main" export of a file. Only one per module.

No name needed - The default export is nameless. The importer chooses what to call it.

Use default exports when a module has one primary thing to share (a class, a function, etc.)

Default Imports

// two.js - Name it whatever you want!
import helloOdinite from "./one.js";
console.log(helloOdinite);  // "Hello, Odinite!"

// Or name it something else
import myGreeting from "./one.js";
console.log(myGreeting);    // "Hello, Odinite!"

import name - No curly braces! You choose any name for the default import.

Same value, different names - Both helloOdinite and myGreeting receive the same default export.

Convention: name the import after the module or what it represents for clarity.

Mixing Default and Named

// one.js - Both default and named exports
export default "Hello, Odinite!";
export const farewell = "Bye bye, Odinite!";

// two.js - Import both
import greeting, { farewell } from "./one.js";

console.log(greeting);  // "Hello, Odinite!"
console.log(farewell);  // "Bye bye, Odinite!"

export default + export const - A file can have one default and many named exports

import greeting, { farewell } - Default first (no braces), then named in { }, separated by comma

Order matters - Default import must come before the named imports in the statement.

Entry Points

Load One File, Get Everything

<!-- Only link the entry point -->
<script src="two.js" type="module"></script>
two.js <── one.js

type="module" - Tells the browser this is an ES module, not a regular script

Dependency graph - Browser loads two.js, sees its imports, and automatically fetches one.js too.

Only one script tag needed β€” the browser follows the import chain!

Loading Modules in HTML

<!-- MUST use type="module" -->
<script src="main.js" type="module"></script>
  • βœ… Automatic defer: Loads after HTML is parsed
  • βœ… Strict mode: Runs in strict mode automatically
  • ⚠️ Local server required: Won't work with file://

type="module" - Required attribute that enables ES6 module behavior

Automatic benefits - No need for defer attribute or "use strict" β€” modules get both for free.

⚠️ CORS required - Modules use CORS, so you need a web server (Live Preview, etc.)

npm - Node Package Manager

Not Actually "Node Package Manager"!

npm is a package manager - a gigantic repository of plugins, libraries, and tools with a CLI to install them in our projects.

package.json

Project Metadata & Dependencies

{
  "name": "my-project",
  "version": "1.0.0",
  "description": "My awesome project",
  "scripts": {
    "test": "jest",
    "build": "webpack"
  },
  "devDependencies": {
    "webpack": "^5.89.0",
    "webpack-cli": "^5.1.4"
  }
}

"scripts" - Custom commands you can run with npm run <name>

"devDependencies" - Packages needed only during development (not in production)

^ - Caret: allows minor and patch updates (e.g., 5.89.0 up to <6.0.0)

Shareable config - Anyone can clone your repo and run npm install to get all dependencies!

Using npm

# Initialize a new project
npm init -y

# Install a package
npm install package-name

# Install as dev dependency
npm install --save-dev webpack

# Install all dependencies from package.json
npm install

npm init -y - Creates package.json with defaults (-y skips questions)

npm install - Adds a package to node_modules/ and records it in package.json

--save-dev - Saves to devDependencies instead of dependencies (build tools, test frameworks)

Running npm install with no package name installs everything from package.json.

node_modules and .gitignore

When you install packages, npm creates a node_modules directory containing all package code.

# .gitignore
node_modules
dist

node_modules - Can be thousands of files! Recreated by npm install.

dist - Generated output folder. Rebuilt every time you bundle.

⚠️ Never commit these! package.json tracks what to install.

Webpack - Module Bundler

Why Do We Need Bundling?

Webpack takes modules with dependencies and generates static assets

src and dist Directories

src (source)

  • Your development code
  • Where you work
  • Multiple files
  • Readable, organized

dist (distribution)

  • Bundled output
  • Ready for deployment
  • Fewer, optimized files
  • Generated by Webpack
πŸ’‘ Workflow: Work in src β†’ Build to dist β†’ Deploy dist

Setting Up Webpack

# Create project and initialize npm
mkdir webpack-practice && cd webpack-practice
npm init -y

# Install Webpack
npm install --save-dev webpack webpack-cli

# Create directories
mkdir src
touch src/index.js

&& - Chains commands: second runs only if first succeeds

webpack - The bundler core. webpack-cli - Command line interface to run it.

src/index.js - The entry point where Webpack starts building the dependency graph.

Both packages are dev dependencies β€” they're build tools, not production code.

webpack.config.js

// webpack.config.js
const path = require("path");

module.exports = {
  mode: "development",
  entry: "./src/index.js",
  output: {
    filename: "main.js",
    path: path.resolve(__dirname, "dist"),
    clean: true,
  },
};

require("path") - Node.js built-in for resolving file paths

mode - "development" for readable output, "production" for minified

entry - Where Webpack starts building the dependency graph

path.resolve(__dirname, "dist") - Absolute path to output directory

clean: true - Empties the dist folder before each build

Running Webpack

# Bundle your code
npx webpack

# Output:
# dist/
#   main.js  (bundled file)

npx - Runs a locally installed package without a global install

What happens:

  1. Reads entry point (src/index.js)
  2. Follows all imports to build dependency graph
  3. Bundles everything into dist/main.js

One command turns many source files into a single optimized bundle!

HtmlWebpackPlugin

Automatically Generate HTML

# Install the plugin
npm install --save-dev html-webpack-plugin
// webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  // ... other config
  plugins: [
    new HtmlWebpackPlugin({
      template: "./src/template.html",
    }),
  ],
};

plugins - Array of Webpack plugins that extend its functionality

new HtmlWebpackPlugin() - Creates an HTML file in dist with the bundled JS automatically linked

template - Your HTML skeleton. Plugin injects the <script> tag for you.

No more manually linking bundle files β€” the plugin handles it!

Loading CSS with Webpack

# Install CSS loaders
npm install --save-dev style-loader css-loader
// webpack.config.js
module.exports = {
  // ... other config
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: ["style-loader", "css-loader"],
      },
    ],
  },
};

module.rules - Tells Webpack how to handle different file types

test: /\.css$/i - Regex matching any file ending in .css

css-loader - Reads CSS files. style-loader - Injects CSS into the DOM.

⚠️ Order matters! Loaders run right-to-left: css-loader first, then style-loader.

Importing CSS in JavaScript

/* src/styles.css */
body {
  background-color: rebeccapurple;
}
// src/index.js
import "./styles.css";  // Side effect import!

console.log("CSS loaded!");

import "./styles.css" - Side effect import: no variable, just triggers the loaders

How it works - css-loader reads the file, style-loader injects a <style> tag into the HTML at runtime.

No <link> tags needed! Import CSS directly in your JavaScript files.

Loading Images

Three Scenarios

  1. In CSS: css-loader handles it automatically!
  2. In HTML template: Need html-loader
  3. In JavaScript: Need asset/resource rule
# For HTML images
npm install --save-dev html-loader

Why loaders? - Webpack only understands JS by default. Loaders teach it to process other file types.

html-loader - Parses HTML and resolves <img src="..."> paths for bundling

CSS images work for free because css-loader already handles url() references.

Image Configuration

// webpack.config.js
module.exports = {
  module: {
    rules: [
      // For HTML template images
      {
        test: /\.html$/i,
        loader: "html-loader",
      },
      // For JavaScript imports
      {
        test: /\.(png|svg|jpg|jpeg|gif)$/i,
        type: "asset/resource",
      },
    ],
  },
};

loader: "html-loader" - Single loader uses loader instead of use array

Regex breakdown: /\.(png|svg|...)$/i

  • / ... / β€” regex delimiters
  • \. β€” literal dot (escaped with \)
  • (png|svg|jpg|...) β€” matches any of these extensions (| means "or")
  • $ β€” must be at the end of the filename
  • i β€” case-insensitive flag (.PNG works too)

type: "asset/resource" - Built-in Webpack 5 feature: copies file to dist and returns the URL

No extra npm package needed for asset/resource β€” it's built into Webpack 5!

Using Images in JavaScript

// src/index.js
import odinImage from "./odin.png";

const image = document.createElement("img");
image.src = odinImage;  // Correct bundled path!

document.body.appendChild(image);

import odinImage - Returns the final URL/path to the image after bundling

image.src = odinImage - Sets the src to the Webpack-resolved path (e.g., dist/abc123.png)

Why import? - Webpack needs to know about the image to include it in the bundle and set the correct path!

webpack-dev-server

Auto-Reload on Changes

# Install dev server
npm install --save-dev webpack-dev-server
// webpack.config.js
module.exports = {
  devtool: "eval-source-map",
  devServer: {
    watchFiles: ["./src/template.html"],
  },
};
# Start dev server
npx webpack serve

devtool: "eval-source-map" - Enables source maps so errors show original file/line numbers

watchFiles - Also watches HTML changes (JS/CSS watched automatically)

npx webpack serve - Starts a local server at localhost:8080 with hot reload

Save a file and the browser refreshes automatically β€” no manual reload needed!

Source Maps

devtool: "eval-source-map" creates source maps so error messages reference your original source files, not the bundled mess!

Project: Restaurant Page

Dynamic DOM with Modules!

Project Goals

  • πŸ—οΈ Set up Webpack from scratch
  • πŸ“¦ Use ES6 modules to organize code
  • 🎨 Create tabbed navigation (Home, Menu, About)
  • ⚑ Generate ALL content with JavaScript

Restaurant Page Structure

restaurant-page/
β”œβ”€β”€ src/
β”‚   β”œβ”€β”€ index.js          # Entry point
β”‚   β”œβ”€β”€ home.js           # Home tab module
β”‚   β”œβ”€β”€ menu.js           # Menu tab module
β”‚   β”œβ”€β”€ about.js          # About tab module
β”‚   β”œβ”€β”€ template.html     # HTML skeleton
β”‚   └── styles.css        # Styles
β”œβ”€β”€ dist/                 # Generated by Webpack
β”œβ”€β”€ webpack.config.js
β”œβ”€β”€ package.json
β”œβ”€β”€ .gitignore
└── node_modules/

src/ - Your working code. Each tab is its own module (home.js, menu.js, about.js).

dist/ - Webpack's output. Never edit files here β€” they get overwritten on every build.

index.js - Entry point that imports all tab modules and handles switching between them.

One module per tab keeps code organized and easy to maintain!

Implementation Steps

  1. Initialize npm and install Webpack
  2. Create webpack.config.js with HtmlWebpackPlugin
  3. Create .gitignore (node_modules, dist)
  4. Build HTML skeleton with header nav in template.html
  5. Create modules for each tab (home, menu, about)
  6. Write tab-switching logic in index.js
  7. Add event listeners to nav buttons
  8. Clear and populate #content div on tab switch

Tab Switching Pattern

// src/index.js
import loadHome from "./home.js";
import loadMenu from "./menu.js";
import loadAbout from "./about.js";

const content = document.getElementById("content");

function clearContent() {
  content.textContent = "";
}

document.getElementById("home-btn")
  .addEventListener("click", () => {
    clearContent();
    loadHome();
  });

document.getElementById("menu-btn")
  .addEventListener("click", () => {
    clearContent();
    loadMenu();
  });

// Load home page initially
loadHome();

import loadHome from - Each tab is a default-exported function from its own module

clearContent() - Empties the content div before loading new tab content

addEventListener - Each button clears then loads its tab module

Initial load - loadHome() at the bottom runs on page load so the home tab shows first.

Tab Module Example

// src/home.js
export default function loadHome() {
  const content = document.getElementById("content");

  const heading = document.createElement("h1");
  heading.textContent = "Welcome to Our Restaurant";

  const description = document.createElement("p");
  description.textContent = "Best food in town!";

  content.appendChild(heading);
  content.appendChild(description);
}

export default function - Declares and exports the function in one line

createElement + appendChild - All DOM content is created in JS, not in HTML

Pattern - Each tab module exports one function that builds and appends its section's DOM elements.

The HTML template only has a nav and an empty #content div β€” JS does the rest!

Deploying to GitHub Pages

# First time only - create gh-pages branch
git branch gh-pages

# Every deployment:
git checkout gh-pages && git merge main --no-edit
npx webpack
git add dist -f && git commit -m "Deployment commit"
git subtree push --prefix dist origin gh-pages
git checkout main

git merge main - Brings latest code into the deploy branch

git add dist -f - Force-adds dist (normally gitignored) for deployment

subtree push --prefix dist - Pushes only the dist folder contents to gh-pages

⚠️ Remember: Set GitHub Pages source to gh-pages branch in repo settings!

Webpack Configuration Summary

const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  mode: "development",
  entry: "./src/index.js",
  output: {
    filename: "main.js",
    path: path.resolve(__dirname, "dist"),
    clean: true,
  },
  devtool: "eval-source-map",
  devServer: {
    watchFiles: ["./src/template.html"],
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: "./src/template.html",
    }),
  ],
  module: {
    rules: [
      { test: /\.css$/i, use: ["style-loader", "css-loader"] },
      { test: /\.html$/i, loader: "html-loader" },
      { test: /\.(png|svg|jpg|jpeg|gif)$/i, type: "asset/resource" },
    ],
  },
};

Complete config - Everything from this lecture in one file:

mode + entry + output - Core: what, where, and how to bundle

devtool + devServer - Development experience: source maps + auto-reload

plugins - Extend Webpack's capabilities (HTML generation)

module.rules - Teach Webpack to process CSS, HTML, and images

Today's Takeaways

You Can Now:

πŸ“… Next Class: JSON and OOP

πŸ“ Homework: Restaurant Page