Modular JavaScript
In Chapter 6, all your JavaScript lived in one file: main.js. That works for a small amount of code. It doesn’t scale. As a project grows, a single JavaScript file becomes hard to navigate, hard to debug, and hard to share logic between pages.
Modules solve this. A module is a JavaScript file with a defined interface: the things it exports (makes available to other files) and the things it imports (uses from other files). Each module handles one concern. Files that need that functionality import it. Nothing else has access to the internals.
This is the same principle behind Sass partials, applied to JavaScript.
How modules work
A module exports values using the export keyword. Another file imports them with import.
Named exports: a file can export multiple things by name.
// src/js/utils.js
export function formatDate(date) {
return date.toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' });
}
export function clamp(value, min, max) {
return Math.min(Math.max(value, min), max);
}Import them by name, in curly braces:
// src/js/main.js
import { formatDate, clamp } from './utils.js';Default exports: a file can have one default export, imported without curly braces.
// src/js/toggle.js
export default function setupToggle(buttonSelector, targetSelector) {
const btn = document.querySelector(buttonSelector);
const target = document.querySelector(targetSelector);
if (!btn || !target) return;
btn.addEventListener('click', () => {
target.classList.toggle('is-hidden');
});
}// src/js/main.js
import setupToggle from './toggle.js';Use named exports for utility functions and values you want to pick and choose from. Use a default export when a file’s main purpose is one thing.
CommonJS vs ES modules
You’ll notice the gulpfile.js uses require() instead of import. That’s because Gulp runs in Node, and the example uses CommonJS, Node’s older module format. ES modules (using import/export) are the current standard for both browsers and modern Node. You’ll write your application code as ES modules and keep the Gulpfile in CommonJS for now, since Gulp plugins vary in their module support.
The key distinction is where each runs: CommonJS is for Node tooling, ES modules are for your application code.
Why you still need a bundler
Modern browsers support ES modules natively. You can use <script type="module" src="main.js"> and browser-native imports work fine in development. For a WordPress theme, you don’t want to rely on that. You want a single, self-contained JavaScript file that works in any browser your theme targets, with all dependencies resolved at build time.
That’s what a bundler does. Rollup takes your entry point, follows all the import statements, and outputs a single bundled file. It’s the bundler used by Vite, and it’s well-suited to this kind of library and theme output.
Install Rollup
npm install --save-dev rollup @rollup/plugin-node-resolverollup is the bundler. @rollup/plugin-node-resolve lets Rollup find packages installed in node_modules, which you’ll need as you add dependencies.
Restructure your JavaScript
Create a js/ folder inside src/ with a module structure:
mkdir src/js
touch src/js/main.js
touch src/js/toggle.jsMove the toggle logic out of src/main.js into src/js/toggle.js:
// src/js/toggle.js
export default function setupToggle(buttonSelector, targetSelector) {
const btn = document.querySelector(buttonSelector);
const target = document.querySelector(targetSelector);
if (!btn || !target) return;
btn.addEventListener('click', () => {
target.classList.toggle('is-hidden');
});
}The null check (if (!btn || !target) return) is a habit worth building. If the expected elements don’t exist on the page, the function exits cleanly instead of throwing a reference error.
Now write src/js/main.js as the entry point that imports and initializes everything:
// src/js/main.js
import setupToggle from './toggle.js';
setupToggle('#toggle-btn', '.intro');Delete the old src/main.js.
Update the Gulp task
Replace the js function in gulpfile.js with a Rollup-powered version:
const { src, dest, watch, series, parallel } = require('gulp');
const browserSync = require('browser-sync').create();
const sass = require('gulp-sass')(require('sass'));
const rollup = require('rollup');
const { nodeResolve } = require('@rollup/plugin-node-resolve');
function html() {
return src('src/**/*.html')
.pipe(dest('dist/'));
}
function css() {
return src('src/scss/main.scss')
.pipe(sass({ outputStyle: 'expanded' }).on('error', sass.logError))
.pipe(dest('dist/css/'));
}
async function js() {
const bundle = await rollup.rollup({
input: 'src/js/main.js',
plugins: [nodeResolve()],
});
await bundle.write({
file: 'dist/js/main.js',
format: 'iife',
name: 'App',
sourcemap: true,
});
}
function serve() {
browserSync.init({
server: {
baseDir: './dist',
},
});
watch('src/**/*.html', series(html, reload));
watch('src/scss/**/*.scss', series(css, reload));
watch('src/js/**/*.js', series(js, reload));
}
function reload(done) {
browserSync.reload();
done();
}
exports.build = parallel(html, css, js);
exports.default = series(parallel(html, css, js), serve);The format: 'iife' option wraps the bundle in an immediately invoked function expression, which prevents your variables from leaking into the global scope. sourcemap: true generates a source map file so browser developer tools show you the original source files when debugging, not the bundled output.
Update the <script> tag in index.html:
<script src="js/main.js"></script>Run gulp. Rollup bundles src/js/main.js and its imports into dist/js/main.js. The toggle still works. The page behavior is unchanged.
A note on Babel
Babel is a JavaScript compiler that transforms modern syntax into code compatible with older browsers. If you need to support environments that don’t understand newer JavaScript features, Babel goes between your source and the bundle output. For this series, you don’t need it: the browsers targeted by modern WordPress themes understand the JavaScript you’re writing. It’s worth knowing Babel exists and what it does, but adding it to this pipeline would be complexity without benefit at this stage.
Commit your work
git add .
git commit -m "Convert JS to ES modules with Rollup bundler"What you now know
You can organize JavaScript into modules with clear import and export interfaces, and bundle them into a single file for production. The entry point pattern you’ve established here (one main.js that imports and initializes everything) is how the WordPress theme’s JavaScript will work. In Chapter 11 you’ll add linting to the pipeline, which automates code quality checks across your JavaScript, Sass, and PHP.
Reference
- MDN: JavaScript modules
- MDN:
import - MDN:
export - Rollup documentation
- Rollup: output formats
- @rollup/plugin-node-resolve
- Babel documentation