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

paths.appPages.forEach((appPage) => {
if (!checkRequiredFiles([appPage.appHtml, appPage.appIndexJs])) {
process.exit(1);
}
});
That's all! You're awesome!