Dynamically require multiple files using Webpack's DefinePlugin? - javascript

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')
}

Related

Detect use of non-existent classNames in CSS Modules/React

In my react project if I use some non-existent className from css modules file,
// mycss.modules.scss
.thing { color: red }
// index.jsx
import styles from mycss.modules.scss
<div className={styles.otherThing}>Some div</div>
// Browser would return:
<div>Some div</div>
it quietly fails without letting me know that this class does not exist. How I can check if this class name exist or not and throw an error. Would be great to receive an error during build time, when saving file.
If you are open to typescript solution, I found one TS plugin for you.
typescript-plugin-css-modules
It can populate the styles object with type information for available keys.
You don't have to switch whole project to typescript, you can add // #ts-check directive at the top of this file to enable the TS engine design time check.
Unless you want to put forth a pull request to add something like a strict mode option to the webpack loader itself, i dont think theres much you can do since its just a base object. An easy alternative is to just do styles.styleName.toString(), this way if styleName is undefined, it will throw an error.
Actually it is possible in javascript code. But I think that className exist check is not good idea.
document.styleSheets[].rules[].selectorText
original link How can you determine if a css class exists with Javascript?
add this function to the top:
// index.jsx
import styles from mycss.modules.scss
function strictStyles (clsName){
if(styles[clsName]){
return styles[clsName]
}else{
throw "CSS class doesn't exist";
}
}
...
<div className={strictStyles(otherThing)}>Some div</div>
...
NOTE: This solution does not require you to change any of your code, just add the loader and it should work out of the box. Please note the caveat about production builds at the end, or check the source for full instructions at Github.
I have created a Webpack loader that works with CSS/LESS/Other CSS module loaders.
The full source and readme can be found on GitHub.
For those who just want to add the loader to their project, it can be used like this:
Add this webpack loader source file somewhere, e.g /webpack/loaders/css-module-proxy.js
/**
* A CSS/LESS/Style module loader that prepends a proxy in non-production builds.
*
* The proxy checks if the loaded style module actually contains the style we are trying to fetch.
* If it doesn't exist (its accessor returns undefined), we crash on debug (non-production) builds!
*
* Inspired by https://github.com/royriojas/css-local-loader
*/
module.exports = function cssLocalLoader(source, map) {
this.cacheable();
if (process.env.NODE_ENV !== "production") {
// noMatch:
// Makes sure that any access prefixed with underscore are filtered out
// otherwise it will crash at runtime when Webpack is probing the locals export.
// toJsonMatch:
// Makes sure that toJSON access on the locals object gets proxied to the correct
// toJSON function.
const requireWrapper = `
// If the access matches this regexp, skip it
const oldLocals = exports.locals;
const noMatch = /^[_]+/;
const toJsonMatch = /^toJSON$/;
const proxy = new Proxy(oldLocals, {
get: function(target, name) {
if (noMatch.test(name)) {
return undefined;
}
if (toJsonMatch.test(name)) {
return oldLocals.toJSON;
}
const clz = target[name];
if (clz === undefined) {
throw new Error("Error: LESS / CSS class named \\"" + name + "\\" does not exist");
}
return clz;
}
});
exports.locals = proxy;
`;
const newSource = `${source}\n\n${requireWrapper}`;
this.callback(null, newSource, map);
} else {
this.callback(null, source, map);
}
};
And then use it from your webpack config, example below is for LESS:
{
test: /\.module\.less$/,
use: [
{ loader: path.resolve("webpack/loaders/css-module-proxy.js") },
{
loader: "css-loader",
options: {
modules: true,
importLoaders: 1,
localIdentName: "[name]__[local]__[hash:base64:5]",
},
},
{ loader: "less-loader" },
],
},
Don't forget to build your release code with NODE_ENV=production or it may crash when a user visits your site...

Webpack global/process.env variables scope?

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
});

Webpack multiple bundle based on configuration

I've worked with Webpack a little. I know who to do multiple bundles depends on multiple entry points, but I have a new situation.
One of our projects has a single codebase for 3 different countries (3 different URL and different URL structure). We have set of common codes and codes for C1, C2, C3. I want to run a single build and compile the ES6 code in 2 different files, like
app-common.js, c1-common.js, c2-common.js, c3-common.j2
Or
c1-app.js, c2-app.js, c3-app.js which will have common and specific js code baesd on configuration, like which files I want to bind with which bundle.
Is there any existing way to do it or how this can be achieved?
You could create a single point-of-entry file for your localized code and do conditional require statements based on a custom environment variable. Then set that up in your webpack config:
new webpack.DefinePlugin({
'process.env.COUNTRY_CODE': 'fr'
}),
Then in your code:
let i18n_config;
const country = process.env.COUNTRY_CODE || 'en';
if (country === 'en') {
i18n_config = require("./i18n/en.js");
}
if (country === 'fr') {
i18n_config = require("./i18n/fr.js");
}
You can go even further and use an aggressive minifier to remove dead code:
const isEN = country === 'en';
const isFR = country === 'fr';
if (isEN) {
i18n_config = require("./i18n/en.js");
}
if (isFR) {
i18n_config = require("./i18n/fr.js");
}
// ... and so on
Your if statements will compile to if (true) {...} or if (false) {...}. The false block will be considered dead-code and will be removed by the minifier.

How to use google-closure-compiler-js for a node.js app without gulp/grunt/webpack?

The docs don't have any examples of using this on its own but they do say this:
Unless you're using the Gulp or Webpack plugins, you'll need to specify code via flags. Both jsCode and externs accept an array containing objects in the form {src, path, sourceMap}. Using path, you can construct a virtual filesystem for use with ES6 or CommonJS imports—although for CommonJS, be sure to set processCommonJsModules: true.
I've created a "compile.js" file based on the docs:
const compile = require('google-closure-compiler-js').compile;
const flags = {
jsCode: [{path: './server/server.js'}],
processCommonJsModules: true
};
const out = compile(flags);
console.info(out.compiledCode);
In my "./server/server.js" file, I put a console.log but it doesn't output. Not sure where to go from here...
Borrowing from icidasset/quotes.
It appears, to me, that path is not intended to be used as you are using it.
Quote:
Using path, you can construct a virtual filesystem for use with ES6 or CommonJS imports—although for CommonJS, be sure to set processCommonJsModules: true.
So instead you must expand your own sources, something webpack and gulp must be doing for you when you go that route.
files=['./server/server.js']
files.map(f => {
const out = compile({
jsCode: [{ src: f.content }],
assumeFunctionWrapper: true,
languageIn: 'ECMASCRIPT5'
});
return out;
}

Webpack: How can I create a loader for "webpack" which takes an array of dependencies?

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");

Categories

Resources