Create React App for multiple entry points

Published on
4 mins read
--- views

Sometimes it happens that you React-project contains not only one true single-page application. Especially, if you create solution for already developed website with different frontend technologies been used on it before. I was very surprised that Create React App doesn't allow you to solve this problem. With it's help from box, you can create only single-page application and nothing more. Also, one of react-developers asks for the same question here:

Well, not preferred solution for me. Because, in future, me with my team have plans to create some new react application where same components will be used. And i want to save file hierarchy in project such as this:

Let me explain the logic:

  • Components - dumb Components/PureComponents/FunctionalComponents
  • Modules - functionality parts with backend interactions, redux-store interactions, actions, selectors, sagas and so on
  • Pages - (or views/scenes) - Component that aggregates all components for target page. For example, if i have a search page, on SearchPage i will include 2 another components: SearchInput, SearchResults and so on. It is like routes but divided on a different pages (just like server-rendering).

As build staff, i want to have separate .html file with .js.bundle file for each entry in pages/ folder. So on a production stand i can take this set manually and include it in page.

On screen there is a lot of other stuff such as maps, code divided by chunks ant so on, but i hope you've got the idea :) So, let me show how i solve this task: ### yarn eject

This command ejects all configuration scripts from create-react-app. Use it carefully because you cannot rollback later (only with git, of course). After that you will have some new folders with js scripts in it:

config/paths.js

Here i added new line with array of paths for all my pages:

// config after eject: we're in ./config/
module.exports = {
  dotenv: resolveApp('.env'),
  appPath: resolveApp('.'),
  appBuild: resolveApp('build'),
  appPublic: resolveApp('public'),
  // techcode
  appPages: [
    {
      name: modalUpdateOperationalPlan,
      title: ModalUpdateOperationalPlan,
      appHtml: resolveApp(public / modalUpdateOperationalPlan.html),
      appIndexJs: resolveApp(src / pages / ModalUpdateOperationalPlan / index.tsx),
    },
  ],
  appPackageJson: resolveApp('package.json'),
  appSrc: resolveApp('src'),
  appTsConfig: resolveApp('tsconfig.json'),
  appJsConfig: resolveApp('jsconfig.json'),
  yarnLockFile: resolveApp('yarn.lock'),
  testsSetup: resolveModule(resolveApp, 'src/setupTests'),
  proxySetup: resolveApp('src/setupProxy.js'),
  appNodeModules: resolveApp('node_modules'),
  publicUrl: getPublicUrl(resolveApp('package.json')),
  servedPath: getServedPath(resolveApp('package.json')),
};

config/webpack.config.js

Here i added part of code that creates new instance of HtmlWebpackPlugin for each entry:

// techcode
const htmlPlugins = [];

paths.appPages.forEach((appPage) => {
  htmlPlugins.push(
    new HtmlWebpackPlugin(
      Object.assign(
        {},
        {
          inject: true,
          template: appPage.appHtml,
          filename: `${appPage.name}.html`,
          title: appPage.title,
          chunks: [appPage.name],
        },
        isEnvProduction
          ? {
              minify: {
                removeComments: true,
                collapseWhitespace: true,
                removeRedundantAttributes: true,
                useShortDoctype: true,
                removeEmptyAttributes: true,
                removeStyleLinkTypeAttributes: true,
                keepClosingSlash: true,
                minifyJS: true,
                minifyCSS: true,
                minifyURLs: true,
              },
            }
          : undefined
      )
    )
  );
});

And, also, you need to include this array in section plugins below (i used spread operator):

<pre class="lang:js" decode:true="">plugins: [
      ...htmlPlugins,

Also, you need to fix ManifestPlugin instance creating. In plugins section too:

[]

/ Generate an asset manifest file with the following content:
      // - files key: Mapping of all asset filenames to their corresponding
      //   output file so that tools can pick it up without having to parse
      //   `index.html`
      // - entrypoints key: Array of files which are included in `index.html`,
      //   can be used to reconstruct the HTML if necessary
      new ManifestPlugin({
        fileName: 'asset-manifest.json',
        publicPath: publicPath,
        generate: (seed, files, entrypoints) => {
          const manifestFiles = files.reduce((manifest, file) => {
            manifest[file.name] = file.path;
            return manifest;
          }, seed);

          // techcode
          let entrypointFiles = [];

          for (let [entryFile, fileName] of Object.entries(entrypoints)) {
            let notMapFiles = fileName.filter(fileName => !fileName.endsWith('.map'));
            entrypointFiles = entrypointFiles.concat(notMapFiles);
          };

          return {
            files: manifestFiles,
            entrypoints: entrypointFiles,
          };
        },
      }),

And last but not least: add new entries to section (don't forget to replace entry: entries as shown on screen):

let entries = {};

paths.appPages.forEach((appPage) => {
  entries[appPage.name] = [
    appPage.appIndexJs,
    isEnvDevelopment && require.resolve('react-dev-utils/webpackHotDevClient'),
  ].filter(Boolean);
});

scripts/build.js

I've changed part of code where file exist checking:

// techcode

paths.appPages.forEach((appPage) => {
  if (!checkRequiredFiles([appPage.appHtml, appPage.appIndexJs])) {
    process.exit(1);
  }
});

New copyPublicFolder function:

function copyPublicFolder() {
  paths.appPages.forEach((appPage) => {
    fs.copySync(paths.appPublic, paths.appBuild, {
      dereference: true,
      filter: (file) => file !== paths.appHtml,
    });
  });
}

scripts/start.js

New function for file check:
paths.appPages.forEach((appPage) => {
  if (!checkRequiredFiles([appPage.appHtml, appPage.appIndexJs])) {
    process.exit(1);
  }
});

That's all! You're awesome!