Open phegman opened 6 years ago
@phegman Seems related to https://github.com/gajus/babel-plugin-react-css-modules/issues/154
@hinok I really appreciate the response! Unfortunately that doesn't appear to be the issue as cacheDirectory
is false
by default. I tried explicitly setting it to cacheDirectory
to false
and still no luck. The issue isn't a total deal breaker, it just ends up being a bit annoying to have to re-save the JS file after adding the class to the CSS file. Overall though it is still SO much better than not using this babel plugin so thanks for an amazing package! I will keep playing around with it in my free time and see if I can come up with a solution.
I'm seeing the same issue:
It would be great with an option to set the class name to the element no matter if there exist a corresponding class name in the css file or not.
Since this is a babel plugin, it could not back-notify webpack to recompile javascript, to address this issue, I've written a loader to establish the dependency between js and less, a very simple one:
import {promisify} from 'util';
import * as babel from '@babel/core';
import traverse from '@babel/traverse';
import * as webpack from 'webpack';
import {getParseOnlyBabelConfig} from '../../config';
import {BabelConfigOptions} from '../../../types';
type StyleNameDependencies = [boolean, string[]];
type Resolve = (request: string) => Promise<string>;
type CollectCallback = (result: StyleNameDependencies) => void;
const babelConfig = getParseOnlyBabelConfig({browserSupport: '', usage: 'build'});
const parse = (source: string, babelConfig: babel.TransformOptions): Promise<babel.ParseResult | null> => {
const execute = (resolve: (ast: babel.ParseResult | null) => void) => babel.parse(
source,
babelConfig,
(error, ast) => resolve(error ? null : ast)
);
return new Promise(execute);
};
const collectDependencies = async (source: string, filename: string, resolve: Resolve, callback: CollectCallback) => {
const ast = await parse(source, {...babelConfig, filename});
const result: StyleNameDependencies = [false, []];
if (!ast) {
callback(result);
return;
}
traverse(
ast,
{
// tslint:disable-next-line function-name
JSXIdentifier({node, parent}) {
if (parent.type === 'JSXAttribute' && node.name === 'styleName') {
result[0] = true;
}
},
// tslint:disable-next-line function-name
ImportDeclaration({node}) {
const value = node.source.value;
// Only collect sass imports
if (value.endsWith('.scss')) {
result[1].push(value);
}
},
}
);
const dependencies = await Promise.all(result[1].map(resolve));
result[1] = dependencies;
callback(result);
};
const loader: webpack.loader.Loader = function styleNameLoader(content, sourceMap) {
if (this.cacheable) {
this.cacheable();
}
this.async();
const resolve = promisify(this.resolve);
collectDependencies(
content.toString(),
this.resourcePath,
request => resolve(this.context, request),
([hasStyleName, dependencies]) => {
if (hasStyleName) {
dependencies.forEach(this.addDependency);
}
this.callback(null, content, sourceMap);
}
);
};
export default loader;
Put this loader before babel-loader
then you have everything you want.
This can slow down your webpack build, so just include this loader in dev-server mode, exclude it in build
@otakustay Where is the getParseOnlyBabelConfig
function coming from ? what does it do?
@otakustay Where is the
getParseOnlyBabelConfig
function coming from ? what does it do?
It's simply for parsing js syntax, for us it's preset-typescript
and preset-react
, or just load it from babel.config.js
Can you please elaborate further? You are saying I can just import the babel.config.json and assign that to babelConfig?
Can you please elaborate further? You are saying I can just import the babel.config.json and assign that to babelConfig?
Yes, import babel.config.json
and assign to babelConfig
variable, however this is an example I made years ago, webpack is in V4 at that time, so it may still work and may not.
Holy crap this worked! <3. Update: For folks who are using commonjs and JS instead of TS. Works with webpack v5.
/**
* Courtesy to original solution.
* https://github.com/gajus/babel-plugin-react-css-modules/issues/200#issuecomment-472248349
*
* This plugin sets up a dependency between JS and CSS files & helps babel
* notify webpack to trigger the HMR
*/
const { promisify } = require('util');
const babel = require('@babel/core');
const traverse = require('@babel/traverse').default;
const babelConfig = require('./babel.config');
const parse = (source, config) => {
const execute = (resolve) =>
babel.parse(source, config, (error, ast) => resolve(error ? null : ast));
return new Promise(execute);
};
const collectDependencies = async (source, filename, resolve, callback) => {
const ast = await parse(source, { ...babelConfig, filename });
const result = [false, []];
if (!ast) {
callback(result);
return;
}
traverse(ast, {
JSXIdentifier({ node, parent }) {
if (parent.type === 'JSXAttribute' && node.name === 'styleName') {
result[0] = true;
}
},
ImportDeclaration({ node }) {
const { value } = node.source;
// Only collect css imports
if (value.endsWith('.css')) {
result[1].push(value);
}
},
});
const dependencies = await Promise.all(result[1].map(resolve));
result[1] = dependencies;
callback(result);
};
const loader = function styleNameLoader(content, sourceMap) {
if (this.cacheable) {
this.cacheable();
}
this.async();
const resolve = promisify(this.resolve);
collectDependencies(
content.toString(),
this.resourcePath,
(request) => resolve(this.context, request),
([hasStyleName, dependencies]) => {
if (hasStyleName) {
dependencies.forEach(this.addDependency);
}
this.callback(null, content, sourceMap);
},
);
};
module.exports = loader;
// Inside the rule you use with babel-loader.
{
test: /\.(ts|js)x?$/,
include: [
path.resolve(__dirname, 'src/'),
],
use: [
{
loader: 'babel-loader',
},
// We only want this on dev.
...(env.ENV === 'production'
? [
{
loader: require.resolve('./CSSModulesPlugin.js'),
},
]
: []),
],
},
I have this all setup and working and HMR will work except for in one use case. If you first add a class to
styleName
and then add the class to your css file HMR will not work. You need to go back to the JS file and re-save that file. This is the case no matter whathandleMissingStyleName
is set to. I have tried changing a number of settings in my Webpack config, but no luck. Any ideas are greatly appreciated. This is my Webpack config if that helps: