I'm trying to build a React app with 2 entry points, one for the App and one for the Admin panel.
I'm starting with Create React App V2 and following this gitHub issue thread https://github.com/facebook/create-react-app/issues/1084 and this tutorial http://imshuai.com/create-react-app-multiple-entry-points/.
I'm trying to port the instructions for adding multiple entry points from CRA V1 to work in V2 but I think I am missing something.
After ejecting CRA, these are the paths I've changed/added to paths.js:
module.exports = {
appBuild: resolveApp('build/app'),
appPublic: resolveApp('public/app'),
appHtml: resolveApp('public/app/index.html'),
appIndexJs: resolveModule(resolveApp, 'src/app'),
appSrc: resolveApp('src'),
adminIndexJs: resolveModule(resolveApp, 'src/admin'),
adminSrc: resolveApp('src'),
adminPublic: resolveApp('public/admin'),
adminHtml: resolveApp('public/admin/index.html'),
};
I've added these entry points to webpack:
entry: {
app: [
isEnvDevelopment &&
require.resolve('react-dev-utils/webpackHotDevClient'),
paths.appIndexJs,
].filter(Boolean),
admin: [
isEnvDevelopment &&
require.resolve('react-dev-utils/webpackHotDevClient'),
paths.adminIndexJs,
].filter(Boolean)
},
output: {
path: isEnvProduction ? paths.appBuild : undefined,
pathinfo: isEnvDevelopment,
filename: isEnvProduction
? 'static/js/[name].[contenthash:8].js'
: isEnvDevelopment && 'static/js/bundle.js',
chunkFilename: isEnvProduction
? 'static/js/[name].[contenthash:8].chunk.js'
: isEnvDevelopment && 'static/js/[name].chunk.js',
publicPath: publicPath,
devtoolModuleFilenameTemplate: isEnvProduction
? info =>
path
.relative(paths.appSrc, info.absoluteResourcePath)
.replace(/\\/g, '/')
: isEnvDevelopment &&
(info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
},
I've modified HtmlWebpackPlugin like so:
new HtmlWebpackPlugin(
Object.assign(
{},
{
inject: true,
template: paths.appHtml,
filename: paths.appPublic,
},
isEnvProduction
? {
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
}
: undefined
)
),
new HtmlWebpackPlugin(
Object.assign(
{},
{
inject: true,
template: paths.adminHtml,
filename: paths.adminPublic,
},
isEnvProduction
? {
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
}
: undefined
)
),
And modified webpack Dev Server:
historyApiFallback: {
disableDotRule: true,
rewrites: [
{ from: /^\/admin.html/, to: '/build/admin/index.html' },
]
},
My file structure is like follows:
.
+-- _src
| +-- app.js
| +-- admin.js
| +-- _app
| +-- App.js
| +-- _admin
| +-- App.js
| +-- _shared
| +-- serviceWorker.js
+-- _public
| +-- _app
| +-- index.html
| +-- manifest.json
| +-- _admin
| +-- index.html
| +-- manifest.json
I would like my build folder to contain an app folder and an admin folder with the 2 separate SPA's in them.
When I run yarn start
it doesn't throw any errors and says Compiled successfully!
however its only partially compiled the app and not the admin app, no js has been compiled or added to the app either.
yarn build
does throw an error and a half compiled app, no admin app. This is the error:
yarn run v1.12.3
$ node scripts/build.js
Creating an optimized production build...
Failed to compile.
EISDIR: illegal operation on a directory, open
'foo/bar/public/app'
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.
In the build folder it had created much this before it exit:
.
+-- _static
| +-- _css
| +-- _js
| +-- _media
| +-- logo.5d5d9eef.svg
+-- precache-manifest.a9c066d088142837bfe429bd3779ebfa.js
+-- service-worker.js
+-- asset-manifest.json
+-- manifest.json
Does anyone know what I am missing out to make this work correctly?
I realised that setting filename
in HTMLWebpackPlugin
to appPublic
or adminPublic
was incorrect and it should be app/index.html
admin/index.html
.
However where I would like 2 separate folders in the build folder, one for the app and the other for the admin app, using this method requires more complexity because there is no entry variable in webpack that you can use to set the destination path. For example I would need to be able to do something like [entry]/static/js/[name].[contenthash:8].chunk.js
. I think one way to do this would be to use Webpack MultiCompiler.
However rather than doing this this I've passed the entry point as an environment variable in package.json, adding REACT_APP_ENTRY=
like so:
"scripts": {
"start-app": "REACT_APP_ENTRY=app node scripts/start.js",
"build-app": "REACT_APP_ENTRY=app node scripts/build.js",
"start-admin": "REACT_APP_ENTRY=admin node scripts/start.js",
"build-admin": "REACT_APP_ENTRY=admin node scripts/build.js",
"test": "node scripts/test.js"
},
In start.js I added const isApp = process.env.REACT_APP_ENTRY === 'app';
at the top:
'use strict';
process.env.BABEL_ENV = 'development';
process.env.NODE_ENV = 'development';
const isApp = process.env.REACT_APP_ENTRY === 'app';
And updated where the port is being set, this is so I can run both development servers at the same time without a clash:
const DEFAULT_PORT = parseInt(process.env.PORT, 10) || (isApp ? 3000 : 3001);
const HOST = process.env.HOST || '0.0.0.0';
Then at the top of paths.js add const isApp = process.env.REACT_APP_ENTRY === 'app';
:
const envPublicUrl = process.env.PUBLIC_URL;
const isApp = process.env.REACT_APP_ENTRY === 'app';
And finally update the paths depending on the env variable set:
module.exports = {
dotenv: resolveApp('.env'),
appPath: resolveApp('.'),
appBuild: isApp ? resolveApp('build/app') : resolveApp('build/admin'),
appPublic: isApp ? resolveApp('public/app') : resolveApp('public/admin'),
appHtml: isApp ? resolveApp('public/app/index.html') : resolveApp('public/admin/index.html'),
appIndexJs: isApp ? resolveModule(resolveApp, 'src/app') : resolveModule(resolveApp, 'src/admin'),
appPackageJson: resolveApp('package.json'),
appSrc: resolveApp('src'),
appTsConfig: resolveApp('tsconfig.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')),
};
I think this method as well as being far simpler is superior for this use case as it allows the flexibility to compile only the app or only the admin rather than forcing you to compile both when only one has been changed. I can run both yarn start-app
and yarn start-admin
at the same time with the separate apps running on different ports.
I know it's a delayed answer, but just for future searches, the steps are:
yarn eject
)appAdminHtml: resolveApp('public/admin.html'),
webpack.config.js
to include one entry per entry point.entry: {
index: [
isEnvDevelopment &&
require.resolve('react-dev-utils/webpackHotDevClient'),
paths.appIndexJs,
].filter(Boolean),
admin: [
isEnvDevelopment &&
require.resolve('react-dev-utils/webpackHotDevClient'),
paths.appSrc + '/admin/index.js',
].filter(Boolean)
},
webpack.config.js
)output: {
path: isEnvProduction ? paths.appBuild : undefined,
pathinfo: isEnvDevelopment,
// This is the important entry
filename: isEnvProduction
? 'static/js/[name].[contenthash:8].js'
: isEnvDevelopment && 'static/js/[name].bundle.js',
futureEmitAssets: true,
chunkFilename: isEnvProduction
? 'static/js/[name].[contenthash:8].chunk.js'
: isEnvDevelopment && 'static/js/[name].chunk.js',
publicPath: publicPath,
devtoolModuleFilenameTemplate: isEnvProduction
? info =>
path
.relative(paths.appSrc, info.absoluteResourcePath)
.replace(/\\/g, '/')
: isEnvDevelopment &&
(info => path.resolve(info.absoluteResourcePath).replace(/\\/g, '/')),
jsonpFunction: `webpackJsonp${appPackageJson.name}`,
globalObject: 'this',
},
webpack.config.js
).// Generates an `index.html` file with the <script> injected.
new HtmlWebpackPlugin(
Object.assign(
{},
{
inject: true,
chunks: ['index'],
template: paths.appHtml,
filename: 'index.html'
},
isEnvProduction
? {
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
}
: undefined
)
),
// Generates an `admin.html` file with the <script> injected.
new HtmlWebpackPlugin(
Object.assign(
{},
{
inject: true,
chunks: ['admin'],
template: paths.appAdminHtml,
filename: 'admin.html',
},
isEnvProduction
? {
minify: {
removeComments: true,
collapseWhitespace: true,
removeRedundantAttributes: true,
useShortDoctype: true,
removeEmptyAttributes: true,
removeStyleLinkTypeAttributes: true,
keepClosingSlash: true,
minifyJS: true,
minifyCSS: true,
minifyURLs: true,
},
}
: undefined
)
),
ManifestPlugin configuration to include the new entry point (also inside
webpack.config.js`):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);
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,
};
},
}),
webpackDevServer.config.js
. historyApiFallback: {
disableDotRule: true,
verbose: true,
rewrites: [
{ from: /^\/admin/, to: '/admin.html' },
]
},
Since Prod server settings can be quite different, I'll let you figure it out.
This post describes everything in more detail.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With