After configuring the development setup with Storybook for JS, CSS and other assets, it's time to finally create a build pipeline to publish our HTML, JS and CSS with Webpack.
yarn add webpack webpack-cli lodash --dev
The key idea is to traverse through the component tree by invoking React's render function and trigger side-effect operations to gather all scripts.
Also, we can tap into the import statements to gather CSS imports as well.
// useScripts.js
import { useEffect } from "react";
// The utils we'll be building
import { addScripts } from "../scripts/buildUtils";
export function useScripts(scriptLoaders) {
  // Accumulate imported scripts during builds
  if (process.env.BUILD) {
    const scriptPaths = scriptLoaders.map((load) => {
      const stringified = load.toString();
      // We extract the import path from the stringified load function
      const match = stringified.match(/"([^"]*)"/);
      if (!match) {
        throw new Error(
          `Error finding matching imported module under \`useScripts\` hooks\nstringified:\n${stringified}`
        );
      }
      return match[1].replace(/\?.*$/, "");
    });
    // The parent's id is required to resolve the absolute path for the script
    addScripts(scriptPaths, module.parent.id);
    // Node caches only the first parent but we need the intermediate ones
    // so we have to clear it.
    delete require.cache[__filename];
  }
  useEffect(() => {
    loadScripts(scriptLoaders);
  });
}
// Each dynamic import is asynchronous
async function loadScripts(scriptLoaders) {
  for (const load of scriptLoaders) {
    await load();
  }
}
// generateEntryPoints.js
import React from "react";
import { renderToStaticMarkup } from "react-dom/server";
import path from "path";
import fs from "fs";
import uniq from "lodash/uniq";
import glob from "glob";
// The utils are also used here
import {
  setCurrentChunkName,
  getScriptGroup,
  addScripts,
  getChunkNameFromPagePath,
} from "../scripts/buildUtils";
// Side-effects tapped into import statements for CSS files
require.extensions[".css"] = (cssModule) => {
  addScripts([cssModule.id], cssModule.parent.id);
};
const cwd = path.resolve(__dirname, "..");
const pages = glob.sync("pages/**/*.jsx", { cwd });
function main() {
  pages.forEach((pagePath) => {
    // We'll use each page's path as its own chunk name
    setCurrentChunkName(getChunkNameFromPagePath(pagePath));
    const PageComponent = require(path.resolve(cwd, pagePath)).default;
    // This is where the magic happens. It invokes React's rendering and will trigger
    // the side-effects to gather the scripts.
    renderToStaticMarkup(<PageComponent />);
  });
  const assetsImportContent = Object.entries(getScriptGroup())
    .map(([chunkName, scriptPaths]) => {
      return `  '${chunkName}': [${uniq(scriptPaths)
        .map((scriptPath) => `'${scriptPath}'`)
        .join(",\n")}]`;
    })
    .join(",\n");
  const assetsMapContent = `module.exports = {\n${assetsImportContent}\n}\n`;
  // The generated entry points are in object notation written in to a file.
  fs.writeFileSync(
    path.resolve(__dirname, "../.tmp/entries.js"),
    assetsMapContent
  );
}
main();
// buildUtils.js
import path from "path";
let currentChunkName;
const scriptGroup = {};
export function getScriptGroup() {
  return scriptGroup;
}
export function addScripts(scripts, parentId) {
  if (!scriptGroup[currentChunkName]) {
    scriptGroup[currentChunkName] = [];
  }
  const resolvedScripts = scripts.map((s) =>
    path.resolve(path.basename(parentId), s)
  );
  scriptGroup[currentChunkName].push(...resolvedScripts);
}
export function setCurrentChunkName(chunkName) {
  currentChunkName = chunkName;
  scriptGroup[currentChunkName] = [];
}
export function getChunkNameFromPagePath(pagePath) {
  return pagePath.replace(/\//g, "-");
}
├── pages │ ├── AboutUs.jsx │ ├── Home.css │ └── Home.jsx ├── scripts │ ├── build.js │ ├── buildUtils.js // added │ └── generateEntryPoints.js // added ├── src │ ├── about-us.js │ ├── home.js │ └── useScripts.js // modified ├── stories │ └── pages.stories.js ├── babel.config.js └── package.json
// package.json
"scripts": {
  "build:entries": "mkdir -p .tmp && BUILD=1 babel-node scripts/generateEntryPoints.js",
  ...
}
yarn build:entries
// entries.js
module.exports = {
  "pages-AboutUs.jsx": [
    "/abs/path/to/pages/Home.css",
    "/abs/path/to/src/about-us",
  ],
  "pages-Home.jsx": [
    "/abs/path/to/pages/Home.css",
    "/abs/path/to/src/home",
  ],
};
yarn add mini-css-extract-plugin css-loader stats-webpack-plugin --dev
// webpack.config.js
const path = require("path");
const glob = require("glob");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const StatsPlugin = require("stats-webpack-plugin");
const entries = require("./.tmp/entries");
module.exports = {
  entry: {
    ...entries,
  },
  resolve: {
    extensions: [".js", ".jsx"],
  },
  output: {
    path: path.resolve(__dirname, "dist/assets"),
    publicPath: "/assets/",
    filename: "[name]-[contenthash].js",
  },
  mode: "production",
  target: "web",
  module: {
    rules: [
      {
        test: /\.css$/i,
        use: [MiniCssExtractPlugin.loader, "css-loader"],
      },
    ],
  },
  plugins: [
    new MiniCssExtractPlugin(),
    new StatsPlugin("../../.tmp/webpack-stats.json"),
  ],
};
"scripts": {
  ...
  "build:webpack": "webpack",
  ...
},
yarn build:webpack
import React from "react";
import path from "path";
import fs from "fs";
import glob from "glob";
import { renderToStaticMarkup } from "react-dom/server";
import { spawnSync } from "child_process";
import webpackStats from "../.tmp/webpack-stats.json";
import { getChunkNameFromPagePath } from "./buildUtils";
require.extensions[".css"] = () => {};
const cwd = path.resolve(__dirname, "..");
// We glob all the files under "pages" directory
const reactFiles = glob.sync("pages/**/*.jsx", { cwd });
// Generate <link rel="stylesheet" href="..."> tags from Webpack's stats file
function generateStyleTags(assets) {
  function getCSSAssets() {
    if (!assets) {
      return [];
    }
    if (Array.isArray(assets)) {
      return assets.filter((asset) => asset.endsWith(".css"));
    }
    return assets.endsWith(".css") ? [assets] : [];
  }
  const cssAssets = getCSSAssets();
  return cssAssets.length > 0
    ? cssAssets
        .map((asset) => `<link rel="stylesheet" href="/assets/${asset}">`)
        .join("\n")
    : "";
}
// Generate <script type="text/javascript" src="..."> tags from Webpack's stats file
function generateScriptTags(assets) {
  function getJSAssets() {
    if (!assets) {
      return [];
    }
    if (Array.isArray(assets)) {
      return assets.filter((asset) => asset.endsWith(".js"));
    }
    return assets.endsWith(".js") ? [assets] : [];
  }
  const jsAssets = getJSAssets();
  return jsAssets.length > 0
    ? jsAssets
        .map(
          (asset) =>
            `<script type="text/javascript" src="/assets/${asset}"></script>`
        )
        .join("\n")
    : "";
}
reactFiles.forEach((reactFile) => {
  const fileName = path.basename(reactFile).replace(".jsx", "");
  // Page components are from "default" export
  const Component = require(path.resolve(__dirname, "../", reactFile)).default;
  const chunkName = getChunkNameFromPagePath(reactFile);
  const assets = webpackStats.assetsByChunkName[chunkName];
  const staticMarkup = renderToStaticMarkup(<Component />);
  // Generate HTML file name from ".jsx" files by replacing the extension with ".html"
  const htmlFile = path.resolve(
    __dirname,
    "../dist/",
    reactFile.replace(".jsx", ".html")
  );
  const dirName = path.dirname(htmlFile);
  // In case we have nested page structure, we need to create directory recursively.
  fs.mkdirSync(dirName, { recursive: true });
  fs.writeFileSync(
    htmlFile,
    `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>${fileName}</title>
  ${generateStyleTags(assets) /* CSS files are linked here */}
</head>
<body>
  ${staticMarkup}
  ${generateScriptTags(assets) /* JS files are linked here */}
</body>
</html>`,
    "utf8"
  );
});
"scripts": {
  ...
  "build": "yarn build:entries && yarn build:webpack && babel-node scripts/build.js",
  ...
},
yarn build
npm install --global http-server
http-server dist

From us to your inbox weekly.