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

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...

Related

module.exports not available on dependency (import)

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?

How to use the TypeScript Compiler API to type-check modules imported using `require()`?

I am using TypeChecker from the TypeScript Compiler API in order to extract (inferred) type information for every node in the AST of my program. In particular, I try to find out the return values from imported module functions such as:
var vec3 = require('gl-matrix/vec3')
var myVec = vec3.fromValues(1, 2, 3) // $ExpectedType vec3
This works well for modules that were imported using the import { … } from '…' statement, but unfortunately, modules that were imported using require() like above are not recognized correctly, I only receive the type any for them. However, I have set both compiler options allowJs and checkJs.
Why are the types form require()d modules not inferred correctly? VS Code (which AFAIK relies on the same API?) is able to infer the types from require() statements as well, so I'd guess that in general, tsc is able of handling them. Are there any other compiler options that I need to set differently? Or is this indeed not supported and I need to use some other package for this?
Here is a minimum script to reproduce, I have also put it on repl.it together with two example files: https://replit.com/#LinqLover/typecheck-js
var ts = require("typescript")
// Run `node index.js sample-import.js`` to see the working TypeScript analysis
const files = process.argv[1] != "/run_dir/interp.js" ? process.argv.slice(2) : ["sample-require.js"]
console.log(`Analyzing ${files}:`)
const program = ts.createProgram(files, {
target: ts.ScriptTarget.ES5,
module: ts.ModuleKind.CommonJS,
allowJs: true,
checkJs: true
})
const checker = program.getTypeChecker()
for (const sourceFile of program.getSourceFiles()) {
if (!sourceFile.isDeclarationFile) {
ts.forEachChild(sourceFile, visit)
}
}
function visit(node) {
try {
const type = checker.getTypeAtLocation(node)
console.log(checker.typeToString(type))
} catch (e) {
// EAFP
}
ts.forEachChild(node, visit)
}
Thank you so much in advance!
For follow-up, this turned out to be an issue with the type definitions for gl-matrix. I should better have tried out multiple packages before suspecting that the TypeScript engine itself could be broken ...
gl-matrix issue: https://github.com/toji/gl-matrix/issues/429

Dynamically require multiple files using Webpack's DefinePlugin?

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

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

Environment Variables in an isomorphic JS app: Webpack find & replace?

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.

Categories

Resources