I am trying to make a React app themeable. For now themes only consist of different sets of Sass variables which define different header colors, etc.
From my current understanding the ExtractTextPlugin seems to be my best bet as I don't want my styles to be inlined and rather have separate files per theme.
So I created two themes:
src/scss/themes/theme-a.scss
src/scss/themes/theme-b.scss
The themes import the basic layout and common styles and override the relevant variables.
But the only way I managed to make webpack create separate css files
for both themes was to create distinct entry points for each theme in
my webpack.prod.config:
entry: {
app: './src/js/init.js',
theme-a: './src/scss/themes/theme-a.scss',
theme-b: './src/scss/themes/theme-b.scss'
},
But adding a new entry point for every new theme that gets added feels wrong and I figure there must be a better way?
Here's your solution:
npm i --save-dev file-loader
In the loaders section, add this:
{
test: /themes\/.+\.scss$/,
loader: "file-loader?name=./compiled-themes/css/[name].css!css!sass"
},
There may be more scss files, if so there must be another section which bundles them as usual, but skips the themes:
{
test: /\.scss$/,
exclude: /themes\/.+\.scss$/,
loader: "css!sass"
},
The file loader writes files by filename, hash and extension so you are able to preserve the name.
Note the name=./compiled-themes/css/[name].css part, where [] vars are substituted.
https://github.com/webpack/file-loader
To avoid having to manually add the themes one possible solution is to create a function that reads contents of the themes folder and programatically add an entry for each *.scss file.
Something along the lines of this:
function getThemes(themePath) {
var themes = {};
fs.readdirSync(themePath).forEach(function(fileName) {
var fileNameWithPath = path.join(themePath, fileName);
var stat = fs.lstatSync(fileNameWithPath);
if (stat.isDirectory()) return;
if (!/\.scss$/.test(fileName)) return;
var nameWithoutExt = path.basename(fileName, '.scss');
themes[nameWithoutExt] = fileNameWithPath;
});
return themes;
}
var themes = getThemes('./src/scss/themes');
var config= {
entry: _.merge({ app: './src/js/init.js' }, themes),
// rest of options
};
This will require you to restart your dev-server or re-run you webpack build when adding new theme files though, but at least you won't have to touch your webpack config.
Related
Does anyone know how to change the names of the build's generated JS files, without using webpack? ex: every time you have a build change the filenames: 'bundle_v1.js', 'chunk_v2.js' (It doesn't necessarily need to be standardized).
Context: every time we publish our project through Azure, whether in development or production environment, we need to ctrl+f5 to clear the cache and show the changes.
I've already tried to add some configs in the config-overrides.js file but I wasn't successful.
Myconfig-overrides.js:
const {
addBabelPlugin,
override,
} = require('customize-cra');
const path = require('path');
module.exports = override(
addBabelPlugin([
'babel-plugin-root-import',
{
rootPathPrefix: '#/',
rootPathSuffix: 'src',
},
]),
);
I've created a React App using create-react-app and have ejected it to get more control over the build process and to turn it into a Browser Extension.
I've got the build to work correctly if there's only one input file, but now as I'm trying to create 2 extra files (which need to be separate from the main as they'll be background files), I'm getting errors while building with no idea why it's happening. I just need to have 1 file with an html page with javascript (react) and the other 2 I just need to be javascript files for the extension functions.
This is the original entry point in the Webpack file:
entry: [
// Include an alternative client for WebpackDevServer. A client's job is to
// connect to WebpackDevServer by a socket and get notified about changes.
// When you save a file, the client will either apply hot updates (in case
// of CSS changes), or refresh the page (in case of JS changes). When you
// make a syntax error, this client will display a syntax error overlay.
// Note: instead of the default WebpackDevServer client, we use a custom one
// to bring better experience for Create React App users. You can replace
// the line below with these two lines if you prefer the stock client:
// require.resolve('webpack-dev-server/client') + '?/',
// require.resolve('webpack/hot/dev-server'),
isEnvDevelopment &&
require.resolve("react-dev-utils/webpackHotDevClient"),
// Finally, this is your app's code:
paths.appIndexJs
// We include the app code last so that if there is a runtime error during
// initialization, it doesn't blow up the WebpackDevServer client, and
// changing JS code would still trigger a refresh.
].filter(Boolean),
So that the system can spit out separate javascript files, I added in 2 more entrypoints like so:
entry: {
app: [
isEnvDevelopment &&
require.resolve("react-dev-utils/webpackHotDevClient"),
paths.appIndexJs
].filter(Boolean),
background: [
isEnvDevelopment &&
require.resolve("react-dev-utils/webpackHotDevClient"),
paths.backgroundIndexJs
].filter(Boolean),
content: [
isEnvDevelopment &&
require.resolve("react-dev-utils/webpackHotDevClient"),
paths.contentIndexJs
].filter(Boolean)
},
The appIndex- backgroundIndex, and contentIndex are all defined in the path file and pointing to valid directories.
When trying to run npm run build on this, I keep getting the error:
Cannot read property 'filter' of undefined
and cannot figure out what is going on with this.
I can't find out anything regarding why this entry point requires the ".filter(Boolean)" or where I can turn it off so it's not required when building.
I thought maybe I could build like this:
entry: {
app: paths.appIndexJs,
background: paths.backgroundIndexJs,
content: paths.contentIndexJs
},
but this also returns the same error with canot read property 'filter' of undefined.
Out of curiosity I tried the following which I understand merges the files together:
entry: [
paths.appIndexJs,
paths.backgroundIndexJs,
paths.contentIndexJs
].filter(Boolean),
And the project builds but even then, I don't see any of the functions from the 2 different files show up.
I've created React Apps from scratch and I know that the multiple entry points method works to generate multiple files, but when the project was created using the create-react-app command and ejected, I cannot get the entry point to work as expected.
I think the problem here is not how the entry is being set, but the errors that show when trying to build without the "filter(Boolean)" set on the entry line.
The only difference I can see from the "usual" and the ejected create-react-app is that the "usual" way for the webpack looks like this:
// This has worked in the past when making configs from scratch
module.exports = {
entry: {
app: paths.appIndexJs
background: paths.backgroundIndexJs
paths.contentIndexJs
},
and ejected create-react-app looks like this:
// this is auto generated and unsure if is the reason why it fails to
module.exports = function(webpackEnv) {
const isEnvDevelopment = webpackEnv === "development";
const isEnvProduction = webpackEnv === "production";
......
return {
entry: {
app: [paths.appIndexJs]
background: [paths.backgroundIndexJs],
content: [paths.contentIndexJs]
},
.....
};
};
Unsure if this has any effect on the entry points but I can't see anything else that could be relevant to this.
What is the error message filter(Boolean) and how can I get around this?
found the solution here from gitmemory
The problem was in the ManifestPlugin configuration from CRA. It assumes that the entry point will be called main, the default from Webpack:
// 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);
const entrypointFiles = entrypoints.main.filter( // <--- This line here
fileName => !fileName.endsWith('.map')
);
return {
files: manifestFiles,
entrypoints: entrypointFiles,
};
},
}),
fix was
...
const entrypointFiles = {};
Object.keys(entrypoints).forEach(entrypoint => {
entrypointFiles[entrypoint] = entrypoints[entrypoint].filter(fileName => !fileName.endsWith('.map'));
});
...
Motivation
I am maintaining an app that is white-labelled for numerous separate brands, which vary mainly in style but also sometimes in core UX. The current (Backbone) solution involves keeping shared code in a separate repo and then building the separate apps with Grunt, with much of the style code and some view overrides for each project living in its own folder. We simply run all the grunt tasks one after the other using a shell script. We're going to build new versions of this thing in React going forward and want to minimize duplicate code, which has now become a major problem in the legacy version.
Desired outcome
The React Native packager builds two versions of its app at the same time. It looks at an import statement like import ComponentA from './ComponentA.js' and goes looking for either ComponentA.android.js or ComponentA.ios.js first, then falls back to importing ComponentA.js if it doesn't find a platform-specific one. I would like to replicate this behavior in Webpack. So I would like to have a folder that looks like this:
react_clients/src/components
|_ ComponentB.js // import ComponentA from './ComponentA.js';
|_ ComponentA.js
|_ ComponentA.brand1.js
|_ ComponentA.brand2.js
Webpack should build ComponentB.js as follows:
brand1.bundle.js imports from ComponentA.brand1.js
brand2.bundle.js imports from ComponentA.brand2.js
brand3.bundle.js and brand4.bundle.js import from ComponentA.js
This would also apply to styles, ideally with the same naming convention.
If necessary, Webpack could be run separately for each version, either using different webpack.config files or accepting command line arguments. The key thing is to avoid duplicating application code.
Current code
The starting point for Webpack is a freshly-generated and ejected create-react-app project.
PS: Apologies in advance if this turns out to be a duplicate but this has been a very tricky question to research. I suspect the answer will have something to do with an advanced configuration of https://webpack.js.org/configuration/resolve/ but can't figure it out yet.
Alright folks here's what I ended up doing:
Dev
In .env.development I specify a variable for the name of the project I want to do dev on:
REACT_APP_VERSION_NAME=brand1
Then in webpack.config.dev.js I take advantage of module resolution to achieve the behavior described above:
const JS_PROJECT_EXTENSION = `.${process.env.REACT_APP_VERSION_NAME}.js`;
const STYLE_PROJECT_EXTENSION = `.${process.env.REACT_APP_VERSION_NAME}.pcss`;
const extensions = [JS_PROJECT_EXTENSION, '.js', '.json', '.jsx', STYLE_PROJECT_EXTENSION, '.pcss'];
...
module.exports = {
...
extensions,
...
}
Then in the code I can simply do the following:
import ComponentA from './componentA';
import Styles from './styles';
And everything works as expected.
Production
I don't specify REACT_APP_VERSION_NAME in .env.production. Instead the relevant config files export functions, and I iterate over the versions I want to build.
First, I created a separate version of config/paths.js that exports a function instead of a static object:
module.exports = function(projectName) {
return {
...
appBuild: resolveApp('build/' + projectName),
...
};
}
And my webpack.config.prod.js looks like this:
...
const getPaths = require('./paths.prod');
...
module.exports = function(projectName) {
const paths = getPaths(projectName);
const JS_PROJECT_EXTENSION = `.${projectName}.js`;
const STYLE_PROJECT_EXTENSION = `.${projectName}.pcss`;
const extensions = [JS_PROJECT_EXTENSION, '.js', '.json', '.jsx', STYLE_PROJECT_EXTENSION, '.pcss'];
...
return {
...
output: {
...
filename: projectName + '-assets/js/[name].[chunkhash:8].js',
chunkFilename: projectName + '-assets/js/[name].[chunkhash:8].chunk.js',
...
}
... [etc, adding projectName to any output that needs to be built separately]
};
}
Finally, just wrap most of the action in scripts/build.js in a loop:
...
[various imports]
...
process.argv[2].split(' ').forEach(projectName => {
const config = require('../config/webpack.config.prod')(projectName);
const paths = require('../config/paths.prod')(projectName);
...
[rest of build.js as normal]
}
After that it's just a matter of pointing your server at the right files for each version, and running yarn build "brand1 brand2" when you want to build.
Going to accept this answer since it's working for me for now but would love to hear about potential improvements from anyone who comes across it in the future.
I am using create-react-app as starter kit and was having a go at antd component library.
I can't seem to find a way to customize their styles (for branding purpose). Documentation doesn't seem to give clear directions regarding customization.
Docs link: https://ant.design/docs/react/customize-theme
They suggest two ways to do it though :
1.Using theme property . But this only works for antd-init or dva-cli boilerplates and not for create-react-app
2.Overriding Less variables .
Now to make the either of these option work for create-react-app without eject , what are the steps I need to take?
You question is not very well researched I am afraid. At a minimum, read the obvious resurces like the instructions at https://ant.design/docs/react/use-with-create-react-app and older Stackoverflow questions (like How do I use .babelrc to get babel-plugin-import working for antd?) before posting a question.
As it stands, CRA does not support changing the Babel configuration. This is needed to include the antd babel-plugin, which is needed to support importing only the necessary antd modules.
As a result you can only import the whole monolithic antd in a non-ejected CRA app.
if you don't intend to use eject you should use config-overrides.js to specify how you want to modify the default webpack.config.js that is part of react-scripts. Then once you have a config-overrides you can add a less Rule that has an option for modifyVars. Here you can specify any changes to the less variables.
function addLessRule(config, env)
{
const { getLoader, loaderNameMatches } = require("react-app-rewired");
const fileLoader = getLoader(
config.module.rules,
rule => loaderNameMatches(rule, 'file-loader')
);
const lessExtension = /\.less$/;
fileLoader.exclude.push(lessExtension);
const cssRules = getLoader(
config.module.rules,
rule => String(rule.test) === String(/\.css$/)
);
var lessModifyVars = {
'primary-color': '#990000',
'link-color': '#1DA57A',
'border-radius-base': '2px',
'layout-header-background' : '#f0f2f5'
};
var lessLoaders = [
{ loader: 'style-loader'} ,
{ loader: 'css-loader' },
{ loader: 'less-loader', options: {
modifyVars: lessModifyVars
} }
];
lessRule = {
test : lessExtension,
use: lessLoaders,
}
config.module["rules"].push( lessRule );
return config
}
If you want to customize antd without using react eject and don't want to change use forked versions of create-react-app or react scripts then
you can simply create a less file (let's say main.less ) , import antd.less and replace the default variables of ant design in this main.less file.
Now compile this less file using lessc (npm i -g less).
lessc "main.less antd.css" --js
--js is for inline javascript in less
Now simply include these complied css in your app.
take a look at https://medium.com/#aksteps/782c53cbc03b
Using Webpack and building an app in React.
In all my files, I have to include to type:
var React = require('React');
seems like a useless repetition.
Is there something I can add in the config file ofwebpack to import/require React in all my files?
You can use ProvidePlugin to automatically require React when needed.
Just add to your webpack config file:
var webpack = require('webpack');
module.exports = {
// Your configuration stuff.
plugins: [
new webpack.ProvidePlugin({
React: 'react'
})
]
};
Every time React is used in a file it will be auto-required/imported without having to do it explicitly.
You only need to require it once in the entry point. While I don't use react, I only include Angular, or any other library that I use, once. Here's an example of how it might look:
// app.ts (my entry point)
namespace app {
"use strict";
//////////// Conditional Requires for Development /////////////
/* istanbul ignore if: only necessary for development environment */
if (process.env.NODE_ENV === "development") {
require("../src/index.html");
}
//////////// Require CSS /////////////////////////////////////
require("../node_modules/codemirror/lib/codemirror.css");
require("./main.css");
//////////// Require Libraries ///////////////////////////////
var angular: IAngularStatic = require("angular");
require("../node_modules/angular-resource");
require("../node_modules/angular-ui-codemirror");
require("../node_modules/angular-sanitize");
//////////// Initialize Angular //////////////////////////////
angular.module("app", [
"ui.codemirror",
"ngResource",
"ngSanitize"
]);
//////////// Require Application Components //////////////////
require("./components/durian.js");
require("./components/testbox.js");
require("./moreapplicationfiles/");
}
This is one file, which I use as the jumping off point for all required libraries and application files.
Once Webpack has packed all of the files together it will do so in the order that I have listed here into one file, so just put React above all application files that use React and they will have access to all React methods and properties. The same advice goes for Flux, Redux, jQuery, or any other library. Of course, not all libraries play nice with Webpack, but it's rare that one doesn't.
As far as adding to the config file... You can also add in additional entry points which can include your JavaScript libraries, by listing all of the libraries in an array at the "entry." You just have to make sure that it will pack these libraries first, so test that they are in the correct order:
// In your webpack.config.js file
{
entry: [ "./node_modules/react", "./app.js"],
}