Webpack from npm: from install to one HTML page

Here webpack means the JavaScript bundler you install from npm. It runs on your computer, and what it produces is a script file that a plain HTML page can load.

"Webpack" here is the JavaScript bundler on npm, not a web page feature. Install it with npm, run it with npx, and it writes one script file that a normal HTML page loads with a script tag.

npm install webpack webpack-cli --save-dev
npx webpack

The build itself cannot run inside an HTML file. What a plain page can show is the idea: many modules, one script. Press the button below.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>What a bundle looks like</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  h1 { font-size: 16px; margin: 0 0 4px; }
  p { margin: 0 0 10px; font-size: 13.5px; line-height: 1.45; color: #4b5563; }
  pre { margin: 0 0 10px; padding: 10px 12px; background: #fff; border: 1px solid #e1e4ea; border-radius: 8px;
        font: 12.5px/1.5 ui-monospace, Consolas, monospace; overflow-x: auto; white-space: pre; }
  button { font: 600 14px system-ui, sans-serif; padding: 9px 16px; border: 0; border-radius: 8px;
           background: #2563eb; color: #fff; cursor: pointer; }
  #out { min-height: 54px; border-color: #86c9a0; background: #f0faf4; }
</style>
</head>
<body>
<h1>One file, three modules</h1>
<p>A bundle keeps each source file in a table and runs the entry file. This is a simplified picture, not webpack's real output.</p>

<pre id="code">// one script, three modules
const modules = {
  'math.js':  (module) =&gt; { module.exports = { add: (a, b) =&gt; a + b }; },
  'greet.js': (module) =&gt; { module.exports = (n) =&gt; 'Hello, ' + n; },
  'index.js': (module, require) =&gt; {
    const { add } = require('math.js');
    const greet = require('greet.js');
    module.exports = greet('bundle') + ' - 2 + 3 = ' + add(2, 3);
  },
};</pre>

<button id="run" type="button">Run index.js</button>
<pre id="out">Press the button.</pre>

<script>
  const modules = {
    'math.js':  (module) => { module.exports = { add: (a, b) => a + b }; },
    'greet.js': (module) => { module.exports = (n) => 'Hello, ' + n; },
    'index.js': (module, require) => {
      const { add } = require('math.js');
      const greet = require('greet.js');
      module.exports = greet('bundle') + ' - 2 + 3 = ' + add(2, 3);
    },
  };

  const cache = {};
  function req(id) {
    if (cache[id]) return cache[id].exports;      // each module runs once
    const module = cache[id] = { exports: {} };
    modules[id](module, req);
    return module.exports;
  }

  document.getElementById('run').addEventListener('click', () => {
    document.getElementById('out').textContent = req('index.js');
  });
</script>
</body>
</html>
Three modules in one table, run from one entry. A simplified picture of a bundle, not webpack's real output.

What webpack does

Webpack's own docs call it a static module bundler. It starts at an entry file, follows every import, and gathers what it finds into output files. Five words from its core concepts page are enough to start.

Word What it means
Entry Where webpack starts reading. Default: ./src/index.js
Output Where the bundle goes. Default: ./dist/main.js
Loaders Turn other file types into modules webpack can include
Plugins Wider jobs, such as optimizing the bundle or generating HTML
Mode development, production or none, each with its own built-in settings
Source files go into webpack and build output files come out.
Source files go into webpack and build output files come out.

Install it and run it

Do this in a project folder that has Node.js and npm. The getting started guide says its examples use webpack-cli 7, which needs Node.js 20.9.0 or later.

  1. Install both packages with the first command above. webpack is the bundler, and webpack-cli runs it from the command line.
  2. Write src/index.js. Import your other files from it.
  3. Run npx webpack. Webpack follows the imports and writes dist/main.js.
  4. Load that file from a page. Point a script tag at main.js.

Nothing else is needed for a first build, because the entry and output defaults already match the folders above. Add a webpack.config.js when you want to change them.

The build step needs Node.js on your computer. The result is plain HTML and one script.
The build step needs Node.js on your computer. The result is plain HTML and one script.

Make the HTML page

Webpack builds scripts, not pages. The html-webpack-plugin fills that gap: it generates an index.html that serves your bundles, and it puts the script in the head with the defer loading strategy.

// webpack.config.js
const HtmlWebpackPlugin = require("html-webpack-plugin");

module.exports = {
  mode: "production",
  entry: "./src/index.js",
  output: { filename: "main.js", path: __dirname + "/dist" },
  plugins: [new HtmlWebpackPlugin()],
};

Install the plugin first. If you write the page by hand instead, point it at the built file, not the source.

npm i --save-dev html-webpack-plugin
The script tag must name the built file. A source file with import lines does not run as a plain script.
The script tag must name the built file. A source file with import lines does not run as a plain script.

Get everything into one HTML file

Two files, a page and a script, are easy to lose on the way to someone. To make one file, paste the contents of dist/main.js into a script tag at the end of the body.

<body>
  <div id="app"></div>
  <script>
    /* paste dist/main.js here */
  </script>
</body>

Put the script after the elements it uses. The defer attribute has no effect on a script without a src, so an inline script runs as soon as it is reached.

Images can join the file too. The asset/inline type exports a data URI, and asset/source exports a file's text as a string. Try the whole idea in the page below: edit two modules, bundle them, and read the single HTML file it writes.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Mini bundler to one HTML file</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  h1 { font-size: 16px; margin: 0 0 4px; }
  p { margin: 0 0 10px; font-size: 13.5px; line-height: 1.45; color: #4b5563; }
  label { display: block; font: 700 12px ui-monospace, Consolas, monospace; color: #0f5132; margin: 8px 0 3px; }
  textarea { width: 100%; box-sizing: border-box; height: 84px; padding: 8px 10px; border: 1px solid #cfd5df; border-radius: 8px;
             font: 12.5px/1.45 ui-monospace, Consolas, monospace; resize: vertical; white-space: pre; }
  button { font: 600 14px system-ui, sans-serif; padding: 9px 16px; border: 0; border-radius: 8px;
           background: #2563eb; color: #fff; cursor: pointer; margin: 10px 0; }
  #result { padding: 10px 12px; min-height: 36px; background: #f0faf4; border: 1px solid #86c9a0; border-radius: 8px; font-size: 14px; }
  #page { width: 100%; box-sizing: border-box; height: 150px; background: #fff; }
  .note { font-size: 12.5px; color: #6b7280; margin: 6px 0 0; }
</style>
</head>
<body>
<h1>Mini bundler</h1>
<p>Edit the two modules, then bundle. You get one script and one HTML page you could save as a file.</p>

<label for="m-math">math.js</label>
<textarea id="m-math" spellcheck="false">module.exports = {
  area: (w, h) => w * h,
};</textarea>

<label for="m-index">index.js (entry)</label>
<textarea id="m-index" spellcheck="false">const { area } = require('math.js');
result('A 6 x 7 card has area ' + area(6, 7));</textarea>

<button id="go" type="button">Bundle and run</button>

<label for="result">Result of running the bundle</label>
<div id="result">Not run yet.</div>

<label for="page">The single HTML file</label>
<textarea id="page" readonly spellcheck="false"></textarea>
<p class="note">Any closing script tag inside the code is written as &lt;\/script so it cannot end the script early.</p>

<script>
  const $ = (id) => document.getElementById(id);

  function bundle(mods, entry) {
    // wrap each module in a function and keep them in one table
    const table = Object.keys(mods).map((name) =>
      JSON.stringify(name) + ': function (module, require) {\n' + mods[name] + '\n}'
    ).join(',\n');
    return '(function () {\n' +
      'var modules = {\n' + table + '\n};\n' +
      'var cache = {};\n' +
      'function require(id) {\n' +
      '  if (cache[id]) return cache[id].exports;\n' +
      '  var module = cache[id] = { exports: {} };\n' +
      '  modules[id](module, require);\n' +
      '  return module.exports;\n' +
      '}\n' +
      'require(' + JSON.stringify(entry) + ');\n' +
      '})();';
  }

  // a closing script tag inside inline code would end the script element
  const safe = (code) => code.replace(/<\/script/gi, '<\\/script');

  // the bundled code calls this to show its answer
  window.result = (text) => { $('result').textContent = text; };

  $('go').addEventListener('click', () => {
    const code = bundle({
      'math.js': $('m-math').value,
      'index.js': $('m-index').value,
    }, 'index.js');

    $('result').textContent = 'Running...';
    const s = document.createElement('script');
    s.textContent = code;
    try { document.body.appendChild(s); } catch (err) { $('result').textContent = 'Error: ' + err.message; }
    s.remove();

    $('page').value =
      '<!doctype html>\n<html>\n<head><meta charset="utf-8"><title>Bundled</title></head>\n<body>\n<div id="result"></div>\n' +
      '<script>\nwindow.result = function (t) { document.getElementById("result").textContent = t; };\n' +
      safe(code) + '\n<\/script>\n</body>\n</html>';
  });

  // errors inside an injected script surface here, not in try/catch
  window.addEventListener('error', (e) => { $('result').textContent = 'Error: ' + e.message; });
</script>
</body>
</html>
Edit the modules, press the button. The page runs the bundle and prints a single HTML file you could save.

One detail matters. The HTML standard says the script text must not contain a closing script tag, and recommends writing it as \x3C/script>. A built bundle that contains that string inside a text value will cut your page off unless it is escaped.

Skip the build when you can

If you only need a library, you may not need webpack. A script tag can load it from a CDN, pinned to one version, and the library adds itself to the page. The lodash README names the global _.

Live exampletry it here, then copy the code
Share it as a link
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>A library without a build step</title>
<style>
  body { margin: 0; padding: 14px; font-family: system-ui, sans-serif; background: #f4f5f7; color: #1d2330; }
  h1 { font-size: 16px; margin: 0 0 4px; }
  p { margin: 0 0 10px; font-size: 13.5px; line-height: 1.45; color: #4b5563; }
  textarea { width: 100%; box-sizing: border-box; height: 64px; padding: 8px 10px; border: 1px solid #cfd5df; border-radius: 8px;
             font: 14px ui-monospace, Consolas, monospace; resize: vertical; }
  .row { display: flex; gap: 8px; margin: 8px 0 10px; flex-wrap: wrap; }
  button { font: 600 14px system-ui, sans-serif; padding: 9px 14px; border: 0; border-radius: 8px;
           background: #2563eb; color: #fff; cursor: pointer; }
  button.alt { background: #fff; color: #1d2330; border: 1px solid #cfd5df; }
  pre { margin: 0; padding: 10px 12px; min-height: 48px; background: #f0faf4; border: 1px solid #86c9a0; border-radius: 8px;
        font: 13px/1.5 ui-monospace, Consolas, monospace; white-space: pre-wrap; overflow-wrap: anywhere; }
  #status { font-size: 12.5px; color: #6b7280; margin-bottom: 8px; }
</style>
<!-- the library arrives with a plain script tag, pinned to one version -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js"></script>
</head>
<body>
<h1>lodash from a script tag</h1>
<p>No npm, no config. Type words separated by spaces.</p>
<div id="status"></div>
<textarea id="words">pear apple fig banana cherry date plum</textarea>
<div class="row">
  <button id="sort" type="button">Sort by length</button>
  <button id="chunk" type="button">Chunk by 3</button>
  <button id="shuffle" class="alt" type="button">Shuffle</button>
</div>
<pre id="out">Pick an action.</pre>

<script>
  const status = document.getElementById('status');
  const out = document.getElementById('out');
  const words = () => document.getElementById('words').value.split(/\s+/).filter(Boolean);

  // the script tag above created a global called _ (lodash)
  if (typeof _ === 'undefined') {
    status.textContent = 'lodash did not load. Check the connection.';
  } else {
    status.textContent = 'lodash ' + _.VERSION + ' loaded.';
  }

  const show = (v) => { out.textContent = JSON.stringify(v); };
  document.getElementById('sort').addEventListener('click', () => {
    if (typeof _ !== 'undefined') show(_.sortBy(words(), 'length'));
  });
  document.getElementById('chunk').addEventListener('click', () => {
    if (typeof _ !== 'undefined') show(_.chunk(words(), 3));
  });
  document.getElementById('shuffle').addEventListener('click', () => {
    if (typeof _ !== 'undefined') show(_.shuffle(words()));
  });
</script>
</body>
</html>
lodash 4.17.21 from cdnjs with one script tag. No npm and no config.

Reach for webpack when you have your own modules to combine, or many dependencies. Reach for a script tag when one library is enough. A related walkthrough with a CSS framework is Add Tailwind to an HTML file.

Development and production mode

If you do not set a mode, webpack uses production, which enables minification. Development mode defaults the devtool setting to eval, so each module runs inside eval(). The docs call that pretty fast, but line numbers do not map back to your original code.

For a file you plan to paste or share, use production, or set devtool: false to get plain output. Minification explains what the smaller file trades away.

When it does not work

What you see Cause Fix
Blank page, console error about import The page loads src/index.js, not the build Point the script tag at dist/main.js
Command not found webpack or webpack-cli is not installed locally Install both, run npx webpack
Module script blocked when opened by double click MDN: modules loaded from file:// hit CORS errors Use a classic script tag, or a web server
Bare import like "lodash" fails in the browser Browsers need a URL or relative path, unless an import map is set Bundle it with webpack, or add an import map
Page cuts off partway through the script The code contains a closing script tag Write it as \x3C/script>
Inline script cannot find your element The script runs before the element exists Move it to the end of the body
Output is full of eval( Development mode defaults devtool to eval Use production, or set devtool to false
Files 404 after uploading to a folder The public path points somewhere else Check output.publicPath, which defaults to auto

For the folder problem in full, see Host a built front end as static files. For other scripts that do nothing, see HTML JavaScript not working.

A built page is easier to show than to explain. A dist folder with several files is easy to break on the way, and a screenshot cannot be clicked. A single inline file is one thing to send.

To send the working version, paste the page into a NOS document and choose Create share link. HTML to link walks through it.

The page renders as written and its scripts run, so the people you send it to can press the buttons themselves. If you change the code later, the same link shows the new version.

Questions people ask

What does npm install webpack actually give me?

The webpack package, which is a static module bundler for JavaScript applications. You also install webpack-cli, the tool that runs webpack on the command line. Neither is needed by the finished page.

Do I need Node.js to use webpack?

Yes, to build. The getting started guide says its examples use webpack-cli 7, which needs Node.js 20.9.0 or later. The built page does not need Node.js at all, because it is plain HTML and JavaScript.

Can I run webpack inside a pasted HTML page?

No. You run webpack from the command line on your own computer. What you can run in a page is its output: one built script, or a small page with that script inlined.

Can webpack produce a single HTML file?

The html-webpack-plugin generates an index.html that points at your bundles. To end up with one file, paste the contents of the built script into a script tag in that page. Asset modules can also inline images as data URIs.

Is webpack free to use?

The webpack repository is published under the MIT license.

Keep reading