Change hard coded url constants for different environments via webpack - javascript

I have a ApiCaller.js module which generate calls to our api server to get data. It has const field API_URL which points to server url.
This API_URL const changes for dev and prod environments.
So when I need to deploy to dev environment I need to change that url (API_URL) manually to point to dev-api-server and vice-versa.
I want these configuration parameters outside the code and during build process I want to change them dynamically so that I can build with different settings.
I am using webpack to bundle my javascript, html, css files.

You can store your API_URL in webpack config:
// this config can be in webpack.config.js or other file with constants
var API_URL = {
production: JSON.stringify('prod-url'),
development: JSON.stringify('dev-url')
}
// check environment mode
var environment = process.env.NODE_ENV === 'production' ? 'production' : 'development';
// webpack config
module.exports = {
// ...
plugins: [
new webpack.DefinePlugin({
'API_URL': API_URL[environment]
})
],
// ...
}
Now in your ApiCaller you can use API_URL as defined variable, which it will be different depend on process.env.NODE_ENV:
ajax(API_URL).then(/*...*/);
(edit) If I have more than production/development config for different environment constants?
Imagine that you have API_URL like in above answer, API_URL_2 and API_URL_3 which should support different environment settings production/development/test
var API_URL = {
production: JSON.stringify('prod-url'),
development: JSON.stringify('dev-url')
};
var API_URL_2 = {
production: JSON.stringify('prod-url-2'),
development: JSON.stringify('dev-url-2'),
test: JSON.stringify('test-url-2')
};
var API_URL_3 = {
production: JSON.stringify('prod-url-3'),
development: JSON.stringify('dev-url-3'),
test: JSON.stringify('test-url-3')
};
// get available environment setting
var environment = function () {
switch(process.env.NODE_ENV) {
case 'production':
return 'production';
case 'development':
return 'development';
case 'test':
return 'test';
default: // in case ...
return 'production';
};
};
// default map for supported all production/development/test settings
var mapEnvToSettings = function (settingsConsts) {
return settingsConsts[environment()];
};
// special map for not supported all production/development/test settings
var mapAPI_URLtoSettings = function () {
switch(environment()) {
case 'production':
return API_URL.production;
case 'development':
return API_URL.development;
case 'test': // don't have special test case
return API_URL.development;
};
};
// webpack config
module.exports = {
// ...
plugins: [
new webpack.DefinePlugin({
'API_URL': mapAPI_URLtoSettings(),
'API_URL_2': mapEnvToSettings(API_URL_2),
'API_URL_3': mapEnvToSettings(API_URL_3)
})
],
// ...
}
(edit 2)
If you pass string as a environment constant you should use JSON.stringify.
You don't need to define new webpack.DefinePlugin multiple times. You can do it in one object passed to new webpack.DefinePlugin - it looks cleaner.

You could set the define plugin to define a PRODUCTION variable as follows (or alternatively to true if you use different configuration files for the builds):
new webpack.DefinePlugin({
PRODUCTION: process.env.NODE_ENV === 'production'
})
Then in your code you will write something like:
var API_URL = PRODUCTION ? 'my-production-url' : 'my-development-url';
During compilation webpack will replace PRODUCTION with its value (so either true or false), and this should allow UglifyJS to minify our expression:
var API_URL = <true/false> ? 'my-production-url' : 'my-development-url';
The worst case scenario is uglify not being able to minify the conditional expression leaving it as is.

Related

How to manually throw a webpack error from the code on a condition like a missing variable?

I have some code where I have placeholders. There are some variables that I don't know yet that I need to fill in when the code is pushed to production. Right now I have the following in a file called constants.js.
export const PublicAddress = process.env.PUBLIC_ADDRESS || console.error( "Public address is not yet set." );
This code works just fine and there will be an error logged to the console when PUBLIC_ADDRESS is empty.
However, if PUBLIC_ADDRESS was empty AND I forgot to hardcode another value, I'd like Webpack to throw an error when compiling instead. It's really important that I don't forget to put in a real value when I compile it for production.
Is that possible?
It's typically this kind of check that you can provide in your own webpack plugin. here is a basic implementation of a custom plugin that check if all string from an array refer to an env variable :
class EnvVarExistPlugin {
apply(compiler) {
const envsVarToCheck = ['TEST_VAR', 'TEST_VAR2'] // put your env var list here
envsVarToCheck.forEach(envVar => {
if (!!!process.env[envVar]) {
throw new Error(`Environment variable : ${envVar} is missing`);
}
});
}
};
module.exports = EnvVarExistPlugin
Then register your plugin in your webpack.config.js :
const { resolve } = require("path");
const EnvVarExistPlugin = require("./pathToYourPlugin");
require('dotenv').config({ path: './.env' }); // adapt path to your env file
module.exports = {
entry: resolve(__dirname, "src/index.js"),
mode: 'development',
output: {
path: resolve(__dirname, "dist"),
filename: "bundle.js"
},
plugins: [new EnvVarExistPlugin()]
};

Problem with parsing javascript file marked as Shebang

I have a react app where I wanted to import a javascript file from a third-party library but file is mark with shebang #!/usr/bin/env node.
I found (e.g. here How to Configure Webpack with Shebang Loader to Ignore Hashbang Importing Cesium React Component into Typescript React Component) I can load file by overriding webpack configuration and adding a new loader shebang-loader (I also have tried shebang-loader2) but overriding webpack in react app is recommended only with #craco/craco so I added it to package.json and tried add loader to existing webpack-config.js.
I produced this lines of code. File craco.config.js:
const throwError = (message) =>
throwUnexpectedConfigError({
packageName: 'craco',
githubRepo: 'gsoft-inc/craco',
message,
githubIssueQuery: 'webpack',
});
module.exports = {
webpack: {
configure: (webpackConfig, {paths}) => {
const shebangLoader = { test: /node_modules\/npm-groovy-lint\/lib\/groovy-lint.js$/, loader: "shebang-loader" }
const {isAdded: shebangLoaderIsAdded1} = addAfterLoader(webpackConfig, loaderByName('url-loader'), shebangLoader);
if (!shebangLoaderIsAdded1) throwError('failed to add shebang-loader');
return webpackConfig;
},
},
};
It resolves problem with shebang and it ignores #!/usr/bin/env node but now I still get error
Module parse failed: Unexpected token (14:16)
File was processed with these loaders:
* ./node_modules/shebang2-loader/index.js
You may need an additional loader to handle the result of these loaders.
| const { getSourceLines, isErrorInLogLevelScope } = require("./utils");
| class NpmGroovyLint {
> "use strict";
| options = {}; // NpmGroovyLint options
| args = []; // Command line arguments
It looks like it does not recognise "use strict" line.
Can anyone put some suggestions what should be a problem ?
After few hours of investigation, I have finally come to a resolution. Firstly I have to say that there is no option to use NpmGroovyLint in react-like applications that run in browsers because after I resolved mentioned problem up here I figured that NpmGroovyLint uses node libraries as perf_hooks which are not available in a browser enviroment.
But I can post code that resolves the problem described in my question. It was needed to add a plugin to babel-loader named 'plugin-proposal-class-properties'. Here is my snipped of craco config. You can use it as a recipe occasionally.
const {addAfterLoader, getLoaders, loaderByName, removeLoaders, throwUnexpectedConfigError} = require('#craco/craco');
const throwError = (message) =>
throwUnexpectedConfigError({
packageName: 'craco',
githubRepo: 'gsoft-inc/craco',
message,
githubIssueQuery: 'webpack',
});
module.exports = {
webpack: {
configure: (webpackConfig, {paths}) => {
const {hasFoundAny, matches} = getLoaders(webpackConfig, loaderByName('babel-loader'));
if (!hasFoundAny) throwError('failed to find babel-loader');
const {hasRemovedAny, removedCount} = removeLoaders(webpackConfig, loaderByName('babel-loader'));
if (!hasRemovedAny) throwError('no babel-loader to remove');
if (removedCount !== 2) throwError('had expected to remove 2 babel loader instances');
//add plugin proposal class properties to existing babel loader
const propClassOptions = {...matches[1].loader.options, ...{plugins: ["#babel/plugin-proposal-class-properties"]}};
const propClassLoader = {...matches[1].loader, ...{options: propClassOptions}};
const babelLoaderWithPropClassPlugin = {...matches[1], ...{loader: propClassLoader}};
const shebangLoader = {
test: /node_modules\/npm-groovy-lint\/lib\/groovy-lint.js$/,
use: [{loader: 'shebang2-loader'}, {...{loader: require.resolve('babel-loader')}, ...{options: propClassOptions}}]
}
const {isAdded: babelLoaderWithPropClassIsAdded} = addAfterLoader(webpackConfig, loaderByName('url-loader'), matches[0].loader);
if (!babelLoaderWithPropClassIsAdded) throwError('failed to add ts-loader');
const {isAdded: babelLoaderIsAdded} = addAfterLoader(webpackConfig, loaderByName('babel-loader'), babelLoaderWithPropClassPlugin.loader);
if (!babelLoaderIsAdded) throwError('failed to add back babel-loader for non-application JS');
const {isAdded: shebangLoaderIsAdded1} = addAfterLoader(webpackConfig, loaderByName('url-loader'), shebangLoader);
if (!shebangLoaderIsAdded1) throwError('failed to add shebang-loader');
return webpackConfig;
},
},
};

How to correctly build NestJS app for production with node_modules dependencies in bundle?

After nest build or nest build --webpack dist folder does not contain all required modules and I got Error: Cannot find module '#nestjs/core' when trying to run node main.js.
I could not find any clear instructions on https://docs.nestjs.com/ on how to correctly build app for production, so maybe I missed something?
Out of the box, nest cli does not support including the node_modules dependencies into the dist bundle.
However, there are some community examples of custom webpack configs that include the dependencies in the bundle, e.g. bundled-nest. As described in this issue, it is necessary to include the webpack.IgnorePlugin to whitelist unused dynamic libraries.
bundle-nest has been archived/discontinued:
We've concluded that it is not recommended to bundle NestJS, or actually, NodeJS web servers in general. This is archived for historical reference during the period of time when the community was attempting to tree-shake, and bundle NestJS apps. Refer to #kamilmysliwiec comment for details:
In many real-world scenarios (depending on what libraries are being used), you should not bundle Node.js applications (not only NestJS applications) with all dependencies (external packages located in the node_modules folder). Although this may make your docker images smaller (due to tree-shaking), somewhat reduce the memory consumption, slightly increase the bootstrap time (which is particularly useful in the serverless environments), it won't work in combination with many popular libraries commonly used in the ecosystem. For instance, if you try to build NestJS (or just express) application with MongoDB, you will see the following error in your console:
Error: Cannot find module './drivers/node-mongodb-native/connection' at webpackEmptyContext
Why? Because mongoose depends on mongodb which depends on kerberos (C++) and node-gyp.
Well, about mongo, you can make some exceptions (leave some modules in node_modules), can you? It's not like it's all or nothing. But still, I'm not sure you want to follow this path. I've just succeeded with bundling a nestjs application. It was a proof of concept, I'm not sure if it'll go into production. And it was hard, I might have broken something in the process, but at first glance it works. The most complex part was adminjs. It has rollup and babel as dependencies. And in the app code they unconditionally call watch for some reason (UDP noop in production). Anyways, if you'd like to follow this path you should be ready to debug/inspect your packages' code. And you might need to add workarounds as new packages are added to the project. But it all depends on your dependencies, it may be easier than in my case. For a freshly created nestjs + mysql app it was relatively simple.
The config I ended up with (it overrides the nestjs defaults):
webpack.config.js (webpack-5.58.2, #nestjs/cli-8.1.4):
const path = require('path');
const MakeOptionalPlugin = require('./make-optional-plugin');
module.exports = (defaultOptions, webpack) => {
return {
externals: {}, // make it not exclude `node_modules`
// https://github.com/nestjs/nest-cli/blob/v7.0.1/lib/compiler/defaults/webpack-defaults.ts#L24
resolve: {
...defaultOptions.resolve,
extensions: [...defaultOptions.resolve.extensions, '.json'], // some packages require json files
// https://unpkg.com/browse/babel-plugin-polyfill-corejs3#0.4.0/core-js-compat/data.js
// https://unpkg.com/browse/core-js-compat#3.19.1/data.json
alias: {
// an issue with rollup plugins
// https://github.com/webpack/enhanced-resolve/issues/319
'#rollup/plugin-json': '/app/node_modules/#rollup/plugin-json/dist/index.js',
'#rollup/plugin-replace': '/app/node_modules/#rollup/plugin-replace/dist/rollup-plugin-replace.cjs.js',
'#rollup/plugin-commonjs': '/app/node_modules/#rollup/plugin-commonjs/dist/index.js',
},
},
module: {
...defaultOptions.module,
rules: [
...defaultOptions.module.rules,
// a context dependency
// https://github.com/RobinBuschmann/sequelize-typescript/blob/v2.1.1/src/sequelize/sequelize/sequelize-service.ts#L51
{test: path.resolve('node_modules/sequelize-typescript/dist/sequelize/sequelize/sequelize-service.js'),
use: [
{loader: path.resolve('rewrite-require-loader.js'),
options: {
search: 'fullPath',
context: {
directory: path.resolve('src'),
useSubdirectories: true,
regExp: '/\\.entity\\.ts$/',
transform: ".replace('/app/src', '.').replace(/$/, '.ts')",
},
}},
]},
// adminjs resolves some files using stack (relative to the requiring module)
// and actually it needs them in the filesystem at runtime
// so you need to leave node_modules/#adminjs/upload
// I failed to find a workaround
// it bundles them to `$prj_root/.adminjs` using `rollup`, probably on production too
// https://github.com/SoftwareBrothers/adminjs-upload/blob/v2.0.1/src/features/upload-file/upload-file.feature.ts#L92-L100
{test: path.resolve('node_modules/#adminjs/upload/build/features/upload-file/upload-file.feature.js'),
use: [
{loader: path.resolve('rewrite-code-loader.js'),
options: {
replacements: [
{search: /adminjs_1\.default\.bundle\('\.\.\/\.\.\/\.\.\/src\/features\/upload-file\/components\/edit'\)/,
replace: "adminjs_1.default.bundle('/app/node_modules/#adminjs/upload/src/features/upload-file/components/edit')"},
{search: /adminjs_1\.default\.bundle\('\.\.\/\.\.\/\.\.\/src\/features\/upload-file\/components\/list'\)/,
replace: "adminjs_1.default.bundle('/app/node_modules/#adminjs/upload/src/features/upload-file/components/list')"},
{search: /adminjs_1\.default\.bundle\('\.\.\/\.\.\/\.\.\/src\/features\/upload-file\/components\/show'\)/,
replace: "adminjs_1.default.bundle('/app/node_modules/#adminjs/upload/src/features/upload-file/components/show')"},
],
}},
]},
// not sure what babel does here
// I made it return standardizedName
// https://github.com/babel/babel/blob/v7.16.4/packages/babel-core/src/config/files/plugins.ts#L100
{test: path.resolve('node_modules/#babel/core/lib/config/files/plugins.js'),
use: [
{loader: path.resolve('rewrite-code-loader.js'),
options: {
replacements: [
{search: /const standardizedName = [^;]+;/,
replace: match => `${match} return standardizedName;`},
],
}},
]},
// a context dependency
// https://github.com/babel/babel/blob/v7.16.4/packages/babel-core/src/config/files/module-types.ts#L51
{test: path.resolve('node_modules/#babel/core/lib/config/files/module-types.js'),
use: [
{loader: path.resolve('rewrite-require-loader.js'),
options: {
search: 'filepath',
context: {
directory: path.resolve('node_modules/#babel'),
useSubdirectories: true,
regExp: '/(preset-env\\/lib\\/index\\.js|preset-react\\/lib\\/index\\.js|preset-typescript\\/lib\\/index\\.js)$/',
transform: ".replace('./node_modules/#babel', '.')",
},
}},
]},
],
},
plugins: [
...defaultOptions.plugins,
// some optional dependencies, like this:
// https://github.com/nestjs/nest/blob/master/packages/core/nest-application.ts#L45-L52
// `webpack` detects optional dependencies when they are in try/catch
// https://github.com/webpack/webpack/blob/main/lib/dependencies/CommonJsImportsParserPlugin.js#L152
new MakeOptionalPlugin([
'#nestjs/websockets/socket-module',
'#nestjs/microservices/microservices-module',
'class-transformer/storage',
'fastify-swagger',
'pg-native',
]),
],
// to have have module names in the bundle, not some numbers
// although numbers are sometimes useful
// not really needed
optimization: {
moduleIds: 'named',
}
};
};
make-optional-plugin.js:
class MakeOptionalPlugin {
constructor(deps) {
this.deps = deps;
}
apply(compiler) {
compiler.hooks.compilation.tap('HelloCompilationPlugin', compilation => {
compilation.hooks.succeedModule.tap(
'MakeOptionalPlugin', (module) => {
module.dependencies.forEach(d => {
this.deps.forEach(d2 => {
if (d.request == d2)
d.optional = true;
});
});
}
);
});
}
}
module.exports = MakeOptionalPlugin;
rewrite-require-loader.js:
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#escaping
function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string
}
function processFile(source, search, replace) {
const re = `require\\(${escapeRegExp(search)}\\)`;
return source.replace(
new RegExp(re, 'g'),
`require(${replace})`);
}
function processFileContext(source, search, context) {
const re = `require\\(${escapeRegExp(search)}\\)`;
const _d = JSON.stringify(context.directory);
const _us = JSON.stringify(context.useSubdirectories);
const _re = context.regExp;
const _t = context.transform || '';
const r = source.replace(
new RegExp(re, 'g'),
match => `require.context(${_d}, ${_us}, ${_re})(${search}${_t})`);
return r;
}
module.exports = function(source) {
const options = this.getOptions();
return options.context
? processFileContext(source, options.search, options.context)
: processFile(source, options.search, options.replace);
};
rewrite-code-loader.js:
function processFile(source, search, replace) {
return source.replace(search, replace);
}
module.exports = function(source) {
const options = this.getOptions();
return options.replacements.reduce(
(prv, cur) => {
return prv.replace(cur.search, cur.replace);
},
source);
};
The supposed way to build the app is:
$ nest build --webpack
I didn't bother with source maps, since the target is nodejs.
It's not a config you can just copy-paste, you should figure out what's needed for your project yourself.
One more trick here, but well, you probably won't need it.
UPD adminjs seems to come with prebuilt bundles, so this config may be significantly simpler.

How to convert a clever require to an import?

Previously I was using a clever index.js file:
var all = { ..... };
module.exports = _.merge(all,
require('./' + process.env.NODE_ENV + '.js') || {});
To import my env settings baed on the NODE_ENV, and where I have files such as:
development.js
production.js
shared.js
...
And when I did a require() for this index.js file, I recieved the env variables I wanted based on my NODE_ENV setting.
Is there any way to do this with ES6 and import???
In the end, I needed to use a different solution for ES6. You can do a declare require(package: string); which will be bound to the correct method after the translation to normal js for the browser. Then do a:
let foo_package = require('foo');
but this seems a bit more difficult. Instead I created:
development.ts
test.ts,
production.ts
and
import development from 'development';
import test from 'test';
import production from 'production';
export function choose(env: string) {
switch(env) {
case 'development':
return development;
case 'test'
return test;
case 'production':
return production;
default:
throw Error(`choose 'development', 'test', or 'production'!);
}
}
This worked just as well and didn't need plain js;

Delete unused webpack chunked files

I'm looking for information on how to delete old webpack chunked files. Here is my current webpack configuration:
var path = require('path');
var webpack = require('webpack');
module.exports = {
debug: false,
outputPathinfo: true,
displayErrorDetails: true,
context: __dirname,
entry: {
common: ['./src/common.coffee'],
a: './src/a.cjsx',
b: './src/b.cjsx'
},
output: {
filename: '[name]-[chunkhash].js',
chunkFileName: '[name].[chunkhash].js',
path: path.join(__dirname, 'js')
},
plugins: [
new webpack.optimize.CommonsChunkPlugin('common', 'common-[chunkhash].js'),
new webpack.optimize.UglifyJsPlugin({
compress: { warnings: false }
})
],
module: {
preLoaders: [
{
test: /\.coffee$/,
exclude: /node_modules/,
loader: 'coffeelint-loader'
}
],
loaders: [
{ test: /\.coffee/, loader: 'coffee' },
{ test: /\.cjsx$/, loaders: ['coffee', 'cjsx'] },
{ test: /\.js$/, loader: 'jsx-loader?harmony' }
]
}
}
If I am running $(npm bin)/webpack --config webpack.js --watch and make changes to a.cjsx, it compiles a newer version of that file with a new chunkedhash. However, the old one remains and I'd like it to be deleted right away.
How can I delete the old version of the chunked file?
Is there a way for me to hook into an after callback once watch finishes compiling?
There is a clean-webpack-plugin for those purposes, or you can write a simple bash script for npm:
"scripts": {
"build": "rm -r dist/* && webpack -p",
"clean": "rm -r dist/*"
}
Here is the webpack-clean-obsolete-chunks plugin, which do what you want. It searches for all updated chunks and deletes obsolete files after each webpack compilation.
The answer
I've decided to write an answer because others - although trying to answer the question directly - overlooked the most important part in my opinion.
And the most important part is: you shouldn't be doing it this way. Using [hash] placeholders in your development setup cause many headaches with other tooling (phpstorm's path autocomplete in symfony plugin for example). Also it's poor for webpack's incremental compilation performance and thus is not recommended by official webpack docs (reference).
So for future readers: just keep it simple for development config - define your filename as [name].js and move on.
Edit
There seems to be a confusion about what to do with the old chunk-files on the production server. Well, you don't do anything. Once a version is deployed it shouldn't be ever changed. You just keep creating new versions when deploying and keep previous as a backup. Why?
Because you want you're rollback to be reliable and for it to be possible your rollback needs to be extremely simple and atomic. If your rollback procedure is doing anything more than switching a symlink, rerouting to previous container (or similar simple operation) you're probably™ going to end up in trouble.
Rollback isn't a process of "re-deploying" the application again, but now to the previous version. It's a process of "un-doing" the deployment. So doing a git checkout to the previous version followed by a npm build --but-please-be-hurry --and-im-begging-you-dont-fail while your production app is hanging there, completely exploded doesn't cut here.
Rebuilding a previous version of the application - just like the deployment - may fail for many reasons. That's why a rollback should be switching/rerouting back to the exact same version-build that is proven to be working. Not ==-the-same, 100% ===-the-same. That's why you need to keep your previous version around, because that's the ===-same. A "regenerated" one is - in best case scenario - only ==-the-same, and so it is not proven to be working, only assumed.
And no, no amount of CI, staging environments or whatever will give you a guaranteed successful deployment. Part of doing it the right way is to be prepared for when things go wrong. And things will go wrong. Hopefully only from time to time, but still.
Of course once you have 3, 5 or <put-your-number-here> versions backed up you may start to remove the oldest ones as you probably won't ever need more than 3.
Since Webpack 5.20.0 you can use output.clean option
Take a look at this pull request:
https://github.com/johnagan/clean-webpack-plugin/pull/32/files
Open raw file view and copy it to index.js of clean webpack plugin.
Remember about config flag -> watch: true
I have solved that problem by adding below in webpack.config.js
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
{
... your configs ...
plugins: [new CleanWebpackPlugin()]
}
You can solve the problem № 1 by using remove-files-webpack-plugin.
Use this plugin like this:
plugins: [
new RemovePlugin({
watch: {
test: [
{
folder: './js',
method: (absPath) => new RegExp(/(.*)-([^-\\\/]+)\.js/).test(absPath)
}
]
}
})
]
In "watch" mode (not normal compilation!) it grabs all files from ./js folder and tests them with this regular expression /(.*)-([^-\\\/]+)\.js/. Analyze this regular expression on regex101 (unit tests are included) if you have problems with understanding.
Note: i'm the creator of this plugin.
Looks like webpack#5.20.0+ has built-in support for this https://webpack.js.org/configuration/output/#outputclean. I use [chunkhash] in my chunk filenames and they get cleared out if I stop comment out dynamic imports and added back in if I uncomment them.
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$hash.css
- - [themes]:
- - - light.css
- - - dark.css
- - - blue.css
For Windows users
"scripts": {
"build": "npm run clean && webpack --mode production",
"clean": "del /f /s /q dist 1>nul"
}
I just had to stop my server and run yarn serve again

Categories