I've got a Webpack server that takes in options, compiles code and returns it as a string (not just a local or pipeline build tool but an actual production service). I need static options/feature flags to decide what code to keep and what feature to use.
As far as I know, I've got 2 options: DefinePlugin and EnvironmentPlugin.
But Webpack I/O is asynchronous and so is my request handling logic.
Is there a chance that if process is asynchronous, 1. request sets "global options", starts compiling, 2. request comes in and sets its "global options" and 1. request compilation continues and uses 2. request options?
Or are defined global/process.env variables only scoped to that specific compilation? Both plugins?
// inside Webpack config
// option 1
new webpack.DefinePlugin({
OPTION1: JSON.stringify(option1),
OPTION2: JSON.stringify(option2),
});
// option 2
process.env.OPTION1 = option1;
process.env.OPTION2 = option2;
new webpack.EnvironmentPlugin(['OPTION1', 'OPTION2']);
// Webpack programmatic API
const compiler = webpack(config);
compiler.run(...);
// in code
// if OPTION1 is falsey, this block
// isn't added to final bundle
if (OPTION1) {
// dynamic import/require fancy feature X
}
process.env is process-wide, so depending on how EnvironmentPlugin is implemented, it either reads environment variables only once (at startup) or whenever it's invoked (which causes the problem you're worried about). So EnvironmentPlugin is not a good choice here.
However, with DefinePlugin the entire configuration appears to be contained within the plugin, so it should be safe. Just make sure to create a new compiler object for every request.
If I understood your question right when you run webpack compilation you want to do it differently based on some condition. Instead of using environmental variables you could actually just pass your condition directly to webpack config.
First make webpack config a function accepting arguments:
module.exports = ({param1, param2, param3}) => {
return {
mode: 'production',
context: path.resolve(__dirname),
entry: `${param1}.js`,
output: {path: param2, filename: `${param3}.js`},
module: {
rules: [
]
},
plugins: [
]
};
};
And second when you call compiler.run just pass those arguments to webpack config:
let webpackConfig = require('./webpack-config-file.js')({
param1: "argument-for-webpack-config",
param2: "argument-for-webpack-config",
param3: "argument-for-webpack-config"
});
const compiler = webpack(webpackConfig);
compiler.run((err, stats) => {
if (err || stats.hasErrors()) {
// show errors
}
// do something
});
Related
Although my dev server is running on localhost:3000, I have set up my host file to point www.mysite.com to localhost. In my JavaScript, I have code like:
import myImage from '../assets/my-image.jpg'
const MyCmp => <img src={myImage} />
Using Webpack's file-loader, it transforms that import into a URL to the hosted image. However, it uses the localhost path to that image, but I'd like it to use the www.mysite.com domain. I looked at both the publicPath and postTransformPublicPath options for file-loader, but those only appear to allow you to modify the part of the path that comes after the domain.
I personally don't like the notion of defining host-information statically in the build output. This is something that should be determined in runtime based on where you actually put your files.
If you are like me then there are two options here.
Both involve you calling a global method that you have defined on i.e. window / global scope.
The purpose of the global method is to resolve the root path (the domain, etc) in runtime.
Define a global method
So lets say you define a method on the global scope somewhere in your startup code like so:
(<any>window).getWebpackBundleRootPath = function (webpackLibraryId) {
if (webpackLibraryId == null) return throw "OOOPS DO SOMETHING HERE!";
// Preferably these variables should be loaded from a config-file of sorts.
if(webpackLibraryId == "yourwebpacklibrary1") return "https://www.yoursite.com/";
// If you have other libraries that are hosted somewhere else, put them here...
return "...some default path for all other libraries...";
};
The next step is to configure webpack to call this global method when it tries to resolve the path.
As I mentioned there are two ways, one that manipulates the output of the webpack and one that is more integrated in webpacks configuration (although only for file-loader but I think it should suffice).
It's worth mentioning that you don't need a global method if you only have one bundle or if you host all your bundles in one place. Then it would be enough to use a global variable instead. It should be quite easy to modify the example below to accommodate this.
First option: configure webpack file-loader to call your method when resolving path
This solution will not require something to be done post build. If this fits your need and covers all scenarios I would go for this option.
Edit your webpack config file
var path = require('path');
let config = {
entry: {
'index': path.join(__dirname, '/public/index.js')
},
output: {
path: path.join(__dirname, '/dist/'),
filename: 'index-bundle.js',
publicPath: 'https://localhost:3000/',
library: 'yourwebpacklibrary1',
...
},
module: {
rules: [{
// Please note that this only defines the resolve behavior for ttf. If you want to resolve other files you need to configure the postTransformPublicPath for those too. This is a big caveat in my opinion and might be a reason for using second option.
test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/,
use: [{
loader: 'file-loader',
options: {
outputPath: 'assets/fonts', // The folder where you store your fonts.
name: '[name].[ext]',
// This is where the magic happens. This lets you override the output of webpack file resolves.
postTransformPublicPath: function (p) {
// Because of the way webpack file-loader works the input from to this method will look something like this: __webpack_public_path__ + "/assets/fonts/yourfont.ttf"
// But we are not interested in using the __webpack_public_path__ variable so lets remove that.
p = p.replace('__webpack_public_path__ + ', '');
// Return a stringified call to our global method and append the relative path to the root path returned.
return `getWebpackBundleRootPath("${config.output.library}") + ${p}`;
}
}
}]
},
},
...
};
module.exports = config;
As you might have noticed in the comments in the webpack config file you need to specify the resolve behavior for each file-loader that you add (if someone knows a better way, please let me know). This is why I still use the second option.
Second option: manipulate the output of the webpack in a postbuild step
Example webpack.config.js file
For completeness sake here is an example of a webpack.config.js file that contains the variables used in the postbuild script.
var path = require('path');
module.exports = {
entry: {
'index': path.join(__dirname, '/public/index.js')
},
output: {
path: path.join(__dirname, '/dist/'),
filename: 'index-bundle.js',
publicPath: 'https://localhost:3000/',
library: 'yourwebpacklibrary1',
...
},
...
}
Create a postbuild.js file
Create a file postbuild.js next to your package.json with the following content:
const fs = require('fs');
// We take the path to the webpack config file as input so that we can read settings from it.
const webpackConfigFile = process.argv[2];
// Read the webpack config file into memory.
const config = require(webpackConfigFile);
// The file to manipulate is the output javascript bundle that webpack produces.
const inputFile = config.output.path + config.output.filename;
// Load the file into memory.
let fileContent = fs.readFileSync(inputFile, 'utf8');
// Replace the default public path with the call to the method. Please note that if you specify a publicPath as '/' or something very common you might end up with a problem so make sure it is unique in the file to avoid other unrelated stuff being replaced as well.
fileContent = fileContent.replace('"' + config.output.publicPath + '"', 'getWebpackBundleRootPath("' + config.output.library + '")');
// Save the manipulated file back to disk.
fs.writeFileSync(inputFile, fileContent, 'utf8');
Call the postbuild.js automatically on build
Next step is to actually call the postbuild.js script after each build.
This can be done in a postscript in package.json like so (in the script section in your package.json):
{
"scripts": {
"build": "webpack",
"postbuild": "node postbuild.js ./webpack.config.js"
}
}
From now on whenever you run the build script it will also run the postbuild script (from npm or yarn, etc).
You can of course also manually run the postbuild.js script manually after each build instead.
but those only appear to allow you to modify the part of the path that comes after the domain.
Not really, you can give it an URL that includes the domain.
In your case, assuming your images are under the assets directory, you will have something like this in your webpack.config.js
...
module: {
rules: [
...
{
test: /\.(png|jpe?g|gif|svg)$/,
use: {
loader: 'file-loader',
options: {
publicPath: 'https://www.example.com/assets',
outputPath: 'assets'
}
}
},
...
]
}
...
I have an array of file names that I want to import. The file names are computed at build time. If I have a single file name, I can do:
new webpack.DefinePlugin({
component_file: '"path/Component"',
})
Then in the source:
require(component_file);
This includes path/Component in the build, as expected.
However, if I try the following, it doesn't work.
new webpack.DefinePlugin({
component_files: ['"path/Component"', '"path/Component2"'],
})
Then in the source:
// component_files is converted object by Webpack.
Object.keys(component_files).forEach(file => require(file));
This causes an error Cannot find module '0'. This makes sense because Webpack just does static analysis, it can't process requires with variables as the argument. Is it possible to do what I'm trying to do?
Rather than use DefinePlugin to define dependencies that are then required within your application, you could include them as entries within your config so that they are included at compile time:
{
entry: [
...component_files,
'app.js'
]
}
For achieving dynamic bundling by environment variables, you have to wrap require statements inside conditional blocks that will be determined as "dead code" or not.
Then, on build time, these dead require statement will be removed, which will result exclusion from the final bundle.
The predicate of each conditional block must be evaluated as a boolean value on build time. This can happen only if the predicate is a plain comparison between 2 primitive values or a pure boolean. For example:
// webpack.config.json
new DefinePlugin({
"process.env.component_a": "true",
"process.env.component_b": "false",
"process.env.component_c": "'true'",
})
// in application:
if (process.env.component_a) {
const a = require('./a') // will be included
}
if (process.env.component_b) {
const b = require('./b') // will be excluded
}
if (process.env.component_c === "true") {
const c = require('./c') // will be included
}
Important Note
Leaving the value undefined is not good enough for excluding a module from the final bundle.
/* THE WRONG WAY */
// webpack.config.json
new DefinePlugin({
"process.env.component_a": "true",
})
// in application:
if (process.env.component_a) {
const a = require('./a') // will be included
}
if (process.env.component_b) {
// even though this block is unreachable, b will be included in the bundle!
const b = require('./b')
}
I have a rather complex scenario.
We are building a desktop application with React which is wrapped with Electron, Webpack takes care of the Babel transpilation and chunking.
The application receives configuration data from a cms.
Part of the configuration may be a javascript class that needs to override one that resides in the application. The JS code as specified in the CMS will be vanilla Javascript code (ES6/7/8 same as what we use for the application)
I see 2 problems here:
How to transpile just this one class and
How to replace it runtime in the application
Is this even possible?
Regards
If with "The application receives configuration data from a cms." you mean runtime data, then, because Webpack acts at compile time, it cannot help you to transpile/replace your code (Runtime vs Compile time).
if your data from a CMS can be fetched at compile time, then, notice that you can return a promise from webpack.config.js.
module.exports = function webpackConfig(env) {
const configs = {
context: __dirname,
plugins: []
// etc...
};
return CMS
.fetchConfig()
.then(cmsConfigs => {
const vars = {
replaceClass: JSON.stringify(cmsConfigs.classINeed.toString())
};
configs.plugins.push(new webpack.DefinePlugin(vars));
return configs;
})
;
}
For example we have multiple entries:
entry: {
main: "./app/entry.js",
view: "./app/entry.js",
},
how to pass current name (main or view) into entry.js?
Ideal solution will be something like this:
new webpack.DefinePlugin({
'_ENTRY_': '[name]'
}),
like other config options can have, but of course DefinePlugin dont know how to process this...
If you're running the code inside Node.js then you can use __filename
and __dirname.
Webpack can mock them for non-Node.js environments.
Please see Webpack Node configuration
node: {
__filename: true,
__dirname: true
}
In this case you can use __filename and __dirname globals as usually done in Node.js enviroments.
if(__filename.indexOf('index.js') != -1) {
// Code here
}
node.__filename
Default: "mock"
Options:
true: The filename of the input file relative to the context option.
false: The regular Node.js __filename behavior. The filename of the output file when run in a Node.js environment.
"mock": The fixed value "index.js".
For example, I use AMD definition in my project, and use "webpack" for project building. It's possible to create some loader which will take a dependencies in array format?
define(
[
'mySuperLoader![./path/dependency-1, ./path/dependency-2, ...]'
],
function() {
// ... some logic here
}
)
Project example: gitHub
If you want to port the load-plugin's behavior to webpack, you need to do this:
1. Create a custom resolver
This is because mySuperLoader![./path/dependency-1, ./path/dependency-2, ...] does not point to a single file. When webpack tries to load a file, it first:
resolves the file path
loads the file content
matches and resolves all loaders
passes the file content to the loader chain
Since [./path/dependency-1, ./path/dependency-2, ...] is not a proper file path, there is some work to do. It is even not a proper JSON.
So, our first goal is to turn this into mySuperLoader!some/random/file?["./path/dependency-1", "./path/dependency-2", ...]. This is usually done by creating a custom resolver:
// webpack.config.js
var customResolverPlugin = {
apply: function (resolver) {
resolver.plugin("resolve", function (context, request) {
const matchLoadRequest = /^\[(.+)]$/.exec(request.path);
if (matchLoadRequest) {
request.query = '?' + JSON.stringify(
matchLoadRequest[1]
.split(", ")
);
request.path = __filename;
}
});
}
};
module.exports = {
...
plugins: [
{
apply: function (compiler) {
compiler.resolvers.normal.apply(customResolverPlugin);
}
}
]
};
Notice request.path = __filename;? We just need to give webpack an existing file so that it does not throw an error. We will generate all the content anyway. Probably not the most elegant solution, but it works.
2. Create our own load-loader (yeah!)
// loadLoader.js
const path = require("path");
function loadLoader() {
return JSON.parse(this.request.match(/\?(.+?)$/)[1])
.map(module =>
`exports['${path.basename(module, '.js')}'] = require('${module}');`
)
.join('\n');
}
module.exports = loadLoader;
This loader parses the request's query we have re-written with our custom resolver and creates a CommonJS module that looks like this
exports['dependency-1'] = require('path/to/dependency-1');
exports['dependency-2'] = require('path/to/dependency-2');
3. Alias our own load-loader
// webpack.config.js
...
resolveLoader: {
alias: {
load: require.resolve('./loadLoader.js')
}
},
4. Configure root
Since /path/to/dependency-1 is root-relative, we need to add the root to the webpack config
// webpack.config.js
resolve: {
root: '/absolute/path/to/root' // usually just __dirname
},
This is neither a beautiful nor an ideal solution, but should work as a makeshift until you've ported your modules.
I don't think that you should use a loader for that. Why don't you just write:
require("./path/dependency-1");
require("./path/dependency-2");
require("./path/dependency-3");
It accomplishes the same thing, is much more expressive and requires no extra code/loader/hack/configuration.
If you're still not satisfied, you might be interested in webpack contexts which allow you to require a bulk of files that match a given filter. So, if you write
require("./template/" + name + ".jade");
webpack includes all modules that could be accessed by this expression without accessing parent directories. It's basically the same like writing
require("./table.jade");
require("./table-row.jade");
require("./directory/folder.jade")
You can also create contexts manually like this
var myRequire = require.context(
"./template", // search inside this directory
false, // false excludes sub-directories
/\.jade$/ // use this regex to filter files
);
var table = myRequire("./table.jade");