I am trying to extract different scss entries to separate css bundle files
My folder structure goes something like:
src
themes
red.scss
blue.scss
index.js
red.scss and blue.scss are importing other (same) .scss files
index.js doesnt import any .scss
i want my output to be:
index.js or bundle.js
blue.css
red.css
i used minicssextractplugin, to do the job, but all i got was:
index.js
blue.js
red.js
Used many tutorials but most of them are compatible with webpack 4.
Used optimization, splitChunks, cacheGroups etc
All kind of different entry cases
My dependencies are:
"devDependencies": {
"#babel/core": "^7.12.10",
"#babel/plugin-proposal-object-rest-spread": "^7.12.1",
"#babel/preset-env": "^7.12.11",
"babel-loader": "^8.2.2",
"babel-polyfill": "^6.26.0",
"clean-webpack-plugin": "^3.0.0",
"css-loader": "^5.0.1",
"file-loader": "^6.2.0",
"mini-css-extract-plugin": "^1.3.3",
"node-sass": "^5.0.0",
"sass-loader": "^10.1.0",
"style-loader": "^2.0.0",
"url-loader": "^4.1.1",
"webpack": "^5.11.0",
"webpack-cli": "^4.2.0"
},
I cant copy my webpack.config.js in here, getting error about format
Here are some parts:
entry: {
red: path.resolve(__dirname, 'src/themes/red/'),
blue: path.resolve(__dirname, 'src/themes/blue/'),
},
//devtool: 'source-map',
//entry: './src/index.js',
//entry: ['./src/index.js', './src/css/index.scss'],
//entry: {
// //default: './src/default.js',
// blue: './src/themes/blue',
// red: './src/themes/red'
//},
//entry: {
// //index: './src/index.js',
// //main: './src/index.js',
// //styles: ['./src/themes/red.scss', './src/themes/default.scss', './src/themes/blue.scss']
// default: ['./src/default.js', './src/themes/default.scss'],
// red: ['./src/red.js', './src/themes/red.scss'],
// blue: ['./src/blue.js', './src/themes/blue.scss'],
//},
output: {
path: path.resolve(__dirname, 'wwwroot/dist'),
filename: '[name].js'
//sourceMapFilename: '[name].js.map'
},
optimization: {
splitChunks: {
cacheGroups: {
redStyles: {
name: 'styles_red',
test: (m, c, entry = 'red') =>
m.constructor.name === 'CssModule' &&
recursiveIssuer(m, c) === entry,
chunks: 'all',
enforce: true,
},
blueStyles: {
name: 'styles_blue',
test: (m, c, entry = 'blue') =>
m.constructor.name === 'CssModule' &&
recursiveIssuer(m, c) === entry,
chunks: 'all',
enforce: true,
},
},
},
},
module: {
rules: [
{
test: /\.s[c|a]ss$/,
include: path.resolve(__dirname, 'src'),
use: [
devMode ? 'style-loader' : MiniCssExtractPlugin.loader,
'css-loader',
'sass-loader',
]
},
//{
// test: /\.js$/,
// include: path.resolve(__dirname, 'src'),
// loader: 'babel-loader',
// options: {
// presets: ["#babel/preset-env"],
// plugins: ['#babel/plugin-proposal-object-rest-spread']
// }
//},
{
test: /\.(png|jpg)$/,
include: path.resolve(__dirname, 'src'),
use: {
loader: "file-loader"
}
}
]
}
plugins: [
new webpack.ProvidePlugin({
$: 'jquery',
jQuery: 'jquery'
}),
//new FixStyleOnlyEntriesPlugin(),
new MiniCssExtractPlugin({
filename: "[name].css",
}),
//new MiniCssExtractPlugin({
// filename: "[name].css",
// //chunkFilename: "[name].css"
//})
//new MiniCssExtractPlugin({
// filename: '[name].css'
//}),
//defaultTheme,
//redTheme,
//blueTheme
]
(copy of my answer)
my case: webpack 5 + multipage application + themes.css via entry points
solution: https://github.com/webdiscus/webpack-remove-empty-scripts
this plugins don't work with webpack 5 entry points or with MiniCssExtractPlugin:
webpack-fix-style-only-entries,
webpack-extraneous-file-cleanup-plugin,
webpack-remove-empty-js-chunks-plugin,
webpack-delete-no-js-entries-plugin.
my webpack.config.js:
const fs = require('fs');
const path = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const RemoveEmptyScriptsPlugin = require('webpack-remove-empty-scripts');
const isProd = process.env.NODE_ENV === 'production';
const isDev = !isProd;
const PAGES = ['app', 'help'];
const getThemes = (themePath, alias) => {
let themes = {};
const longPath = './' + alias + '/' + themePath;
fs.readdirSync(longPath).forEach(function(fileName) {
const fileNameWithPath = path.join(themePath, fileName);
const fileNameWithLongPath = path.join(longPath, fileName);
const stat = fs.lstatSync(fileNameWithLongPath);
if (stat.isDirectory()) return;
if (!/\.scss$/.test(fileName)) return;
const nameWithoutExt = path.basename(fileName, '.scss');
themes[nameWithoutExt] = ['./' + fileNameWithPath];
});
console.log(themes);
return themes;
};
const themes = getThemes('scss/themes', 'src');
const getFilename = (filename, ext) => {
let name = filename == 'index' ? 'bundle' : filename;
const isTheme = (ext == 'css' && name.startsWith('theme')) ? true : false;
const needHash = (isDev || isTheme) ? false : true;
return needHash ? name +`.[fullhash].` + ext : name+'.'+ext;
};
const getCSSDirname = filename => {
const isTheme = filename.startsWith('theme');
return !isTheme? '/css/' : '/css/theme/';
};
const getHTMLWebpackPlugins = arr => {
// this function config multipages names and add to html-pages
// inside <head> tag our themes via tag <link rel="stylesheet" href="....css" ...>
// and return array of HTMLWebpackPlugins
};
module.exports = {
// ... //
entry: {
// mutipage:
app: ['./index.js', './scss/app.scss'],
help: ['./help.js', './scss/help.scss'],
// multitheme:
...themes,
},
optimization: {
removeEmptyChunks: true, // not work!!!
},
// ... //
plugins: [
// ... //
...getHTMLWebpackPlugins(PAGES),
new RemoveEmptyScriptsPlugin({
ignore: PAGES,
enabled: isDev === false,
}),
new MiniCssExtractPlugin({
filename: pathdata => {
return getCSSDirname(pathdata.chunk.name) + getFilename(pathdata.chunk.name, 'css');
},
chunkFilename: isDev ? '[id].css' : '[id].[contenthash].css',
}),
],
};
my src files:
[src]:
- index.js
- index.html
- help.js
- help.html
- [scss]:
- - app.scss
- - help.scss
- - [themes]:
- - - light.scss
- - - dark.scss
- - - blue.scss
after build:
[dist]:
- app.js
- index.html
- help$hash.js
- help$hash.html
- [css]:
- - app$hash.css
- - help$.css
- - [themes]:
- - - light.css
- - - dark.css
- - - blue.css
Related
So after npm build and npm run , why does my react application open with console on the screen? It was not happening when my packages were "webpack-cli": "^4.2.0" and "webpack-dev-server": "^3.11.1". Upgrading them is causing this issue. How can we fix this?
My package.json contains (devDependencies)
"clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^7.0.0",
"webpack": "^5.11.0",
"webpack-bundle-analyzer": "^4.3.0",
"webpack-cli": "^4.9.2",
"webpack-dev-server": "^4.7.3",
"webpack-manifest-plugin": "2.2.0",
"webpack-merge": "^5.7.2"
"eslint-webpack-plugin": "^2.1.0",
"html-webpack-plugin": "5.3.2",
"scripts": {
"start": "webpack serve --config config/webpack.config.js --env TARGET_ENV=development --env app=web --port 3000",
"build": "webpack --config config/webpack.config.js --env TARGET_ENV=production",
"build:staging": "webpack --config config/webpack.config.js --env TARGET_ENV=staging"
}
webpack.config.js
const { merge } = require("webpack-merge");
//const commonConfig = require("./webpack.common.js");
const paths = require("./paths");
const webpack = require("webpack");
const { CleanWebpackPlugin } = require("clean-webpack-plugin");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ESLintPlugin = require("eslint-webpack-plugin");
const getClientEnvironment = require("./env");
module.exports = (env) => {
const targetEnv = env.TARGET_ENV;
const mode = targetEnv === "development" ? "development" : "production";
process.env.NODE_ENV = mode;
// Source maps are resource heavy and can cause out of memory issue for large source files.
// const shouldUseSourceMap = webpackEnv === "development";
const commonConfig = {
mode: mode,
// Where webpack looks to start building the bundle
entry: {
web: paths.src + "/web.tsx",
},
// Where webpack outputs the assets and bundles
resolve: {
extensions: [".tsx", ".ts", ".js"],
fallback: {
util: require.resolve("util/"),
},
modules: ["node_modules", "./src"],
},
// Customize the webpack build process
plugins: [
new webpack.ProvidePlugin({
process: "process/browser",
}),
// Makes some environment variables available to the JS code, for example:
// if (process.env.NODE_ENV === 'production') { ... }. See `./env.js`.
// It is absolutely essential that NODE_ENV is set to production
// during a production build.
// Otherwise React will be compiled in the very slow development mode.
new webpack.DefinePlugin(getClientEnvironment(targetEnv).stringified),
new webpack.IgnorePlugin({
resourceRegExp: /^\.\/locale$/,
contextRegExp: /moment$/,
}),
// Removes/cleans build folders and unused assets when rebuilding
new CleanWebpackPlugin(),
// Copies files from target to destination folder
new CopyWebpackPlugin({
patterns: [
{
from: paths.public,
to: paths.build,
globOptions: {
ignore: ["**/*.html"],
},
},
{
from: paths.appPath + "/web.config",
to: paths.build,
},
],
}),
// Generates an HTML file from a template
// Generates deprecation warning: https://github.com/jantimon/html-webpack-plugin/issues/1501
new HtmlWebpackPlugin({
//favicon: paths.src + "/images/favicon.png",
template: paths.public + "/web.html", // template file
chunks: ["vendor", "web"],
filename: "web.html", // output file
inject: true,
}),
new HtmlWebpackPlugin({
template: paths.public + "/crossplatform.html", // template file
chunks: ["vendor", "crossplatform"],
filename: "crossplatform.html", // output file
inject: true,
}),
new ESLintPlugin({
// Plugin options
extensions: ["js", "jsx", "ts", "tsx"],
}),
],
// Determine how modules within the project are treated
module: {
rules: [
// JavaScript: Use Babel to transpile JavaScript files
{
test: /\.(ts|js)x?$/,
include: paths.src,
//exclude: /node_modules/,
loader: "babel-loader",
options: {
cacheDirectory: true,
// See #6846 for context on why cacheCompression is disabled
cacheCompression: false,
// Babel sourcemaps are needed for debugging into node_modules
// code. Without the options below, debuggers like VSCode
// show incorrect code and set breakpoints on the wrong lines.
//sourceMaps: shouldUseSourceMap,
//inputSourceMap: shouldUseSourceMap,
},
},
// Images: Copy image files to build folder
{ test: /\.(?:ico|gif|png|jpg|jpeg)$/i, type: "asset/resource" },
// Fonts and SVGs: Inline files
{ test: /\.(woff(2)?|eot|ttf|otf|svg|)$/, type: "asset/inline" },
],
},
};
const envConfig = require(`./webpack.${mode}.js`)({ app: env.app });
return merge(commonConfig, envConfig);
};
webpack.development.js
const webpack = require("webpack");
const paths = require("./paths");
module.exports = (args) => {
return {
// Control how source maps are generated
devtool: "cheap-module-source-map",
output: {
//path: paths.build,
publicPath: "./",
filename: "[name].js",
},
// Needed because of an issue with webpack dev server HMR with Webpack https://github.com/webpack/webpack-dev-server/issues/2758
target: "web",
// Spin up a server for quick development
devServer: {
historyApiFallback: {
index: "/" + args.app + ".html",
},
open: true,
devMiddleware: {
publicPath: "/",
},
hot: true,
// By default files from `contentBase` will not trigger a page reload.
// watchContentBase: true,
},
module: {
rules: [
// Styles: Inject CSS into the head with source maps
{
test: /\.(css)$/,
use: [
"style-loader",
{
loader: "css-loader",
//options: { sourceMap: true },
},
],
},
],
},
};
};
My webpack.config.js file is as follows:
var path = require('path');
var webpack = require('webpack');
var CommonsChunkPlugin = webpack.optimize.CommonsChunkPlugin;
var autoprefixer = require('autoprefixer');
var HtmlWebpackPlugin = require('html-webpack-plugin');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var CopyWebpackPlugin = require('copy-webpack-plugin');
var ENV = process.env.npm_lifecycle_event;
var isTest = ENV === 'test' || ENV === 'test-watch';
var isProd = ENV === 'build';
module.exports = function makeWebpackConfig() {
var config = {};
if (isTest) {
config.devtool = 'inline-source-map';
} else if (isProd) {
config.devtool = 'source-map';
} else {
config.devtool = 'eval-source-map';
}
config.debug = !isProd || !isTest;
config.entry = isTest ? {} : {
'vendor': './src/vendor.ts',
'app': './src/bootstrap.ts' // our angular app
};
config.output = isTest ? {} : {
path: root('../edu_analytics_prod_front/dist'),
publicPath: isProd ? '/' : '/',
filename: isProd ? 'js/[name].[hash].js' : 'js/[name].js',
chunkFilename: isProd ? '[id].[hash].chunk.js' : '[id].chunk.js'
};
config.resolve = {
cache: !isTest,
root: root(),
extensions: ['', '.ts', '.js', '.json', '.css', '.scss', '.html'],
alias: {
'app': 'src/client',
'common': 'src/common'
}
};
config.module = {
preLoaders: isTest ? [] : [{test: /\.ts$/, loader: 'tslint'}],
loaders: [
// Support for .ts files.
{
test: /\.ts$/,
loader: 'ts',
query: {
'ignoreDiagnostics': [
2403, // 2403 -> Subsequent variable declarations
2300, // 2300 -> Duplicate identifier
2374, // 2374 -> Duplicate number index signature
2375, // 2375 -> Duplicate string index signature
2502 // 2502 -> Referenced directly or indirectly
]
},
exclude: [isTest ? /\.(e2e)\.ts$/ : /\.(spec|e2e)\.ts$/, /node_modules\/(?!(ng2-.+))/]
},
{test: /\.(png|jpe?g|gif|svg|woff|woff2|ttf|eot|ico)$/, loader: 'file?name=fonts/[name].[hash].[ext]?'},
{test: /\.json$/, loader: 'json'},
{
test: /\.css$/,
exclude: root('src', 'client'),
loader: isTest ? 'null' : ExtractTextPlugin.extract('style', 'css?sourceMap!postcss')
},
// all css required in src/client files will be merged in js files
{test: /\.css$/, include: root('src', 'client'), loader: 'raw!postcss'},
{
test: /\.scss$/,
exclude: root('src', 'client'),
loader: isTest ? 'null' : ExtractTextPlugin.extract('style', 'css?sourceMap!postcss!sass')
},
{test: /\.scss$/, exclude: root('src', 'style'), loader: 'raw!postcss!sass'},
{test: /\.html$/, loader: 'raw'}
],
postLoaders: [],
noParse: [/.+angular2\/bundles\/.+/]
};
if (isTest) {
config.module.postLoaders.push({
test: /\.(js|ts)$/,
include: path.resolve('src'),
loader: 'istanbul-instrumenter-loader',
exclude: [/\.spec\.ts$/, /\.e2e\.ts$/, /node_modules/]
})
}
config.plugins = [
new webpack.DefinePlugin({
'process.env': {
ENV: JSON.stringify(ENV)
}
})
];
if (!isTest) {
config.plugins.push(
new CommonsChunkPlugin({
name: ['vendor', 'polyfills']
}),
new HtmlWebpackPlugin({
template: './src/public/index.html',
inject: 'body',
chunksSortMode: packageSort(['polyfills', 'vendor', 'app'])
}),
new webpack.optimize.UglifyJsPlugin({
compress: { warnings: false }
}),
new ExtractTextPlugin('css/[name].[hash].css', {disable: !isProd})
);
}
if (isProd) {
config.plugins.push(
new webpack.NoErrorsPlugin(),
new webpack.optimize.DedupePlugin(),
new webpack.optimize.UglifyJsPlugin({
compress: { warnings: false }
}),
new CopyWebpackPlugin([{
from: root('src/public')
}])
);
}
config.postcss = [
autoprefixer({
browsers: ['last 2 version']
})
];
config.sassLoader = {
//includePaths: [path.resolve(__dirname, "node_modules/foundation-sites/scss")]
};
config.tslint = {
emitErrors: false,
failOnHint: false
};
config.devServer = {
contentBase: './src/public',
historyApiFallback: true,
stats: 'minimal' // none (or false), errors-only, minimal, normal (or true) and verbose
};
return config;
}();
function root(args) {
args = Array.prototype.slice.call(arguments, 0);
return path.join.apply(path, [__dirname].concat(args));
}
function rootNode(args) {
args = Array.prototype.slice.call(arguments, 0);
return root.apply(path, ['node_modules'].concat(args));
}
function packageSort(packages) {
var len = packages.length - 1;
var first = packages[0];
var last = packages[len];
return function sort(a, b) {
if (a.names[0] === first) {
return -1;
}
if (a.names[0] === last) {
return 1;
}
if (a.names[0] !== first && b.names[0] === last) {
return -1;
} else {
return 1;
}
}
}
When I'm running command like webpack / webpack -p
It is creating three files as follows:
But all three files has output as string and using eval function to deploy it and one file has very big size(7 MB) as follows.
I want simple JavaScript file without eval used inside it as all other common minification library works and I want to reduce size of vendor file as well.
Your config uses this configuration as default:
config.devtool = 'eval-source-map';
The fine manual states:
eval-source-map - Each module is executed with eval and a SourceMap is added as DataUrl to the eval.
If you don't want that, use another devtool option.
As for decreasing code size, you probably want to either disable the creation of a source map entirely (just don't set the devtool option) or have Webpack write the source map to a separate file (devtool : 'source-map' or devtool : 'cheap-source-map', AFAIK).
Also set the NODE_ENV environment variable to production if you want less code:
# if you're on a Unix-like OS:
env NODE_ENV=production webpack -p
I hate to admit it, but I've been spending three long evenings trying to do - what I thought would be straightforward thing to do. I finally reached the stage where I'm fed up with it, and frankly rather frustrated because "it just won't work".
Here is what I try to achieve:
Bundle my Express server with Webpack (although my current code just renders a string in the browser, it is supposed to compile server rendered React components compiled with Babel)
Save the bundle in memory (or on disk if there is no other way)
Run webpack / dev / hot middleware to serve my Node Express app in a way that changes to the server rendered pages (which will be React components) will auto-update in the browser.
I've tried numerous combinations, tutorials that have been deprecated, npm packages that are no longer maintained and downloaded examples that just don't work.
Here is my current setup:
webpack.server.config.js:
const path = require('path');
const webpack = require('webpack');
const nodeExternals = require('webpack-node-externals');
module.exports = {
name: 'server',
mode: 'development',
target: 'node',
externals: nodeExternals(),
entry: [ './src/server/index' ],
output: {
path: path.resolve(__dirname, 'dist'),
// path: "/",
filename: '[name].js',
publicPath: '/assets/',
libraryTarget: 'commonjs2'
},
plugins: [
new webpack.HotModuleReplacementPlugin()
],
module: {
rules: [
{
test: /.js$/,
loader: 'babel-loader',
include: path.resolve(__dirname, 'src/'),
exclude: /node_modules/,
options: {
presets:
[['#babel/preset-env', { modules: 'false' }], '#babel/preset-react'],
plugins: [
['#babel/plugin-proposal-object-rest-spread', { useBuiltIns: true }],
'#babel/plugin-proposal-class-properties'
]
}
},
{
test: /\.scss$/,
loader: 'ignore-loader'
},
{
test: /\.css$/,
loader: 'ignore-loader'
},
{
test: /\.(jpg|png|svg|gif|pdf)$/,
loader: 'file-loader',
options: {
name: '[path][name].[ext]'
}
}
]
}
};
index.js:
import http from 'http';
import fs from "fs";
import express from "express";
import favicon from 'serve-favicon';
// import renderer from "./renderer";
import renderApp from './welcome';
const app = express();
app.use(favicon('./public/favicon.ico'));
app.use(express.static("public"));
if (process.env.NODE_ENV !== 'production') {
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const webpackHotMiddleware = require('webpack-hot-middleware');
const serverConfig = require('../../webpack.server.config');
const compiler = webpack(serverConfig);
app.use(webpackDevMiddleware(compiler, {
stats: {colors: true},
headers: { "Access-Control-Allow-Origin": "http://localhost"},
publicPath: serverConfig.output.publicPath
}));
app.use(require("webpack-hot-middleware")(compiler));
}
app.get("*", function(req, res) {
fs.readFile("./src/server/html/index.html", "utf8", function(err, data) {
const context = {};
const html = renderApp();
//const html = renderer(data, req.path, context);
res.set('content-type', 'text/html');
res.send(html);
res.end();
});
});
const PORT = process.env.PORT || 8080;
app.listen(3000);
Frankly I'm also rather confused about how this is supposed to work.
Are the following steps supposed to be executed?:
webpack webpack.server.config.js --watch
node dist/server.js // webpack output folder
Would this magically hot reload my server?
All help is welcome, or if you happened to have a working demo.
I just couldn't manage to make this work.
In the end I will also hot reload (re-render) my client bundle but I guess that will be the easy part as I've seen many, many resources about that.
Night sleep was probably needed.
I got this working (incl with React server rendered components) using StartServerPlugin.
Following setup hot reloads the Node Express server:
const path = require('path');
const webpack = require('webpack');
const nodeExternals = require('webpack-node-externals');
const StartServerPlugin = require('start-server-webpack-plugin');
module.exports = {
name: 'server',
mode: 'development',
target: 'node',
externals: nodeExternals({
whitelist: ['webpack/hot/poll?1000']
}),
entry: [ 'webpack/hot/poll?1000', './src/server/index' ],
output: {
path: path.resolve(__dirname, 'dist'),
// path: "/",
filename: 'server.js',
publicPath: '/assets/',
libraryTarget: 'commonjs2'
},
plugins: [
new StartServerPlugin({'name': 'server.js', nodeArgs: ['--inspect']}),
new webpack.NamedModulesPlugin(),
new webpack.HotModuleReplacementPlugin(),
new webpack.DefinePlugin({
"process.env": {
"BUILD_TARGET": JSON.stringify('server')
}
})
],
module: {
rules: [
{
test: /.js$/,
loader: 'babel-loader',
include: path.resolve(__dirname, 'src/'),
exclude: /node_modules/,
options: {
presets:
[['#babel/preset-env', { modules: 'false' }], '#babel/preset-react'],
plugins: [
['#babel/plugin-proposal-object-rest-spread', { useBuiltIns: true }],
'#babel/plugin-proposal-class-properties'
]
}
},
{
test: /\.scss$/,
loader: 'ignore-loader'
},
{
test: /\.css$/,
loader: 'ignore-loader'
},
{
test: /\.(jpg|png|svg|gif|pdf)$/,
loader: 'file-loader',
options: {
name: '[path][name].[ext]'
}
}
]
}
};
index.js:
import http from 'http'
import app from './server'
const server = http.createServer(app)
let currentApp = app;
const PORT = process.env.PORT || 8080;
server.listen(PORT);
if (module.hot) {
module.hot.accept('./server', () => {
server.removeListener('request', currentApp);
server.on('request', app);
currentApp = app;
})
}
server.js:
import http from 'http';
import fs from "fs";
import express from "express";
import favicon from 'serve-favicon';
import renderer from "./renderer";
import renderApp from './welcome';
const app = express();
app.use(favicon('./public/favicon.ico'));
app.use(express.static("public"));
app.get("*", function(req, res) {
fs.readFile("./src/server/html/index.html", "utf8", function(err, data) {
const context = {};
//const html = renderApp();
console.log('test');
const html = renderer(data, req.path, context);
res.set('content-type', 'text/html');
res.send(html);
res.end();
});
});
export default app;
Run with:
rm -rf ./dist && webpack --config webpack.server.config.js --watch
I think anwsers here are bit too complicated. It seems that Webpack does not make this easy.
Rather than trying to cook up complicated configurations, I took the issue in my own hand and created a small devserver, that does both the webpack build and server reload upon file changes. The client also has a reload logic, so the page in both cases is auto reloaded.
hot module reload for express server and webpack client
Synopsis
devserver
const fetch = require('node-fetch')
let process
function spawnserver(){
process = require('child_process').spawn("node", ["server/server.js", "dev"])
process.stdout.on('data', (data) => {
console.error(`stdout: ${data}`)
})
process.stderr.on('data', (data) => {
console.error(`stderr: ${data}`)
})
}
function rebuildsrc(){
process = require('child_process').spawn("npm", ["run", "build"])
process.stdout.on('data', (data) => {
console.error(`stdout: ${data}`)
})
process.stderr.on('data', (data) => {
console.error(`stderr: ${data}`)
})
process.on("close", code => {
console.log("build exited with code", code)
fetch("http://localhost:3000/reloadsrc").then(response=>response.text().then(content=>console.log(content)))
})
}
spawnserver()
const watcher = require("chokidar").watch("./server")
watcher.on("ready", _=>{
watcher.on("all", _=>{
console.log("server reload")
process.kill()
spawnserver()
})
})
const srcWatcher = require("chokidar").watch("./src")
srcWatcher.on("ready", _=>{
srcWatcher.on("all", _=>{
console.log("rebuild src")
rebuildsrc()
})
})
client reload
let stamp = new Date().getTime()
let lastStamp = null
app.get('/stamp', (req, res) => {
lastStamp = new Date().getTime()
res.send(`${stamp}`)
})
app.get('/reloadsrc', (req, res) => {
stamp = new Date().getTime()
res.send(`updated stamp to ${stamp}`)
})
let welcomeMessage = "Welcome !!"
let reloadScript = IS_PROD ? ``:`
let stamp = null
let reloadStarted = false
setInterval(_=>{
fetch('/stamp').then(response=>response.text().then(content=>{
if(stamp){
if(content != stamp) setTimeout(_=>{
if(!reloadStarted){
console.log("commence reload")
setInterval(_=>{
fetch(document.location.href).then(response=>response.text().then(content=>{
if(content.match(/Welcome/)){
console.log("reloading")
document.location.reload()
}
}))
}, 1000)
reloadStarted = true
}
}, 500)
}else{
if(!reloadStarted){
stamp = content
console.log("stamp set to", stamp)
}
}
}))
}, 200)
`
app.use('/dist', express.static('dist'))
app.get('/', (req, res) => {
res.send(`
<!doctype html>
<head>
<title>Reload Express Sample App</title>
</head>
<body>
<h1>${welcomeMessage}</h1>
<script>
${reloadScript}
</script>
<script src="dist/bundle.js"></script>
</body>
</html>
`)
})
Node.js, babel and webpack have been the bane of my month. There's several components that should be included. You should have a start file called "package.json"
The contents should look like:
{
"name": "react-complete-guide",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"start": "webpack-dev-server",
"build": "rimraf dist && webpack --config webpack.prod.config.js --progress --profile --color"
},
"author": "",
"license": "ISC",
"devDependencies": {
"autoprefixer": "^7.1.5",
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-plugin-syntax-dynamic-import": "^6.18.0",
"babel-preset-env": "^1.6.0",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-2": "^6.24.1",
"css-loader": "^0.28.7",
"file-loader": "^1.1.5",
"html-webpack-plugin": "^2.30.1",
"postcss-loader": "^2.0.7",
"style-loader": "^0.19.0",
"url-loader": "^0.6.2",
"webpack": "^3.6.0",
"webpack-dev-server": "^2.9.1"
},
"dependencies": {
"react": "^16.0.0",
"react-dom": "^16.0.0",
"react-router-dom": "^4.2.2"
}
}
If you type "npm start", then the bit of code "start": "webpack-dev-server" is run. This will load a preview of the code.
To package the contents into a build you type "npm run build" and that will run the code "build": "rimraf dist && webpack --config webpack.prod.config.js --progress --profile --color". That code will run the "rimraf", which deletes the "dist" folder if it exists, the rest runs the webpack config file.
You should have two webpack files. One for hot reloads and one for packaging for the production environment. The files should be called:
"webpack.config.js" and "webpack.prod.config.js".
The contents of "webpack.config.js" look like this:
const path = require('path');
const autoprefixer = require('autoprefixer');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
devtool: 'cheap-module-eval-source-map',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js',
chunkFilename: '[id].js',
publicPath: ''
},
resolve: {
extensions: ['.js', '.jsx']
},
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.css$/,
exclude: /node_modules/,
use: [
{ loader: 'style-loader' },
{
loader: 'css-loader',
options: {
importLoaders: 1,
modules: true,
localIdentName: '[name]__[local]__[hash:base64:5]'
}
},
{
loader: 'postcss-loader',
options: {
ident: 'postcss',
plugins: () => [
autoprefixer({
browsers: [
"> 1%",
"last 2 versions"
]
})
]
}
}
]
},
{
test: /\.(png|jpe?g|gif)$/,
loader: 'url-loader?limit=8000&name=images/[name].[ext]'
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: __dirname + '/src/index.html',
filename: 'index.html',
inject: 'body'
})
]
};
The contents of "webpack.prod.config.js" look like this:
const path = require('path');
const autoprefixer = require('autoprefixer');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const webpack = require('webpack');
module.exports = {
devtool: 'cheap-module-source-map',
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist/new'),
filename: 'bundle.js',
chunkFilename: '[id].js',
publicPath: ''
},
resolve: {
extensions: ['.js', '.jsx']
},
module: {
rules: [
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.css$/,
exclude: /node_modules/,
use: [
{ loader: 'style-loader' },
{
loader: 'css-loader',
options: {
importLoaders: 1,
modules: true,
localIdentName: '[name]__[local]__[hash:base64:5]'
}
},
{
loader: 'postcss-loader',
options: {
ident: 'postcss',
plugins: () => [
autoprefixer({
browsers: [
"> 1%",
"last 2 versions"
]
})
]
}
}
]
},
{
test: /\.(png|jpe?g|gif)$/,
loader: 'url-loader?limit=8000&name=images/[name].[ext]'
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: __dirname + '/src/index.html',
filename: 'index.html',
inject: 'body'
}),
new webpack.optimize.UglifyJsPlugin()
]
};
You also need a file to tell babel how to act. The file is called ".babelrc" if you are using babel. The contents look like this
{
"presets": [
["env", {
"targets": {
"browsers": [
"> 1%",
"last 2 versions"
]
}
}],
"stage-2",
"react"
],
"plugins": [
"syntax-dynamic-import"
]
}
There's a lot going on in this code. I strongly recomment watching some tutorial videos on this.
I just spent 3 hours putting this line:
exclude: ['./src/assets/sass']
in 20 different places. Please tell me where this should go?
Here is my current setup for the css-loader (util.js):
'use strict'
const path = require('path')
const config = require('../config')
const ExtractTextPlugin = require('extract-text-webpack-plugin')
const packageConfig = require('../package.json')
exports.assetsPath = function (_path) {
const assetsSubDirectory = process.env.NODE_ENV === 'production'
? config.build.assetsSubDirectory
: config.dev.assetsSubDirectory
return path.posix.join(assetsSubDirectory, _path)
}
exports.cssLoaders = function (options) {
options = options || {}
const cssLoader = {
loader: 'css-loader',
options: {
sourceMap: options.sourceMap
}
}
const postcssLoader = {
loader: 'postcss-loader',
options: {
sourceMap: options.sourceMap
}
}
// generate loader string to be used with extract text plugin
function generateLoaders (loader, loaderOptions) {
const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader]
if (loader) {
loaders.push({
loader: loader + '-loader',
// exclude: ['./src/assets/sass'],
options: Object.assign({}, loaderOptions, {
sourceMap: options.sourceMap
})
})
}
// Extract CSS when that option is specified
// (which is the case during production build)
if (options.extract) {
return ExtractTextPlugin.extract({
use: loaders,
fallback: 'vue-style-loader'
})
} else {
return ['vue-style-loader'].concat(loaders)
}
}
// https://vue-loader.vuejs.org/en/configurations/extract-css.html
return {
css: generateLoaders(),
postcss: generateLoaders(),
less: generateLoaders('less'),
sass: generateLoaders('sass', { indentedSyntax: true }),
scss: generateLoaders('sass'),
stylus: generateLoaders('stylus'),
styl: generateLoaders('stylus')
}
}
// Generate loaders for standalone style files (outside of .vue)
exports.styleLoaders = function (options) {
const output = []
const loaders = exports.cssLoaders(options)
for (const extension in loaders) {
const loader = loaders[extension]
output.push({
test: new RegExp('\\.' + extension + '$'),
exclude: ['./src/assets/sass'],
use: loader
})
}
return output
}
exports.createNotifierCallback = () => {
const notifier = require('node-notifier')
return (severity, errors) => {
if (severity !== 'error') return
const error = errors[0]
const filename = error.file && error.file.split('!').pop()
notifier.notify({
title: packageConfig.name,
message: severity + ': ' + error.name,
subtitle: filename || '',
icon: path.join(__dirname, 'logo.png')
})
}
}
Here is what my base webpack file looks like:
'use strict'
const path = require('path')
const utils = require('./utils')
const config = require('../config')
const vueLoaderConfig = require('./vue-loader.conf')
function resolve (dir) {
return path.join(__dirname, '..', dir)
}
module.exports = {
context: path.resolve(__dirname, '../'),
entry: {
app: ['babel-polyfill','./src/main.js']
},
output: {
path: config.build.assetsRoot,
filename: '[name].js',
publicPath: process.env.NODE_ENV === 'production'
? config.build.assetsPublicPath
: config.dev.assetsPublicPath
},
resolve: {
extensions: ['.js', '.vue', '.json'],
alias: {
'vue$': 'vue/dist/vue.esm.js',
'#': resolve('src'),
}
},
module: {
rules: [
{
test: /\.vue$/,
loader: 'vue-loader',
options: vueLoaderConfig
},
{
test: /\.js$/,
loader: 'babel-loader',
include: [resolve('src'), resolve('test'), resolve('node_modules/webpack-dev-server/client')],
},
// {
// test: /\.scss$/,
// exclude: ['./src/assets/sass']
// },
{
test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('img/[name].[hash:7].[ext]')
}
},
{
test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('media/[name].[hash:7].[ext]')
}
},
{
test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/,
loader: 'url-loader',
options: {
limit: 10000,
name: utils.assetsPath('fonts/[name].[hash:7].[ext]')
}
}
]
},
node: {
// prevent webpack from injecting useless setImmediate polyfill because Vue
// source contains it (although only uses it if it's native).
setImmediate: false,
// prevent webpack from injecting mocks to Node native modules
// that does not make sense for the client
dgram: 'empty',
fs: 'empty',
net: 'empty',
tls: 'empty',
child_process: 'empty'
}
}
Here is the vue-webpack file:
'use strict'
const utils = require('./utils')
const config = require('../config')
const isProduction = process.env.NODE_ENV === 'production'
const sourceMapEnabled = isProduction
? config.build.productionSourceMap
: config.dev.cssSourceMap
module.exports = {
loaders: utils.cssLoaders({
sourceMap: sourceMapEnabled,
extract: isProduction
}),
cssSourceMap: sourceMapEnabled,
cacheBusting: config.dev.cacheBusting,
transformToRequire: {
video: ['src', 'poster'],
source: 'src',
img: 'src',
image: 'xlink:href'
}
}
Presumably this line should go in one of these files unfortunately it is not preventing webpack from attempting to build it (and therefore failing to do so)
Turns out after much experimentation that if I removed this line from the first snippet:
scss: generateLoaders('sass'),
The reason seems to be that even though the files in this directory are never used in my project, the loader attempts to load them because of the file name, so by not having a loader it does not attempt that and no other errors are thrown since the file is not used.
Presumably if one wanted to keep the loader and exclude a specific directory then you would need to put in a condition on this section in the first snippet:
for (const extension in loaders) {
const loader = loaders[extension]
//enter your condition here, i.e. if(loader === something) then push an object
// with "exclude"
output.push({
test: new RegExp('\\.' + extension + '$'),
exclude: ['./src/assets/sass'],
use: loader
})
}
I have a react application that I'm trying to extract the css into separate files but I'm having some issues here. I'm suing the MiniCssExtractPlugin for this. My current webpack configuration included below works fine when I include my own css files but it fails when I include my bootstrap.css from node_modules.
index.js
import React from 'react'
import ReactDOM from 'react-dom'
import MyApp from './scenes/MyApp/MyApp'
import 'bootstrap/dist/css/bootstrap.css'
import './index.css'
import './assets/stylesheets/scenes.scss'
ReactDOM.render(<MyApp />, document.getElementById('root'))
webpack.config.js
const appConstants = function() {
switch (process.env.NODE_ENV) {
case 'local':
const localConfig = require('./config/local');
return localConfig.config();
case 'development':
const devConfig = require('./config/development');
return devConfig.config();
case 'production':
default:
const prodConfig = require('./config/production');
return prodConfig.config();
}
};
const HtmlWebPackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin');
const webpack = require('webpack');
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
const htmlWebpackPlugin = new HtmlWebPackPlugin({
template: "./src/index.html",
filename: "./index.html",
hash: true
});
let webpackConfig = {
module: {
rules: [
{
test: /\.(js|jsx)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader"
}
},
{
test: /\.(css|scss)$/,
exclude: [/node_modules/],
include: [/node_modules\/bootstrap\/dist/],
use: [
{
loader: process.env.NODE_ENV !== 'local' ? MiniCssExtractPlugin.loader : 'style-loader'
},
{
loader: 'css-loader'
},
{
loader: 'sass-loader'
}
]
},
{
test: /\.(pdf|jpg|png|gif|svg|ico)$/,
exclude: [/node_modules/],
use: [
{
loader: 'file-loader'
},
]
},
{
test: /\.(woff|woff2|eot|ttf|svg|otf)$/,
exclude: [/node_modules/],
use: {
loader: 'url-loader?limit100000'
}
}
]
},
entry: [ "#babel/polyfill", "./src/index.js"],
output: {
publicPath: appConstants().DS_BASENAME ? JSON.parse(appConstants().DS_BASENAME) : '/'
},
optimization: {
splitChunks: {
chunks: 'all'
}
},
plugins: [
htmlWebpackPlugin,
new webpack.ContextReplacementPlugin(/moment[/\\]locale$/, /en/),
new BundleAnalyzerPlugin({
analyzerMode: 'disabled',
generateStatsFile: true,
statsOptions: { source: false }
}),
new webpack.DefinePlugin({
'process.env': appConstants()
}),
new webpack.EnvironmentPlugin(['NODE_ENV']),
new MiniCssExtractPlugin({
filename: (process.env.NODE_ENV == "local") ? "[name].css" : "[name].[hash].css",
chunkFilename: (process.env.NODE_ENV == "local") ? "[id].css" : "[id].[hash].css"
}),
new OptimizeCSSAssetsPlugin()
],
devServer: {
historyApiFallback: true,
port: 9090
},
watchOptions: {
aggregateTimeout: 300,
poll: 1000
}
};
// configure source map per-env
if (process.env.NODE_ENV === 'local') {
webpackConfig.devtool = 'eval-source-map';
} else {
webpackConfig.devtool = 'hidden-source-map';
}
module.exports = webpackConfig;
Here is the error I get during build:
ERROR in ./node_modules/bootstrap/dist/css/bootstrap.css 7:0
Module parse failed: Unexpected token (7:0)
You may need an appropriate loader to handle this file type.
| * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
| */
:root {
| --blue: #007bff;
| --indigo: #6610f2;
That error makes me think I'm missing the proper loader but I don't know what other loader I would need for this.
TL;DR
I want to extract all my css files including bootstrap.css into a separate file from my main.js .