How to split dynamically by directories with Webpack/SplitChunks plugin? - reactjs

I'm trying to split my React code (created with create-react-app) with the splitChunks plugin in the following way :
I have following components (JSX) structure :
services
serviceA
ComponentA1
ComponentA2
subFolder
ComponentA3
...
serviceB
ComponentB1
ComponentB2
...
serviceC
ComponentB1
ComponentB2
...
...
and I want to have following output (build) :
static/js
serviceA
serviceA.bundle.chunkhash.js
serviceB
serviceB.bundle.chunkhash.js
serviceC
serviceC.bundle.chunkhash.js
(other runtimes / mains are at the root of /static/js)
Another restriction is that components are loaded dynamically with
const Component = lazy(() => import(componentPath));
...
<Suspense fallback={..}>Component</suspense>
"componentPath" is determined on the fly (when a user clicks on an icon then it opens a given service).
The reason for this is that I want to include each bundle into a separate Docker image running the backend. Then each Docker image is reachable thanks to Application routing :
static/js/serviceA/ ==> js served by Docker container running service A
static/js/serviceB/ ==> js served by Docker container running service B
static/js/serviceC/ ==> js served by Docker container running service C
So far, I'v tried to:
set the output.chunkFilename to [name]/[name].[chunkhash].js
use the webpackChunkName with [name] and [request]:
[name] doesn't seem to work (got just litterally "[name]" as part of my directory name).
[request] flattens the name of the directories:
serviceA-ComponentA1
serviceA-ComponentA2
serviceA-subFolder-ComponentA3
serviceB-componentB1
serviceB-componentB2
...
Then I tried to use the splitChunks plugin with following :
splitChunks: {
chunks: 'all',
name: function(module) {
let serviceName = module.rawRequest ? module.rawRequest : 'default';
serviceName = serviceName.replace('../', '').replace('./', '');
serviceName = serviceName.split('/')[0];
return serviceName;
},
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/,
priority: -10
},
default: {
minChunks: 2,
priority: -20,
reuseExistingChunk: true
},
serviceA: {
test: /[\\/]serviceA[\\/]/,
priority: -10
},
serviceB: {
test: /[\\/]serviceB[\\/]/,
priority: -10
},
serviceC: {
test: /[\\/]serviceC[\\/]/,
priority: -10
},
}
},
This approach looks like working as all my services are in their own directories. But I still have some additional directories as numbers (bundle ID probably) that I would have expect to be rather included into the default.
So the question is : is my approach correct ?

I'm not sure if the following option would work for you. I had a similar problem, where I needed different folders to be outputed on different bundles.
In my case, I started with the glob solution, suggested here.
Then, knowing that I needed an array of inputs for each desired output, I came up with this:
const path = require('path');
const glob = require('glob');
const plugins = [...];
module.exports = {
entry: glob.sync('./src/**/*.js').reduce((acc, item) => {
const path = item.split('/');
path.pop();
const rootFolder = path[2] ? `${path[0]}/${path[2]}` : path[0];
if (acc[rootFolder]) {
acc[rootFolder].push(item);
} else {
acc[rootFolder] = [item];
}
return acc;
}, {}),
output: {
filename: '[name]/main.js',
path: path.resolve(__dirname, 'dist'),
},
module: { ... },
plugins,
};
This is a simplified version of my config and it could probably be improved, but it works fine for my needs. :)
More info on glob library: https://github.com/isaacs/node-glob

Related

Gatsby - ERROR #98123 WEBPACK I cant build my app

So I couldn't build my gatsby app because it has quite a lot of dependencies on window. Many components are built depending on the width of the browser window.
After the "gatsby build" command I got WebpackError: ReferenceError: window is not defined.
I found a solution on the internet to paste the following code into gatsby-node.js:
exports.onCreateWebpackConfig = ({ stage, loaders, actions }) => {
if (stage === 'build-html' || stage === 'develop-html') {
actions.setWebpackConfig({
module: {
rules: [
{
test: /node_modules/,
use: loaders.null(),
},
],
},
});
}
};
but when rebuilding the app I get this error:
<w> [webpack.cache.PackFileCacheStrategy] Skipped not serializable cache item 'Compilation/modules|json|C:\Users\Damian\Documents\Ossolinsky\app\node_modules\null-loader\dist\cjs.js??ruleSet[1].rules[13].use!C:\Users\Damian\Documents\Ossolinsky\app\node_modules\gatsby\package.json': No serializer registered for JSONParseError
ERROR #98123 WEBPACK
Generating SSR bundle failed
Unexpected end of JSON input while parsing empty string
File: node_modules\gatsby\package.json
not finished Building HTML renderer - 1.257s
I found a solution on the internet to paste the following code into
gatsby-node.js
This is the recipe of the devil. With the following snippet:
{
test: /node_modules/,
use: loaders.null(),
},
You are adding a null loader to all node_modules folder and the idea here is to add only instances that need to be ignored in the server because they had a window dependency. Since rules is an array you can do:
exports.onCreateWebpackConfig = ({ stage, loaders, actions }) => {
if (stage === 'build-html' || stage === 'develop-html') {
actions.setWebpackConfig({
module: {
rules: [
{
test: /some_package_1/,
use: loaders.null(),
},
{
test: /some_package_2/,
use: loaders.null(),
},
{
test: /some_package_3/,
use: loaders.null(),
},
],
},
});
}
};
Keep in mind that /some_package_3/ is a regular expression (hence the test key and that's why is between slashes) that needs to match the folder name inside node_modules.
To know which string should be placed as a regular expression, normally if you import { some_package_1 } from 'some_package_1' means that some_package_1 is the folder inside node_modules that needs to be added.
There's a chance that some_package_1 is not using a window per se, and it's a dependency of the dependency: in that case, you need to find out which one is it and apply the same reasoning ignoring it.
Many components are built depending on the width of the browser
window.
If those are internal components, you should check first if there's a window available when the component is being rendered/parsed, as the docs suggests, with the following condition:
export default function MyComponent() {
if (typeof window !== "undefined") {
return <div>Component that can use window and will bypass SSR</div>
}
return <div>Component that can't use window</div>
}

Webpack obfuscator not working with craco, maps disabled

today I have a very large problem using react & craco, I can't seem to get my webpack-obfuscator to do anything. I have disabled source maps, but to no avail.
This is my craco config:
const path = require("path");
const WebpackObfuscator = require('webpack-obfuscator');
module.exports = {
webpack: {
configure: (webpackConfig) => {
// Because CEF has issues with loading source maps properly atm,
// lets use the best we can get in line with `eval-source-map`
if (webpackConfig.mode === 'development' && process.env.IN_GAME_DEV) {
webpackConfig.devtool = 'eval-source-map'
webpackConfig.output.path = path.join(__dirname, 'build')
}
return webpackConfig
},
plugins: {
add: [
new WebpackObfuscator ({
rotateStringArray: true
}),
],
},
},
devServer: (devServerConfig) => {
if (process.env.IN_GAME_DEV) {
// Used for in-game dev mode
devServerConfig.writeToDisk = true
}
return devServerConfig
}
}
I get no visible maps files when building, and I've put "GENERATE_SOURCEMAP=false" in my .env file that's located where the package.json is.
Hopefully someone has the answer as to why this is happening.
Kind regards, and thanks for reading.
To upgrade a short config, you can use a construct that, if the condition is met, updates the configuration without using WebpackObfuscator:
module.exports = {
webpack: {
configure: {
...(process.env.IN_GAME_DEV && process.env.NODE_ENV === 'development' && {devtool: 'eval-source-map'})
}
}
}
Also, if you need additional properties for the configuration, in addition to the dvttool, you can add them

Ejected React app stuck on “Starting the development server” after changing entry config

I need to support multiple entry points in the React App. One of the option to do that is via eject: npm run eject.
Here is an original source I use How to Add Multiple Entry Points to Your React App
Create a new application and eject it:
$ npx create-react-app react-hello
$ cd react-hello
$ npm run eject
Then add into App.js:
import React from "react";
And start application to make sure it works with default configuration:
$ npm start
To be able to have multiple entry points, I have to adopt the configuration of entry in the config/webpack.config.js. Original configuration looks like (without comments):
entry: [
isEnvDevelopment &&
require.resolve('react-dev-utils/webpackHotDevClient'),
paths.appIndexJs,
].filter(Boolean),
To support multiple entries, I have to convert it into the associative array/hash table, in this case I use index as a key for the default index page:
entry: {
index: [
isEnvDevelopment && require.resolve('react-dev-utils/webpackHotDevClient'),
paths.appIndexJs
].filter(Boolean)
},
And as soon as I change the format, the npm start stucks and Starting the development server... lasts forever. I try to build it and get an error:
$ npm run build
...
> node scripts/build.js
Creating an optimized production build...
Failed to compile.
Cannot read property 'filter' of undefined
Note: I haven't added a new entry yet, I only modify the current entry to be able to add another one later.
Webpack Entry Points says my configuration is correct.
What am I doing wrong and miss, could you please help?
I ran into this same issue. For me, the problem came from the "ManifestPlugin" further down. It looks like the initial configuration refers to the default "main" entry point key.
new ManifestPlugin({
fileName: 'asset-manifest.json',
publicPath: paths.publicUrlOrPath,
generate: (seed, files, entrypoints) => {
const manifestFiles = files.reduce((manifest, file) => {
manifest[file.name] = file.path;
return manifest;
}, seed);
// This next line is the problem!!!!
const entrypointFiles = entrypoints.main.filter(
fileName => !fileName.endsWith('.map')
);
return {
files: manifestFiles,
entrypoints: entrypointFiles,
};
},
}),
Because you made the entry point into "index" instead of "main", entrypoints.main is now undefined. If you were to change the above code to entrypoints.index, then the code should work as expected. However, this solution is not very flexible and only includes one entry point. The final solution I came up with was this.
new ManifestPlugin({
fileName: 'asset-manifest.json',
publicPath: paths.publicUrlOrPath,
generate: (seed, files, entrypoints) => {
const manifestFiles = files.reduce((manifest, file) => {
manifest[file.name] = file.path;
return manifest;
}, seed);
const entrypointFiles = Object.values(entrypoints).map(values => {
return values.filter(
fileName => !fileName.endsWith('.map')
)
});
return {
files: manifestFiles,
entrypoints: entrypointFiles,
};
},
}),
Encountered this issue using an ejected create-react-app 5.0.1 as-well.
In addition to the above answer, had to change the webpack output filename from static/js/bundle.js static/js/[name].js
output: {
filename: isEnvProduction
? "static/js/[name].[contenthash:8].js"
: isEnvDevelopment && "static/js/bundle.js
to
output: {
filename: isEnvProduction
? "static/js/[name].[contenthash:8].js"
: isEnvDevelopment && "static/js/[name].js"

Debugging create-react-app Build used for Unpacked Chrome Extension

My current build process is to build the project with create-react app, then load the unpacked build as an extension. From there, I launch it, but when I do, I get an error in my main.[hash].js:formatted file.
Normally, I'd click on that error to go to the line number, drop a debugger, reload, and then figure out what's causing the problem. However, the file seems to not be source-mapped. I can't step through this code:
{
key: "render",
value: function() {
var e = this
, t = this.state
, n = t.chromeVersion,
/// ... props, etc
, v = this.props.aProp,
, u = this.state.aStateVal,
, _ = Object(H.b)(this.state.anotherVal)
return p.a.createElement("div", {
// etc etc
which should instead look something like this:
render() {
const {
chromeVersion,
// other state values
} = this.state;
const error = this.props.aProp
const display = this.state.aStateVal,
let getStuff = aFunction(
this.state.anotherVal
);
return (
<div /...
// etc etc
my webpack.config.js at the base of the project looks like this:
const webpack = require("webpack");
if (!process.env.NODE_ENV) {
process.env.NODE_ENV = "development";
}
module.exports = {
entry: `${__dirname}/src/index.js`,
output: {
path: `${__dirname}/../build`,
filename: "background.js"
},
module: {
loaders: [
{
test: /\.(js|jsx)$/,
loader: "babel-loader",
query: {
babelrc: false,
presets: [require.resolve("babel-preset-react-app")]
}
}
]
},
plugins: [
// there used to be an uglify here, but I've removed it
],
devtool: 'source-map'
};
I run my build with the following commands:
GENERATE_SOURCEMAP=true NODE_ENV=development react-scripts build
&& webpack --config path/to/webpack.config.js
&& node /path/to/prepareManifestForExtension.js // Needed for chrome app
What am I missing to be able to debug my code when running my chrome app? How do I get error messages that point to the source mapped line?

React multiple output files of bundle of single Input file

I am working in React and want to publish my code . I am creating bundle using webpack , since I want the bundle to be divided in three parts , that is , my code should have divided in to three different files so that not only one file get populated too much . I went through the official docs of webpack and other online sites but still not found the solution for this .
Here is a complete configuration webpack.config.js that you can base yours on.
const webpack = require('webpack');
const CommonsChunkPlugin = webpack.optimize.CommonsChunkPlugin;
const WebpackConfig = {
// multiple component entry points
entry: {
AppPart1: './src/AppPart1',
AppPart2: './src/AppPart2',
AppPart3: './src/AppPart3'
},
// generate files
output: {
path: './assets',
// creates JS files like "AppPart1.js"
filename: '[name].js'
},
module: {
preLoaders: [
// add any pre-loaders here, OPTIONAL
],
loaders: [
// add any loaders here, like for ES6+ transpiling, React JSX etc
]
},
resolve: {
extensions: ['.jsx', '.js']
},
plugins: [
// this will factor out some common code into `bundle.js`
new CommonsChunkPlugin('bundle.js'),
]
};
module.exports = WebpackConfig;
At the end of webpack build, here is what you will have in the assets folder
AppPart1.js
AppPart2.js
AppPart3.js
bundle.js
bundle.js will contain some shared code and must be included on all your pages along with the appropriate part file for the page.
If you have the three separate files, you can put multiple entry points on your webpack config:
entry: {
"bundle1":"./src/app/somefile1.jsx",
"bundle2":"./src/app/somefile2.jsx,
"bundle3":"./src/app/somefile2.jsx"
}

Resources