I have to use Webpack for one of my projects to build front-end bundles for js, css and other static assets. It does the job well, but in my early stages of the project I've got only some css and static images and no js files yet. Here is my full webpack.config.js
const Webpack = require("webpack");
const Glob = require("glob");
const path = require("path");
const CopyWebpackPlugin = require("copy-webpack-plugin");
const configurator = {
entries: function(){
var entries = {
application: [
'./assets/dummy.js',
],
}
return entries
},
plugins() {
var plugins = [
new CopyWebpackPlugin([{from: "./assets",to: ""}], {copyUnmodified: true,ignore: ["css/**", "js/**", "**.js"] }),
];
return plugins
},
moduleOptions: function() {
return {
rules: [
]
}
},
buildConfig: function(){
const env = process.env.NODE_ENV || "development";
var config = {
mode: env,
entry: configurator.entries(),
output: {filename: "[name].[hash].js", path: `${__dirname}/public/assets`},
plugins: configurator.plugins(),
module: configurator.moduleOptions()
}
return config
}
}
module.exports = configurator.buildConfig()
Practically what it does for me is copying assets to public dir. I don't have any javascripts yet, but they will be in the future. So, I tried commenting entry, setting it to null or empty string with no luck. It seems Webpack needs to process js files so badly. My current solution is creating an empty dummy.js file and feeding it to Webpack. Annoyingly it generates some 3.3kb application.afff4a3748b8d5d33a3a.js file with some boilerplate js code, despite that my source js file is totally empty.
I understand that this is an edge use case for Webpack and Webpack was primarily created for processing javascript, but I bet many people still use it not just for bundling javascripts. So, my question, is there a better, more elegant way to skip bundling js files in Webpack?
p.s.
I think, I've found a related question without an answer here How to make WebPack copy a library instead of bundling?
p.s.#2
The suggested duplicate question has an accepted answer with an invalid Webpack config Invalid configuration object., so, I can't use it to solve my issue.
Moreover, the answer reads
webpack will create a dummy javascript file
and I'm specifically asking how to avoid creating unnecessary files.
Related
I'm trying and failing to write a loader for webpack that allows me to do some custom image processing on loaded images. The webpack config is generated by Vue CLI, so I have to integrate the loader into chainWebpack somehow. The simplest example I could think of was one that shouldn't change the images at all:
vue.config.js
const path = require("path");
const { defineConfig } = require('#vue/cli-service');
module.exports = defineConfig({
transpileDependencies: true,
chainWebpack: config => {
config.module
.rule("resize-image")
.test(/\.(jpe?g|png|webp)$/i)
.use("test-loader")
.loader(path.resolve("./test_loader.js"));
}
});
test_loader.js
function testLoader(resourcePath) {
return resourcePath;
}
module.exports = testLoader;
Unfortunately adding this "no-op" loader already breaks the loading of images completely and I'm left wondering if I have some sort of fundamental misunderstanding of how Webpack loaders work. I would think that doing absolutely nothing to the input data shouldn't change the outcome, but it doesn't seem to be the case. Am I missing something obvious here? Why is my basic loader not working?
The app is created with the default template for Kotlin React apps:
uses KTS-based Gradle build scripts;
Kotlin JS plugin 1.6.10;
Kotlin wrappers for React 17.0.2.
When using ./gradlew browserProductionWebpack without any additional tweaks, it generates a build/distributions directory with:
all resources (without any modifications);
index.html (without any modifications);
Kotlin sources compiled into one minified .js file.
What I want is to:
add some hash to the generated .js file;
minify the index.html file and refer the hashed .js file in it;
minify all resources (.json localization files).
Please prompt me some possible direction to do it. Looking to webpack configuration by adding corresponding scripts into webpack.config.d, but no luck yet: tried adding required dependencies into build.gradle.kts, i.e.:
implementation(devNpm("terser-webpack-plugin", "5.3.1"))
implementation(devNpm("html-webpack-plugin", "5.5.0"))
and describing webpack scripts:
const TerserPlugin = require("terser-webpack-plugin");
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
optimization: {
minimizer: [
new TerserPlugin(),
new HtmlWebpackPlugin({
minify: {
removeAttributeQuotes: true,
collapseWhitespace: true,
removeComments: true,
},
}),
],
}
}
Any hint will be appreciated.
A couple of things to put into consideration first.
If some flexible bundling configuration is needed, most likely it won't be possible to use Kotlin-wrapped (Gradle) solutions. After checking the org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpackConfig it turns out that there is only a limited set of things can be configured with it.
JS-based webpack configs require a little bit of reverse engineering to find out what is generated from the Kotlin/Gradle side and how to extend it.
For simple configurations (IMHO) there is almost no need in tweaking bundling options while using the Kotlin JS plugin.
Let's start from initial webpack configs. In my case (see the short environment description in the question above) they appear in ./build/js/packages/<project-name>/webpack.config.js. These original configs will also include all contents from JS files we create inside the ./webpack.config.d folder.
Some webpack configurations require external JS dependencies. We need to declare them in the dependencies block of build.gradle.kts. In my case they are represented with:
// Bundling.
implementation(devNpm("html-webpack-plugin", "5.5.0"))
implementation(devNpm("uglifyjs-webpack-plugin", "2.2.0"))
implementation(devNpm("terser-webpack-plugin", "5.3.1"))
implementation(devNpm("copy-webpack-plugin", "9.1.0" )) // newer versions don't work correctly with npm and Yarn
implementation(devNpm("node-json-minify", "3.0.0"))
I also dropped all commonWebpackConfigs from build.gradle.kts as they are going to be performed manually on the JS level.
All webpack JS configs (inside the ./webpack.config.d folder) are divided into 3 files:
common.js (dev server configuration for both dev and production builds):
// All route paths should fallback to the index page to make SPA's routes processed correctly.
const devServer = config.devServer = config.devServer || {};
devServer.historyApiFallback = true;
development.js:
// All configs inside of this file will be enabled only in the development mode.
// To check the outputs of this config, see ../build/processedResources/js/main
if (config.mode == "development") {
const HtmlWebpackPlugin = require("html-webpack-plugin");
// Pointing to the template to be used as a base and injecting the JS sources path into.
config.plugins.push(new HtmlWebpackPlugin({ template: "./kotlin/index.html" }));
}
and production.js:
// All configs inside of this file will be enabled only in the production mode.
// The result webpack configurations file will be generated inside ../build/js/packages/<project-name>
// To check the outputs of this config, see ../build/distributions
if (config.mode == "production") {
const HtmlWebpackPlugin = require("html-webpack-plugin"),
UglifyJsWebpackPlugin = require("uglifyjs-webpack-plugin"),
TerserWebpackPlugin = require("terser-webpack-plugin"),
CopyWebpackPlugin = require("copy-webpack-plugin"),
NodeJsonMinify = require("node-json-minify");
// Where to output and how to name JS sources.
// Using hashes for correct caching.
// The index.html will be updated correspondingly to refer the compiled JS sources.
config.output.filename = "js/[name].[contenthash].js";
// Making sure optimization and minimizer configs exist, or accessing its properties can crash otherwise.
config.optimization = config.optimization || {};
const minimizer = config.optimization.minimizer = config.optimization.minimizer || [];
// Minifying HTML.
minimizer.push(new HtmlWebpackPlugin({
template: "./kotlin/index.html",
minify: {
removeAttributeQuotes: true,
collapseWhitespace: true,
removeComments: true,
},
}));
// Minifying and obfuscating JS.
minimizer.push(new UglifyJsWebpackPlugin({
parallel: true, // speeds up the compilation
sourceMap: false, // help to match obfuscated functions with their origins, not needed for now
uglifyOptions: {
compress: {
drop_console: true, // removing console calls
}
}
}));
// Additional JS minification.
minimizer.push(new TerserWebpackPlugin({
extractComments: true // excluding all comments (mostly licence-related ones) into a separate file
}));
// Minifying JSON locales.
config.plugins.push(new CopyWebpackPlugin({
patterns: [
{
context: "./kotlin",
from: "./locales/**/*.json",
to: "[path][name][ext]",
transform: content => NodeJsonMinify(content.toString())
}
]
}));
}
I use styled components, so no CSS configs are provided. In other things these configs do almost the same minification as being done out-of-the-box without any additional configs. The differences are:
JS sources use a hash in their name: it is referenced correctly from the index page HTML template;
HTML template is minified;
locales (just simple JSON files) are minified.
It can look like an overhead slightly because as mentioned in the beginning, it does almost the same with minimal differences from the out-of-the-box configs. But as advantage we're getting more flexible configs which can be tweaked easier further.
I am quite new to Webpack, so bear with me if thats a stupid question.
My goal is to transform my old, AMD based codebase to a ES6 Module based solution. What I am struggling with is handling dynamic import()s. So my app router works on a module basis, i.e. each route is mapped to a module path and then required. Since I know what modules will be included, I just add those dynamically imported modules to my r.js configuration and am able to build everything in a single file, with all require calls still working.
Now, I am trying to do the same with ES6 modules and Webpack. With my devmode this is no problem as I can just replace require() with import(). However I cannot get this to work with bundling. Either Webpack splits my code (and still fails to load the dynamic module anyways), or - if I use the Array format for the entry config, the dynamic module is included in the bundle but loading still fails: Error: Cannot find module '/src/app/DynClass.js'
This is how my Webpack config looks like:
const webpack = require('webpack');
const path = require('path');
module.exports = {
mode: "development",
entry: ['./main.js', './app/DynClass.js'],
output: {
filename: 'main.js',
path: path.resolve(__dirname, "../client/")
},
resolve: {
alias: {
"/src": path.resolve(__dirname, '')
}
},
module: {
rules: [
{
test: /\.tpl$/i,
use: 'raw-loader',
},
]
}
};
So basically I want to tell Webpack: "hey, there is another module (or more) that is to be loaded dynamically and I want it to be included in the bundle"
How can I do this?
So yeah, after much fiddling there seems to be a light at the end of the tunnel. Still, this is not a 100% solution and it is surely not for the faint of heart, as it is quite ugly and fragile. But still I want to share my approach with you:
1) manual parsing of my routes config
My router uses a config file looking like this:
import StaticClass from "/src/app/StaticClass.js";
export default {
StaticClass: {
match: /^\//,
module: StaticClass
},
DynClass: {
match: /^\//,
module: "/src/app/DynClass.js"
}
};
So as you can see the export is an object, with keys acting as the route id, and an object that contains the matches (regex based) and the module which should be executed by the router if the route matches. I can feed my router with both a Constructor function (or an object) for modules which are available immediatly (i.e. contained in the main chunk) or if the module value is a string, this means that the router has to load this module dynamically by using the path specified in the string.
So as I know what modules could be potentially loaded (but not if and when) I can now parse this file within my build process and transform the route config to something webpack can understand:
const path = require("path");
const fs = require("fs");
let routesSource = fs.readFileSync(path.resolve(__dirname, "app/routes.js"), "utf8");
routesSource = routesSource.substr(routesSource.indexOf("export default"));
routesSource = routesSource.replace(/module:\s*((?!".*").)*$/gm, "module: undefined,");
routesSource = routesSource.replace(/\r?\n|\r/g, "").replace("export default", "var routes = ");
eval(routesSource);
let dummySource = Object.entries(routes).reduce((acc, [routeName, routeConfig]) => {
if (typeof routeConfig.module === "string") {
return acc + `import(/* webpackChunkName: "${routeName}" */"${routeConfig.module}");`;
}
return acc;
}, "") + "export default ''";
(Yeah I know this is quite ugly and also a bit brittle so this surely could be done better)
Essentially I create a new, virtual module where every route entry which demands a dynamic import is translated, so:
DynClass: {
match: /^\//,
module: "/src/app/DynClass.js"
}
becomes:
import(/* webpackChunkName: "DynClass" */"/src/app/DynClass.js");
So the route id simply becomes the name of the chunk!
2) including the virtual module in the build
For this I use the virtual-module-webpack-plugin:
plugins: [
new VirtualModulePlugin({
moduleName: "./app/dummy.js",
contents: dummySource
})
],
Where dummySource is just a string containing the sourcecode of my virtual module I just have generated. Now, this module is pulled in and the "virtual imports" can be processed by webpack. But wait, I still need to import the dummy module, but I do not have any in my development mode (where I use everything natively, so no loaders).
So in my main code I do the following:
let isDev = false;
/** #remove */
isDev = true;
/** #endremove */
if (isDev) { import('./app/dummy.js'); }
Where "dummy.js" is just an empty stub module while I am in development mode. The parts between that special comments are removed while building (using the webpack-loader-clean-pragma loader), so while webpack "sees" the import for dummy.js, this code will not be executed in the build itself since then isDev evaluates to false. And since we already defined a virtual module with the same path, the virtual module is included while building just like I want, and of course all dependencies are resolved as well.
3) Handling the actual loading
For development, this is quite easy:
import routes from './app/routes.js';
Object.entries(routes).forEach(async ([routeId, route]) => {
if (typeof route.module === "function") {
new route.module;
} else {
const result = await import(route.module);
new result.default;
}
});
(Note that this is not the actual router code, just enough to help me with my PoC)
Well, but for the build I need something else, so I added some code specific to the build environment:
/** #remove */
const result = await import(route.module);
new result.default;
/** #endremove */
if (!isDev) {
if (typeof route.module === "string") { await __webpack_require__.e(routeId); }
const result = __webpack_require__(route.module.replace("/src", "."));
new result.default;
}
Now, the loading code for the dev environment is just stripped out, and there is another loading code that uses webpack internally. I also check if the module value is a function or string, and if it is the latter I invoke the internal require.ensure function to load the correct chunk: await __webpack_require__.e(routeId);. Remember that I named my chunks when generating the virtual module? Now thats why I still can find them now!
4) more needs to be done
Another thing I encountered is when several dynamically loaded modules have the same dependencies, webpack tries to generate more chunks with names like module1~module2.bundle.js, breaking my build. To counter this, I needed to make sure that all those shared modules go into a specific named bundle I called "shared":
optimization: {
splitChunks: {
chunks: "all",
name: "shared"
}
}
And when in production mode, I simply load this chunk manually before any dynamic modules depending on it are requested:
if (!isDev) {
await __webpack_require__.e("shared");
}
Again, this code only runs in production mode!
Finally, I have to prevent webpack renaming my modules (and chunks) to something like "1", "2" etc, but rather keep the names I just have defined:
optimization: {
namedChunks: true,
namedModules: true
}
Se yeah, there you have it! As I said this wasn't pretty but seems to work, at least with my simplified test setup. I really hope there aren't any blockers ahead of me when I do all the rest (like ESLint, SCSS etc)!
In a vue cli 3 project I want to display a version number in the webpage. The version number lies in the package.json file.
The only reference to this that I found is this link in the vue forum.
However, I can't get the proposed solution to work.
Things I tried
Use webpack.definePlugin as in the linked resource:
vue.config.js
const webpack = require('webpack');
module.exports = {
lintOnSave: true,
configureWebpack: config => {
return {
plugins: [
new webpack.DefinePlugin({
'process.env': {
VERSION: require('./package.json').version,
}
})
]
}
},
}
Then in main.ts I read process.env, but it does not contain VERSION (I tried several variants to this, like generating a PACKAGE_JSON field like in the linked page, and generating plain values like 'foo' instead of reading from package-json). It never worked, it is like the code is being ignored. I guess the process.env is being redefined later by vue webpack stuff.
The process log in main.ts contains, however, all the stuff that process usually contains in a vue-cli project, like the mode and the VUE_APP variables defined in .env files.
Try to write to process right on the configure webpack function,
like:
configureWebpack: config => {
process.VERSION = require('./package.json').version
},
(to be honest I did not have much hope with this, but had to try).
Tried the other solution proposed in the linked page,
like:
// vue.config.js
module.exports = {
chainWebpack: config => {
config.plugin('define').tap( ([options = {}]) => {
return [{
...options, // these are the env variables from your .env file, if any arr defined
VERSION: JSON.stringify(require('./package.json').version)
}]
})
}
}
But this fail silently too.
Use the config.plugin('define') syntax suggested by #Oluwafemi,
like:
chainWebpack: (config) => {
return config.plugin('define').tap(
args => merge(args, [VERSION])
)
},
Where VERSION is a local variable defined as:
const pkgVersion = require('./package.json').version;
const VERSION = {
'process.env': {
VUE_APP_VERSION: JSON.stringify(pkgVersion)
}
}
But this is not working either.
I am re-starting the whole project everytime, so that's not the reason why the process stuff does not show up.
My vue-cli version is 3.0.1.
I am adding my 2 cents, as I found a shorter way and apparently the right way (https://docs.npmjs.com/misc/scripts#packagejson-vars)
Add this in your vue.config.file before the export, not inside:
process.env.VUE_APP_VERSION = process.env.npm_package_version
And voilà!
You can then use it from a component with process.env.VUE_APP_VERSION
TLDR
The following snippet in the vue.config.js file will do the trick, and will allow you to access the version of your app as APPLICATION_VERSION:
module.exports = {
configureWebpack: config => {
return {
plugins: [
new webpack.DefinePlugin({
'APPLICATION_VERSION': JSON.stringify(require('./package.json').version),
})
]
}
},
}
TIP:
Don't even try to add some key to process.env via webpack.definePlugin: it won't work as you probably expect.
Why my previous efforts did not work
At the end, I solved the issue via webpack.DefinePlugin. The main issue I had is that the original solution I found was using definePlugin to write to a process.env.PACKAGE_JSON variable.
This suggests that definePlugin somehow allows to add variables to process or process.env, which is not the case. Whenever I did log process.env in the console, I didn't find the variables I was trying to push into process.env : so I though the definePlugin tech was not working.
Actually, what webpack.definePlugin does is to check for strings at compile time and change them to its value right on your code. So, if you define an ACME_VERSION variable via:
module.exports = {
lintOnSave: true,
configureWebpack: config => {
return {
plugins: [
new webpack.DefinePlugin({
'ACME_VERSION': 111,
})
]
}
},
}
and then, in main.js you print console.log(ACME_VERSION), you will get 111 properly logged.
Now, however, this is just a string change at compile time. If instead of ACME_VERSION you try to define process.env.VUE_APP_ACME_VERSION...
when you log process.env the VUE_APP_ACME_VERSION key won't show up in the object. However, a raw console.log('process.env.VUE_APP_ACME_VERSION') will yield 111 as expected.
So, basically, original link and the proposed solutions were correct to some degree. However, nothing was really being added to the process object. I was logging proccess.env during my initial tries, so I didn't see anything working.
Now, however, since the process object is not being modified, I strongly suggest anyone trying to load variables to their vue app at compile time not to use it. Is misleading at best.
You can simply import your package.json file and use its variables.
import { version } from "../../package.json";
console.log(version)
If you are using Webpack, you can inject the variable in the following way:
// webpack.config.js
plugins: [
new webpack.DefinePlugin({
VERSION: JSON.stringify(require("package.json").version)
})
]
// any-module.js
console.log("Version: " + VERSION);
https://github.com/webpack/webpack/issues/237
When building the Vue app, environment variables that don't begin with the VUE_APP_ prefix are filtered out. NODE_ENV and BASE_URL environment variables are the exception.
The above information applies when the environment variables are set prior to building the Vue app and not in this situation.
In a situation where environment variables are set during the build, it's important to look at what Vue CLI is doing.
The Vue CLI uses webpack.DefinePlugin to set environment variables using the object returned from the call to resolveClientEnv.
resolveClientEnv returns
{
'process.env': {}
}
This means when configuring your environment variables at build time, you need to come upon a way to merge with the existing one.
You need to perform a deep merge of both arrays, so that value for process.env key is an object containing keys from the resolved client environment and your keys.
chainWebpack key in the default export for vue.config.js is just about one of the ways to get this done.
The arguments passed to initialize the DefinePlugin can be merged with new environment variables that you like to configure using the underlying webpack-chain API. Here is an example:
// vue.config.js
const merge = require('deepmerge');
const pkgVersion = require('./package.json').version;
const VERSION = {
'process.env': {
VERSION: JSON.stringify(pkgVersion)
}
}
module.exports = {
chainWebpack: config =>
config
.plugin('define')
.tap(
args => merge(args, [VERSION])
)
}
Your initial attempt was fine, you were just missing the JSON.stringify part:
const webpack = require('webpack');
module.exports = {
configureWebpack: config => {
return {
plugins: [
new webpack.DefinePlugin({
'process.env': {
VERSION: JSON.stringify(require('./package.json').version),
}
})
]
}
},
}
Edit: although the webpack docs recommend the 'process.env.VERSION' way (yellow panel):
new webpack.DefinePlugin({
'process.env.VERSION': JSON.stringify(require('./package.json').version),
}),
Official solutions tend to be more reliable Modes and Environment Variables | Vue CLI
TIP
You can have computed env vars in your vue.config.js file. They still need to be prefixed with VUE_APP_. This is useful for version info
process.env.VUE_APP_VERSION = require('./package.json').version
module.exports = {
// config
}
I attempted the accepted answer, and had errors. However, in the vue docs, I was able to find an answer similar (but not quite) that of #antoni's answer.
In short, just have the following in vue.config.js:
process.env.VUE_APP_VERSION = require('./package.json').version
module.exports = {
// config
}
Docs 2020-10-27:
You can access env variables in your application code:
process.env.VUE_APP_NOT_SECRET_CODE = require('./package.json').version
During build, process.env.VUE_APP_NOT_SECRET_CODE will be replaced by the corresponding value. In the case of VUE_APP_NOT_SECRET_CODE=some_value, it will be replaced by "some_value".
In addition to VUE_APP_* variables, there are also two special variables that will always be available in your app code:
NODE_ENV - this will be one of "development", "production" or "test" depending on the mode the app is running in.
BASE_URL - this corresponds to the publicPath option in vue.config.js and is the base path your app is deployed at.
The answer for this on the official VueJS forum is like so:
chainWebpack: config => {
config
.plugin('define')
.tap(args => {
let v = JSON.stringify(require('./package.json').version)
args[0]['process.env']['VERSION'] = v
return args
})
}
Description
Add this line to your vue.config.js file
module.exports = {
...
chainWebpack: config => {
config
.plugin('define')
.tap(args => {
let v = JSON.stringify(require('./package.json').version)
args[0]['process.env']['VERSION'] = v
return args
})
}
};
Then you can use this in your vue files like so:
version: function () {
return process.env.VERSION
}
A one liner alternative:
//script tag
let packageJsonInfo = require("../../package.json");
//Then you can for example, get the version no
packageJsonInfo.version
I am trying to display git commit hash in my react application using https://www.npmjs.com/package/git-revision-webpack-plugin this webpack plugin that supposedly exposes COMMITHASH variable
In my jsx I included:
<p>{process.COMMITHASH}</p>
and installed plugin in production webpack config as described:
plugins: [
new GitRevisionPlugin()
]
yet generated html returns <p></p>
If you want to access the COMMITHASH variable inside your code, you need to use the Define plugin, just as it says in the documentation here: https://www.npmjs.com/package/git-revision-webpack-plugin#plugin-api
var GitRevisionPlugin = require('git-revision-webpack-plugin');
var webpack = require('webpack');
var gitRevisionPlugin = new GitRevisionPlugin()
module.exports = {
plugins: [
new webpack.DefinePlugin({
'VERSION': JSON.stringify(gitRevisionPlugin.version()),
'COMMITHASH': JSON.stringify(gitRevisionPlugin.commithash()),
})
]
};
Then every occurrence of COMMITHASH "constant" in your code should be replaced by webpack when you build the bundle.