Skip to content

Commit 3c4e6d0

Browse files
IanVSgajus
authored andcommitted
feat: add handleMissingStyleName option (#112)
* Add .eslintignore for test fixtures The double quotes that the plugin adds go against the linting config of the project, and editors that auto-fix linting rules will try to convert quotes in `expected.js` files to single quotes, causing tests to fail. It seems safest to ignore all fixture files from linting. * Allow createObjectExpression to take a boolean * Allow getClassName to take an options argument The only option is to silence styleName errors, for example in production. If `silenceStyleNameErrors` is true, the styleNames which cannot be found will simply be removed from the resulting classNames, matching the behavior of `react-css-modules`. This commit alone will not result in a change in behavior, the next steps will be to pass in the option from the babel-plugin. * Add `silenceStyleNameErrors` to options schema * Pass silenceStyleNameErrors to getClassName This hooks up the options set by the user into getClassName, so that it has an effect. However, getClassName can also be used at runtime, which this commit does not address. * Provide silenceStyleNameErrors at runtime This commit adds the options object for getClassName iif silenceStyleNameErrors is true. By not always including the options object, it helps to save a few characters in the generated code. * Restructure Configuration docs Seeing the flow types first thing in the Configuration section was confusing to new users, so this adds a brief example of how to set babel plugin options, and moves the flow types underneath the table of options. * Add silenceStyleNameErrors to README * Loosen InputObjectType Flow was already not happy with using `InputObjectType` within its own definition, and was converting it to `{[key: string]: string | any}` when I inspected it in Nuclide. I’m honestly not sure how to specify a union type on an indexer as was being attempted, because flow complains as soon as you try to provide an object that has a property with a typed value. * Allow more options to handle missing styleNames This changes the option from a boolean “silenceStyleNameErrors” to an enum “handleMissingStyleName” option, with possible values of “throw” (default), “warn”, and “ignore”.
1 parent 24fd47c commit 3c4e6d0

File tree

19 files changed

+180
-32
lines changed

19 files changed

+180
-32
lines changed

.eslintignore

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/test/fixtures

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ node_modules
66
!.babelrc
77
!.editorconfig
88
!.eslintrc
9+
!.eslintignore
910
!.flowconfig
1011
!.gitignore
1112
!.npmignore

README.md

+31-13
Original file line numberDiff line numberDiff line change
@@ -170,22 +170,21 @@ NODE_ENV=production ./test
170170

171171
## Configuration
172172

173-
```js
174-
type FiletypeOptionsType = {|
175-
+syntax: string,
176-
+plugins?: $ReadOnlyArray<string>
177-
|};
178-
179-
type FiletypesConfigurationType = {
180-
[key: string]: FiletypeOptionsType
181-
};
182-
183-
type GenerateScopedNameType = (localName: string, resourcePath: string) => string;
184-
185-
type GenerateScopedNameConfigurationType = GenerateScopedNameType | string;
173+
Configure the options for the plugin within your `.babelrc` as follows:
174+
175+
```json
176+
{
177+
"plugins": [
178+
["react-css-modules", {
179+
"option": "value"
180+
}]
181+
]
182+
}
186183

187184
```
188185

186+
### Options
187+
189188
|Name|Type|Description|Default|
190189
|---|---|---|---|
191190
|`context`|`string`|Must match webpack [`context`](https://webpack.github.io/docs/configuration.html#context) configuration. [`css-loader`](https://github.com/webpack/css-loader) inherits `context` values from webpack. Other CSS module implementations might use different context resolution logic.|`process.cwd()`|
@@ -194,12 +193,31 @@ type GenerateScopedNameConfigurationType = GenerateScopedNameType | string;
194193
|`generateScopedName`|`?GenerateScopedNameConfigurationType`|Refer to [Generating scoped names](https://github.com/css-modules/postcss-modules#generating-scoped-names)|`[path]___[name]__[local]___[hash:base64:5]`|
195194
|`removeImport`|`boolean`|Remove the matching style import. This option is used to enable server-side rendering.|`false`|
196195
|`webpackHotModuleReloading`|`boolean`|Enables hot reloading of CSS in webpack|`false`|
196+
|`handleMissingStyleName`|`"throw"`, `"warn"`, `"ignore"`|Determines what should be done for undefined CSS modules (using a `styleName` for which there is no CSS module defined). Setting this option to `"ignore"` is equivalent to setting `errorWhenNotFound: false` in [react-css-modules](https://github.com/gajus/react-css-modules#errorwhennotfound). |`"throw"`|
197197

198198
Missing a configuration? [Raise an issue](https://github.com/gajus/babel-plugin-react-css-modules/issues/new?title=New%20configuration:).
199199

200200
> Note:
201201
> The default configuration should work out of the box with the [css-loader](https://github.com/webpack/css-loader).
202202
203+
#### Option types (flow)
204+
205+
```js
206+
type FiletypeOptionsType = {|
207+
+syntax: string,
208+
+plugins?: $ReadOnlyArray<string>
209+
|};
210+
211+
type FiletypesConfigurationType = {
212+
[key: string]: FiletypeOptionsType
213+
};
214+
215+
type GenerateScopedNameType = (localName: string, resourcePath: string) => string;
216+
217+
type GenerateScopedNameConfigurationType = GenerateScopedNameType | string;
218+
219+
```
220+
203221
### Configurate syntax loaders
204222

205223
To add support for different CSS syntaxes (e.g. SCSS), perform the following two steps:

src/createObjectExpression.js

+3-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import BabelTypes, {
55
} from 'babel-types';
66

77
type InputObjectType = {
8-
[key: string]: string | InputObjectType
8+
[key: string]: *
99
};
1010

1111
/**
@@ -26,6 +26,8 @@ const createObjectExpression = (t: BabelTypes, object: InputObjectType): ObjectE
2626
newValue = t.stringLiteral(value);
2727
} else if (typeof value === 'object') {
2828
newValue = createObjectExpression(t, value);
29+
} else if (typeof value === 'boolean') {
30+
newValue = t.booleanLiteral(value);
2931
} else {
3032
throw new Error('Unexpected type.');
3133
}

src/getClassName.js

+50-8
Original file line numberDiff line numberDiff line change
@@ -2,37 +2,69 @@
22

33
import type {
44
StyleModuleMapType,
5-
StyleModuleImportMapType
5+
StyleModuleImportMapType,
6+
HandleMissingStyleNameOptionType
67
} from './types';
8+
import optionsDefaults from './schemas/optionsDefaults';
79

810
const isNamespacedStyleName = (styleName: string): boolean => {
911
return styleName.indexOf('.') !== -1;
1012
};
1113

12-
const getClassNameForNamespacedStyleName = (styleName: string, styleModuleImportMap: StyleModuleImportMapType): string => {
14+
const getClassNameForNamespacedStyleName = (
15+
styleName: string,
16+
styleModuleImportMap: StyleModuleImportMapType,
17+
handleMissingStyleNameOption?: HandleMissingStyleNameOptionType
18+
): ?string => {
1319
// Note:
1420
// Do not use the desctructing syntax with Babel.
1521
// Desctructing adds _slicedToArray helper.
1622
const styleNameParts = styleName.split('.');
1723
const importName = styleNameParts[0];
1824
const moduleName = styleNameParts[1];
25+
const handleMissingStyleName = handleMissingStyleNameOption || optionsDefaults.handleMissingStyleName;
1926

2027
if (!moduleName) {
21-
throw new Error('Invalid style name.');
28+
if (handleMissingStyleName === 'throw') {
29+
throw new Error('Invalid style name.');
30+
} else if (handleMissingStyleName === 'warn') {
31+
// eslint-disable-next-line no-console
32+
console.warn('Invalid style name.');
33+
} else {
34+
return null;
35+
}
2236
}
2337

2438
if (!styleModuleImportMap[importName]) {
25-
throw new Error('CSS module import does not exist.');
39+
if (handleMissingStyleName === 'throw') {
40+
throw new Error('CSS module import does not exist.');
41+
} else if (handleMissingStyleName === 'warn') {
42+
// eslint-disable-next-line no-console
43+
console.warn('CSS module import does not exist.');
44+
} else {
45+
return null;
46+
}
2647
}
2748

2849
if (!styleModuleImportMap[importName][moduleName]) {
29-
throw new Error('CSS module does not exist.');
50+
if (handleMissingStyleName === 'throw') {
51+
throw new Error('CSS module does not exist.');
52+
} else if (handleMissingStyleName === 'warn') {
53+
// eslint-disable-next-line no-console
54+
console.warn('CSS module does not exist.');
55+
} else {
56+
return null;
57+
}
3058
}
3159

3260
return styleModuleImportMap[importName][moduleName];
3361
};
3462

35-
export default (styleNameValue: string, styleModuleImportMap: StyleModuleImportMapType): string => {
63+
type OptionsType = {|
64+
handleMissingStyleName: HandleMissingStyleNameOptionType
65+
|};
66+
67+
export default (styleNameValue: string, styleModuleImportMap: StyleModuleImportMapType, options: OptionsType): string => {
3668
const styleModuleImportMapKeys = Object.keys(styleModuleImportMap);
3769

3870
return styleNameValue
@@ -42,7 +74,7 @@ export default (styleNameValue: string, styleModuleImportMap: StyleModuleImportM
4274
})
4375
.map((styleName) => {
4476
if (isNamespacedStyleName(styleName)) {
45-
return getClassNameForNamespacedStyleName(styleName, styleModuleImportMap);
77+
return getClassNameForNamespacedStyleName(styleName, styleModuleImportMap, options.handleMissingStyleName);
4678
}
4779

4880
if (styleModuleImportMapKeys.length === 0) {
@@ -56,10 +88,20 @@ export default (styleNameValue: string, styleModuleImportMap: StyleModuleImportM
5688
const styleModuleMap: StyleModuleMapType = styleModuleImportMap[styleModuleImportMapKeys[0]];
5789

5890
if (!styleModuleMap[styleName]) {
59-
throw new Error('Could not resolve the styleName \'' + styleName + '\'.');
91+
if (options.handleMissingStyleName === 'throw') {
92+
throw new Error('Could not resolve the styleName \'' + styleName + '\'.');
93+
}
94+
if (options.handleMissingStyleName === 'warn') {
95+
// eslint-disable-next-line no-console
96+
console.warn('Could not resolve the styleName \'' + styleName + '\'.');
97+
}
6098
}
6199

62100
return styleModuleMap[styleName];
63101
})
102+
.filter((className) => {
103+
// Remove any styles which could not be found (if handleMissingStyleName === 'ignore')
104+
return className;
105+
})
64106
.join(' ');
65107
};

src/index.js

+5-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import BabelTypes from 'babel-types';
99
import ajvKeywords from 'ajv-keywords';
1010
import Ajv from 'ajv';
1111
import optionsSchema from './schemas/optionsSchema.json';
12+
import optionsDefaults from './schemas/optionsDefaults';
1213
import createObjectExpression from './createObjectExpression';
1314
import requireCssModule from './requireCssModule';
1415
import resolveStringLiteral from './resolveStringLiteral';
@@ -190,7 +191,8 @@ export default ({
190191
resolveStringLiteral(
191192
path,
192193
filenameMap[filename].styleModuleImportMap,
193-
styleNameAttribute
194+
styleNameAttribute,
195+
{handleMissingStyleName: stats.opts.handleMissingStyleName || optionsDefaults.handleMissingStyleName}
194196
);
195197

196198
return;
@@ -205,7 +207,8 @@ export default ({
205207
path,
206208
styleNameAttribute,
207209
filenameMap[filename].importedHelperIndentifier,
208-
filenameMap[filename].styleModuleImportMapIdentifier
210+
filenameMap[filename].styleModuleImportMapIdentifier,
211+
{handleMissingStyleName: stats.opts.handleMissingStyleName || optionsDefaults.handleMissingStyleName}
209212
);
210213
}
211214
},

src/replaceJsxExpressionContainer.js

+23-5
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,25 @@ import BabelTypes, {
99
jSXExpressionContainer,
1010
jSXIdentifier
1111
} from 'babel-types';
12+
import type {
13+
HandleMissingStyleNameOptionType
14+
} from './types';
1215
import conditionalClassMerge from './conditionalClassMerge';
16+
import createObjectExpression from './createObjectExpression';
17+
import optionsDefaults from './schemas/optionsDefaults';
18+
19+
type OptionsType = {|
20+
handleMissingStyleName: HandleMissingStyleNameOptionType
21+
|};
1322

1423
export default (
1524
t: BabelTypes,
1625
// eslint-disable-next-line flowtype/no-weak-types
1726
path: Object,
1827
styleNameAttribute: JSXAttribute,
1928
importedHelperIndentifier: Identifier,
20-
styleModuleImportMapIdentifier: Identifier
29+
styleModuleImportMapIdentifier: Identifier,
30+
options: OptionsType
2131
): void => {
2232
const expressionContainerValue = styleNameAttribute.value;
2333
const classNameAttribute = path.node.openingElement.attributes
@@ -31,12 +41,20 @@ export default (
3141

3242
path.node.openingElement.attributes.splice(path.node.openingElement.attributes.indexOf(styleNameAttribute), 1);
3343

44+
const args = [
45+
expressionContainerValue.expression,
46+
styleModuleImportMapIdentifier
47+
];
48+
49+
// Only provide options argument if the options are something other than default
50+
// This helps save a few bits in the generated user code
51+
if (options.handleMissingStyleName !== optionsDefaults.handleMissingStyleName) {
52+
args.push(createObjectExpression(t, options));
53+
}
54+
3455
const styleNameExpression = t.callExpression(
3556
importedHelperIndentifier,
36-
[
37-
expressionContainerValue.expression,
38-
styleModuleImportMapIdentifier
39-
]
57+
args
4058
);
4159

4260
if (classNameAttribute) {

src/resolveStringLiteral.js

+8-3
Original file line numberDiff line numberDiff line change
@@ -9,19 +9,24 @@ import {
99
import conditionalClassMerge from './conditionalClassMerge';
1010
import getClassName from './getClassName';
1111
import type {
12-
StyleModuleImportMapType
12+
StyleModuleImportMapType,
13+
HandleMissingStyleNameOptionType
1314
} from './types';
1415

16+
type OptionsType = {|
17+
handleMissingStyleName: HandleMissingStyleNameOptionType
18+
|};
19+
1520
/**
1621
* Updates the className value of a JSX element using a provided styleName attribute.
1722
*/
18-
export default (path: *, styleModuleImportMap: StyleModuleImportMapType, styleNameAttribute: JSXAttribute): void => {
23+
export default (path: *, styleModuleImportMap: StyleModuleImportMapType, styleNameAttribute: JSXAttribute, options: OptionsType): void => {
1924
const classNameAttribute = path.node.openingElement.attributes
2025
.find((attribute) => {
2126
return typeof attribute.name !== 'undefined' && attribute.name.name === 'className';
2227
});
2328

24-
const resolvedStyleName = getClassName(styleNameAttribute.value.value, styleModuleImportMap);
29+
const resolvedStyleName = getClassName(styleNameAttribute.value.value, styleModuleImportMap, options);
2530

2631
if (classNameAttribute) {
2732
if (isStringLiteral(classNameAttribute.value)) {

src/schemas/optionsDefaults.js

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
const optionsDefaults = {
2+
handleMissingStyleName: 'throw'
3+
};
4+
5+
export default optionsDefaults;

src/schemas/optionsSchema.json

+3
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@
4343
},
4444
"webpackHotModuleReloading": {
4545
"type": "boolean"
46+
},
47+
"handleMissingStyleName": {
48+
"enum": ["throw", "warn", "ignore"]
4649
}
4750
},
4851
"type": "object"

src/types.js

+2
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ export type StyleModuleImportMapType = {
1111
export type GenerateScopedNameType = (localName: string, resourcePath: string) => string;
1212

1313
export type GenerateScopedNameConfigurationType = GenerateScopedNameType | string;
14+
15+
export type HandleMissingStyleNameOptionType = 'throw' | 'warn' | 'ignore';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import "./foo.css";
2+
3+
<div styleName="a not-found" />;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import "./foo.css";
2+
3+
<div className="foo__a" />;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.a {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"plugins": [
3+
[
4+
"../../../../src",
5+
{
6+
"generateScopedName": "[name]__[local]",
7+
"handleMissingStyleName": "ignore"
8+
}
9+
]
10+
]
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import './foo.css';
2+
3+
const styleNameFoo = 'a-c';
4+
5+
<div styleName={styleNameFoo}></div>;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import _getClassName from 'babel-plugin-react-css-modules/dist/browser/getClassName';
2+
import './foo.css';
3+
4+
const _styleModuleImportMap = {
5+
'./foo.css': {
6+
'a-b': 'foo__a-b'
7+
}
8+
};
9+
const styleNameFoo = 'a-c';
10+
11+
<div className={_getClassName(styleNameFoo, _styleModuleImportMap, {
12+
'handleMissingStyleName': 'ignore'
13+
})}></div>;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
.a-b {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"plugins": [
3+
[
4+
"../../../../src",
5+
{
6+
"generateScopedName": "[name]__[local]",
7+
"handleMissingStyleName": "ignore"
8+
}
9+
]
10+
]
11+
}

0 commit comments

Comments
 (0)