Recently upgraded a project to Gatsby 3 whose dependency is Webpack 5. In one of the .tsx classes, an import to the library countdown is done. The import returns an empty object every time, {}.
Looking at the code from "countdown" library I see they export the module like this:
/*global window */
var module;
var countdown = (
function(module) {
'use strict';
...
if (module && module.exports) {
module.exports = countdown;
} else if (typeof window.define === 'function' && typeof window.define.amd !== 'undefined') {
window.define('countdown', [], function() {
return countdown;
});
}
return countdown;
})(module);
Using console.log inside the library I see that module.exports is actually undefined, it seems that var module; is overriding whatever value Node makes available when the import is called. To test the hypothesis out I removed the var module; and removed it as an argument to the function, and everything worked. Of course, that's not the answer to my problem since this is a dependency library, I have no right to touch its code.
I can't figure out what the upgrade to Webpack 5 could have broken to not make module.exports available to countdown.js even though they declare the variable.
I looked at webpack updates and tried to tell it to use commonjs or import-loader on countdown.js and pass it "module" (different test cases have been commented out):
module: {
rules: [
{
test: /\countdown.js?$/,
use: {
loader: 'babel-loader',
},
// loader: "imports-loader",
// options: {
// syntax: "default",
// type: "commonjs"
// },
// use: [
// {
// loader: "imports-loader",
// options: {
// thisArg: "module"
// },
// },
// ],
},
],
}
None of that work. I can't quite figure out what changed in Webpack 5 to cause countdown's way of exporting the library to break.
Any ideas?
To make it clear: the library countdown is not mine, and when using previous webpack version it worked great when importing and using it.
Turns out the package has been updated but version update hasn't been propagated. There's a request soliciting that.
In the meantime a colleague had a solution, thought I'd share it here in case anyone can use it until version is updated. In summary it modifies the script from the library after installation is done, it removes the declared global variable that is overriding the one that Node is supposed to pass.
In a file called yarn-postinstall.js (or whatever you please):
fixCountdownLib();
function fixCountdownLib() {
const fs = require("fs");
const countdownLibPath = __dirname + "/node_modules/countdown/countdown.js";
const countdownLibCode = fs.readFileSync(countdownLibPath).toString();
const fixedCountdownLibCode = countdownLibCode.replace("var module;", "");
fs.writeFileSync(countdownLibPath, fixedCountdownLibCode);
}
Then update package.json scripts to point to that file.
"scripts": {
"postinstall": "node yarn-postinstall.js"
},
The module variable isn't an object, and doesn't have a property exports.
Changing var module; to var module = {exports: {}} should work. But I'm not sure you should be checking for module.exports, since it's supposed to be undefined anyways, I guess?
Related
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...
I've been using gulp for a while now and know how to import another node module, e.g.
var sass = require('gulp-sass');
That's fine, but my gulpfile is filling up with code that I'd like to move into a separate file and "require". Specifically I am writing a postcss plugin, which I already have working when declared as a function inside of the gulpfile. My question is how to put my function in an external file and require it like I do a node module. Do I need to "export" the function in the file being required? Do I need to use ES6 modules or something like that?
As an aside, I realise that if i was doing this probably I would either (A) turn this into a proper node module and put it on a private NPM repository, but that seems unnecessary, or (B) turn it into a proper gulp plugin, but that would require learning how to author a gulp plugin and learning about streams and stuff. Both of these are probably better but would take more time so I've decided to just keep the function simple and local for now.
First create a new js file (here ./lib/myModule.js):
//./lib/myModule.js
module.exports = {
fn1: function() { /**/ },
fn2: function() { /**/ },
}
You could also pass some arguments to your module:
// ./lib/myAwesomeModule.js
var fn1 = function() {
}
module.exports = function(args) {
fn1: fn1,
fn2: function() {
// do something with the args variable
},
}
Then require it in your gulpfile:
//gulpfile.js
var myModule = require('./lib/myModule')
// Note: here you required and call the function with some parameters
var myAwesomeModule = require('./lib/myAwesomeModule')({
super: "duper",
env: "development"
});
// you could also have done
/*
var myAwesomeModuleRequire = require('./lib/myAwesomeModule')
var myAwesomeModule = myAwesomeModuleRequire({
super: "duper",
env: "development"
});
*/
gulp.task('test', function() {
gulp.src()
.pipe(myModule.fn1)
.pipe(myAwesomeModule.fn1)
.gulp.dest()
}
First, you have to add export default <nameOfYourFile> at the end of your file
Then to use it, write import gulp from 'gulp'
If you have an error message, install babel-core and babel-preset-es2015 with NPM, and add a preset "presets": ["es2015"] in your .babelrc config file.
I fix my problem by install:
npm i babel-plugin-add-module-exports
Then i add "plugins": [["add-module-exports"]] to the .babelrc
I am trying to export a es6 module in header.js:
export default {
setHeaderHighlight: function (index) {
// do somethings
}
};
And import it in index.js:
import header from "./header.js"
$(function () {
header.setHeaderHighlight(0);
});
Then transformation comes out in index.bundle.js:
var _header = __webpack_require__(129);
var _header2 = _interopRequireDefault(_header);
function _interopRequireDefault(obj) {
return obj && obj.__esModule ? obj : { default: obj }; // crash here
}
So here is the problem, ie8 will rise a Expected identifier Exception at { default: obj }, but everythings is ok >=ie9.
Is there something i can do with this?
By default, Babel 6.x requires you to enable an explicit set of transformations. The standard es2015 preset converts ES6 to ES5, however IE8 is not ES5-compatible. In this case, if you look at the plugins list, you will see
transform-es3-member-expression-literals
transform-es3-property-literals
These will convert your properties to be compatible with IE8. Generally in Babel 6.x you'd do this by passing those names as part of your plugins array, alongside the presets array and install the transforms via
npm install --save-dev babel-plugin-transform-es3-member-expression-literals babel-plugin-transform-es3-property-literals
I use webpack + es3ify-loader as workaround.
loaders: {
{
test: /\.jsx?$/,
exclude: /node_modules/,
loaders: ['es3ify', `babel?${JSON.stringify(babelQuery)}`],
},
}
I also have the problem, and I wrote a webpack plugin to resolve it. I didn't really know if there is a nicer way to handle it, but it works.
The module in node_modules also works well.
I'm using webpack to bundle an isomorphic JS app (based on this example) so that the browser runs the same code as the server. Everything is running smoothly except I have a config.js with some settings which are pulled in from environment variables on the server:
module.exports = {
servers:
auth: process.env.AUTH_SERVER_URL,
content: process.env.CONTENT_SERVER_URL
}
}
On the server this is grand, but when webpack renders this for the client process is empty and this doesn't work.
I'm hoping there's a kind of 'find and replace' webpack plugin that will replace them with their content in that file alone?
"…config.js content…".replace(/process\.env\.([a-z0-9_]+)/, function(match, varName) {
return process.env[varName];
})
Note that using the DefinePlugin as suggested in the accepted answer is potentially a dangerous action as it completely exposes process.env. As Tobias commented above there's actually a plugin EnvironmentPlugin that does exactly this with an added whitelisting ability, using DefinePlugin internally.
In your webpack.config.js:
{
plugins: [
new webpack.EnvironmentPlugin([
'NODE_ENV',
'WHITELISTED_ENVIRONMENT_VARIABLE'
])
]
}
In your webpack.config.js,
use the following preLoaders (or postLoaders),
module: {
preLoaders: [
{ test: /\.js$/, loader: "transform?envify" },
]
}
Another way using the webpack.DefinePlugin:
plugins: [
new DefinePlugin({
'process.env': Object.keys(process.env).reduce(function(o, k) {
o[k] = JSON.stringify(process.env[k]);
return o;
}, {})
})
]
NOTE: The old method using envify-loader was deprecated:
DEPRECATED: use transform-loader + envify instead.
Yeah; looks like envify-loader was the easy solution.
I just added the following to my webpack loaders:
{
test: /config\.js$/, loader: "envify-loader"
}
And the config.js (and only that file) is modified to include any referenced environment variables statically :)
I needed a way to use the env variables set on the machine that is running the code, no the env variables of the machine building the app.
I do not see a solution for this yet. This is what I did.
In publicEnv.js:
// List of the env variables you want to use on the client. Careful on what you put here!
const publicEnv = [
'API_URL',
'FACEBOOK_APP_ID',
'GA_ID'
];
const isBrowser = typeof window !== 'undefined';
const base = (isBrowser ? window.__ENV__ : process.env) || {};
const env = {};
for (const v of publicEnv) {
env[v] = base[v];
}
export default env;
In the HTML template file of the page I have:
import publicEnv from 'publicEnv.js';
...
<script>
window.__ENV__ = ${stringify(publicEnv)};
// Other things you need here...
window.__INITIAL_STATE__ = ${stringify(initialState)};
</script>
So now I can get the value of the env variable on both frontend and backend with:
import publicEnv from 'publicEnv.js';
...
console.log("Google Analytic code is", publicEnv.GA_ID);
I hope it can help.
I have some things for development - e.g mocks which I would like to not bloat my distributed build file with.
In RequireJS you can pass a config in a plugin file and conditonally require things in based on that.
For webpack there doesn't seem to be a way of doing this. Firstly to create a runtime config for an environment I have used resolve.alias to repoint a require depending on the environment, e.g:
// All settings.
var all = {
fish: 'salmon'
};
// `envsettings` is an alias resolved at build time.
module.exports = Object.assign(all, require('envsettings'));
Then when creating the webpack config I can dynamically assign which file envsettings points to (i.e. webpackConfig.resolve.alias.envsettings = './' + env).
However I would like to do something like:
if (settings.mock) {
// Short-circuit ajax calls.
// Require in all the mock modules.
}
But obviously I don't want to build in those mock files if the environment isn't mock.
I could possibly manually repoint all those requires to a stub file using resolve.alias again - but is there a way that feels less hacky?
Any ideas how I can do that? Thanks.
You can use the define plugin.
I use it by doing something as simple as this in your webpack build file where env is the path to a file that exports an object of settings:
// Webpack build config
plugins: [
new webpack.DefinePlugin({
ENV: require(path.join(__dirname, './path-to-env-files/', env))
})
]
// Settings file located at `path-to-env-files/dev.js`
module.exports = { debug: true };
and then this in your code
if (ENV.debug) {
console.log('Yo!');
}
It will strip this code out of your build file if the condition is false. You can see a working Webpack build example here.
Not sure why the "webpack.DefinePlugin" answer is the top one everywhere for defining Environment based imports/requires.
The problem with that approach is that you are still delivering all those modules to the client -> check with webpack-bundle-analyezer for instance. And not reducing your bundle.js's size at all :)
So what really works well and much more logical is: NormalModuleReplacementPlugin
So rather than do a on_client conditional require -> just not include not needed files to the bundle in the first place
Hope that helps
Use ifdef-loader. In your source files you can do stuff like
/// #if ENV === 'production'
console.log('production!');
/// #endif
The relevant webpack configuration is
const preprocessor = {
ENV: process.env.NODE_ENV || 'development',
};
const ifdef_query = require('querystring').encode({ json: JSON.stringify(preprocessor) });
const config = {
// ...
module: {
rules: [
// ...
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: `ifdef-loader?${ifdef_query}`,
},
},
],
},
// ...
};
I ended up using something similar to Matt Derrick' Answer, but was worried about two points:
The complete config is injected every time I use ENV (Which is bad for large configs).
I have to define multiple entry points because require(env) points to different files.
What I came up with is a simple composer which builds a config object and injects it to a config module.
Here is the file structure, Iam using for this:
config/
└── main.js
└── dev.js
└── production.js
src/
└── app.js
└── config.js
└── ...
webpack.config.js
The main.js holds all default config stuff:
// main.js
const mainConfig = {
apiEndPoint: 'https://api.example.com',
...
}
module.exports = mainConfig;
The dev.js and production.js only hold config stuff which overrides the main config:
// dev.js
const devConfig = {
apiEndPoint: 'http://localhost:4000'
}
module.exports = devConfig;
The important part is the webpack.config.js which composes the config and uses the DefinePlugin to generate a environment variable __APP_CONFIG__ which holds the composed config object:
const argv = require('yargs').argv;
const _ = require('lodash');
const webpack = require('webpack');
// Import all app configs
const appConfig = require('./config/main');
const appConfigDev = require('./config/dev');
const appConfigProduction = require('./config/production');
const ENV = argv.env || 'dev';
function composeConfig(env) {
if (env === 'dev') {
return _.merge({}, appConfig, appConfigDev);
}
if (env === 'production') {
return _.merge({}, appConfig, appConfigProduction);
}
}
// Webpack config object
module.exports = {
entry: './src/app.js',
...
plugins: [
new webpack.DefinePlugin({
__APP_CONFIG__: JSON.stringify(composeConfig(ENV))
})
]
};
The last step is now the config.js, it looks like this (Using es6 import export syntax here because its under webpack):
const config = __APP_CONFIG__;
export default config;
In your app.js you could now use import config from './config'; to get the config object.
another way is using a JS file as a proxy, and let that file load the module of interest in commonjs, and export it as es2015 module, like this:
// file: myModule.dev.js
module.exports = "this is in dev"
// file: myModule.prod.js
module.exports = "this is in prod"
// file: myModule.js
let loadedModule
if(WEBPACK_IS_DEVELOPMENT){
loadedModule = require('./myModule.dev.js')
}else{
loadedModule = require('./myModule.prod.js')
}
export const myString = loadedModule
Then you can use ES2015 module in your app normally:
// myApp.js
import { myString } from './store/myModule.js'
myString // <- "this is in dev"
Faced with the same problem as the OP and required, because of licensing, not to include certain code in certain builds, I adopted the webpack-conditional-loader as follows:
In my build command I set an environment variable appropriately for my build. For example 'demo' in package.json:
...
"scripts": {
...
"buildDemo": "./node_modules/.bin/webpack --config webpack.config/demo.js --env.demo --progress --colors",
...
The confusing bit that is missing from the documentation I read is that I have to make this visible throughout the build processing by ensuring my env variable gets injected into the process global thus in my webpack.config/demo.js:
/* The demo includes project/reports action to access placeholder graphs.
This is achieved by using the webpack-conditional-loader process.env.demo === true
*/
const config = require('./production.js');
config.optimization = {...(config.optimization || {}), minimize: false};
module.exports = env => {
process.env = {...(process.env || {}), ...env};
return config};
With this in place, I can conditionally exclude anything, ensuring that any related code is properly shaken out of the resulting JavaScript. For example in my routes.js the demo content is kept out of other builds thus:
...
// #if process.env.demo
import Reports from 'components/model/project/reports';
// #endif
...
const routeMap = [
...
// #if process.env.demo
{path: "/project/reports/:id", component: Reports},
// #endif
...
This works with webpack 4.29.6.
I've struggled with setting env in my webpack configs. What I usually want is to set env so that it can be reached inside webpack.config.js, postcss.config.js and inside the entry point application itself (index.js usually). I hope that my findings can help someone.
The solution that I've come up with is to pass in --env production or --env development, and then set mode inside webpack.config.js.
However, that doesn't help me with making env accessible where I want it (see above), so I also need to set process.env.NODE_ENV explicitly, as recommended here.
Most relevant part that I have in webpack.config.js follow below.
...
module.exports = mode => {
process.env.NODE_ENV = mode;
if (mode === "production") {
return merge(commonConfig, productionConfig, { mode });
}
return merge(commonConfig, developmentConfig, { mode });
};
Use envirnment variables to create dev and prod deployments:
https://webpack.js.org/guides/environment-variables/
I use string-replace-loader to get rid of an unnecessary import from the production build, and it works as expected: the bundle size becomes less, and a module for development purposes (redux-logger) is completely removed from it. Here is the simplified code:
In the file webpack.config.js:
rules: [
// ... ,
!env.dev && {
test: /src\/store\/index\.js$/,
loader: 'string-replace-loader',
options: {
search: /import.+createLogger.+from.+redux-logger.+;/,
replace: 'const createLogger = null;',
}
}
].filter(Boolean)
In the file src/store/index.js:
// in prod this import declaration is substituted by `const createLogger = null`:
import { createLogger } from 'redux-logger';
// ...
export const store = configureStore({
reducer: persistedReducer,
middleware: createLogger ? [createLogger()] : [],
devTools: !!createLogger
});
While this is not the best solution, it may work for some of your needs. If you want to run different code in node and browser using this worked for me:
if (typeof window !== 'undefined')
return
}
//run node only code now