Skip to content

Commit

Permalink
feat: add autoResolveMultiImports option (#234)
Browse files Browse the repository at this point in the history
* feat: add autoResolveMultiImports option

* test: add test cases for autoResolveMultiImports option

* docs: add for autoResolveMultiImports option
  • Loading branch information
AlbertLucianto authored and gajus committed Feb 22, 2019
1 parent 37fe030 commit 23b3560
Show file tree
Hide file tree
Showing 30 changed files with 219 additions and 58 deletions.
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -199,6 +199,7 @@ Configure the options for the plugin within your `.babelrc` as follows:
|`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"`|
|`attributeNames`|`?AttributeNameMapType`|Refer to [Custom Attribute Mapping](#custom-attribute-mapping)|`{"styleName": "className"}`|
|`skip`|`boolean`|Whether to apply plugin if no matching `attributeNames` found in the file|`false`|
|`autoResolveMultipleImports`|`boolean`|Allow multiple anonymous imports if `styleName` is only in one of them.|`false`|

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

Expand Down
3 changes: 3 additions & 0 deletions src/createObjectExpression.js
Expand Up @@ -28,6 +28,9 @@ const createObjectExpression = (t: BabelTypes, object: InputObjectType): ObjectE
newValue = createObjectExpression(t, value);
} else if (typeof value === 'boolean') {
newValue = t.booleanLiteral(value);
} else if (typeof value === 'undefined') {
// eslint-disable-next-line no-continue
continue;
} else {
throw new TypeError('Unexpected type: ' + typeof value);
}
Expand Down
97 changes: 58 additions & 39 deletions src/getClassName.js
Expand Up @@ -3,18 +3,26 @@
import type {
StyleModuleMapType,
StyleModuleImportMapType,
HandleMissingStyleNameOptionType
HandleMissingStyleNameOptionType,
GetClassNameOptionsType
} from './types';
import optionsDefaults from './schemas/optionsDefaults';

type OptionsType = {|
handleMissingStyleName: HandleMissingStyleNameOptionType
|};

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

const handleError = (message: string, handleMissingStyleName: HandleMissingStyleNameOptionType): null => {
if (handleMissingStyleName === 'throw') {
throw new Error(message);
} else if (handleMissingStyleName === 'warn') {
// eslint-disable-next-line no-console
console.warn(message);
}

return null;
};

const getClassNameForNamespacedStyleName = (
styleName: string,
styleModuleImportMap: StyleModuleImportMapType,
Expand All @@ -30,47 +38,60 @@ const getClassNameForNamespacedStyleName = (
optionsDefaults.handleMissingStyleName;

if (!moduleName) {
if (handleMissingStyleName === 'throw') {
throw new Error('Invalid style name: ' + styleName);
} else if (handleMissingStyleName === 'warn') {
// eslint-disable-next-line no-console
console.warn('Invalid style name: ' + styleName);
} else {
return null;
}
return handleError('Invalid style name: ' + styleName, handleMissingStyleName);
}

if (!styleModuleImportMap[importName]) {
if (handleMissingStyleName === 'throw') {
throw new Error('CSS module import does not exist: ' + importName);
} else if (handleMissingStyleName === 'warn') {
// eslint-disable-next-line no-console
console.warn('CSS module import does not exist: ' + importName);
} else {
return null;
}
return handleError('CSS module import does not exist: ' + importName, handleMissingStyleName);
}

if (!styleModuleImportMap[importName][moduleName]) {
if (handleMissingStyleName === 'throw') {
throw new Error('CSS module does not exist: ' + moduleName);
} else if (handleMissingStyleName === 'warn') {
// eslint-disable-next-line no-console
console.warn('CSS module does not exist: ' + moduleName);
} else {
return null;
}
return handleError('CSS module does not exist: ' + moduleName, handleMissingStyleName);
}

return styleModuleImportMap[importName][moduleName];
};

export default (styleNameValue: string, styleModuleImportMap: StyleModuleImportMapType, options?: OptionsType): string => {
const getClassNameFromMultipleImports = (
styleName: string,
styleModuleImportMap: StyleModuleImportMapType,
handleMissingStyleNameOption?: HandleMissingStyleNameOptionType
): ?string => {
const handleMissingStyleName = handleMissingStyleNameOption ||
optionsDefaults.handleMissingStyleName;

const importKeysWithMatches = Object.keys(styleModuleImportMap)
.map((importKey) => {
return styleModuleImportMap[importKey][styleName] && importKey;
})
.filter((importKey) => {
return importKey;
});

if (importKeysWithMatches.length > 1) {
throw new Error('Cannot resolve styleName "' + styleName + '" because it is present in multiple imports:' +
'\n\n\t' + importKeysWithMatches.join('\n\t') +
'\n\nYou can resolve this by using a named import, e.g:' +
'\n\n\timport foo from "' + importKeysWithMatches[0] + '";' +
'\n\t<div styleName="foo.' + styleName + '" />' +
'\n\n');
}

if (importKeysWithMatches.length === 0) {
return handleError('Could not resolve the styleName \'' + styleName + '\'.', handleMissingStyleName);
}

return styleModuleImportMap[importKeysWithMatches[0]][styleName];
};

export default (styleNameValue: string, styleModuleImportMap: StyleModuleImportMapType, options?: GetClassNameOptionsType): string => {
const styleModuleImportMapKeys = Object.keys(styleModuleImportMap);

const handleMissingStyleName = options && options.handleMissingStyleName ||
optionsDefaults.handleMissingStyleName;

const autoResolveMultipleImports = options && options.autoResolveMultipleImports;

if (!styleNameValue) {
return '';
}
Expand All @@ -91,20 +112,18 @@ export default (styleNameValue: string, styleModuleImportMap: StyleModuleImportM
}

if (styleModuleImportMapKeys.length > 1) {
throw new Error('Cannot use anonymous style name \'' + styleName +
'\' with more than one stylesheet import.');
if (!autoResolveMultipleImports) {
throw new Error('Cannot use anonymous style name \'' + styleName +
'\' with more than one stylesheet import without setting \'autoResolveMultipleImports\' to true.');
}

return getClassNameFromMultipleImports(styleName, styleModuleImportMap, handleMissingStyleName);
}

const styleModuleMap: StyleModuleMapType = styleModuleImportMap[styleModuleImportMapKeys[0]];

if (!styleModuleMap[styleName]) {
if (handleMissingStyleName === 'throw') {
throw new Error('Could not resolve the styleName \'' + styleName + '\'.');
}
if (handleMissingStyleName === 'warn') {
// eslint-disable-next-line no-console
console.warn('Could not resolve the styleName \'' + styleName + '\'.');
}
return handleError('Could not resolve the styleName \'' + styleName + '\'.', handleMissingStyleName);
}

return styleModuleMap[styleName];
Expand Down
14 changes: 8 additions & 6 deletions src/index.js
Expand Up @@ -212,19 +212,23 @@ export default ({
}

const handleMissingStyleName = stats.opts && stats.opts.handleMissingStyleName || optionsDefaults.handleMissingStyleName;
const autoResolveMultipleImports = stats.opts && stats.opts.autoResolveMultipleImports || optionsDefaults.autoResolveMultipleImports;

for (const attribute of attributes) {
const destinationName = attributeNames[attribute.name.name];

const options = {
autoResolveMultipleImports,
handleMissingStyleName
};

if (t.isStringLiteral(attribute.value)) {
resolveStringLiteral(
path,
filenameMap[filename].styleModuleImportMap,
attribute,
destinationName,
{
handleMissingStyleName
}
options
);
} else if (t.isJSXExpressionContainer(attribute.value)) {
if (!filenameMap[filename].importedHelperIndentifier) {
Expand All @@ -237,9 +241,7 @@ export default ({
destinationName,
filenameMap[filename].importedHelperIndentifier,
filenameMap[filename].styleModuleImportMapIdentifier,
{
handleMissingStyleName
}
options
);
}
}
Expand Down
11 changes: 4 additions & 7 deletions src/replaceJsxExpressionContainer.js
Expand Up @@ -11,16 +11,12 @@ import BabelTypes, {
jSXIdentifier
} from '@babel/types';
import type {
HandleMissingStyleNameOptionType
GetClassNameOptionsType
} from './types';
import conditionalClassMerge from './conditionalClassMerge';
import createObjectExpression from './createObjectExpression';
import optionsDefaults from './schemas/optionsDefaults';

type OptionsType = {|
handleMissingStyleName: HandleMissingStyleNameOptionType
|};

export default (
t: BabelTypes,
// eslint-disable-next-line flowtype/no-weak-types
Expand All @@ -29,7 +25,7 @@ export default (
destinationName: string,
importedHelperIndentifier: Identifier,
styleModuleImportMapIdentifier: Identifier,
options: OptionsType
options: GetClassNameOptionsType
): void => {
const expressionContainerValue = sourceAttribute.value;
const destinationAttribute = path.node.openingElement.attributes
Expand All @@ -50,7 +46,8 @@ export default (

// Only provide options argument if the options are something other than default
// This helps save a few bits in the generated user code
if (options.handleMissingStyleName !== optionsDefaults.handleMissingStyleName) {
if (options.handleMissingStyleName !== optionsDefaults.handleMissingStyleName ||
options.autoResolveMultipleImports !== optionsDefaults.autoResolveMultipleImports) {
args.push(createObjectExpression(t, options));
}

Expand Down
8 changes: 2 additions & 6 deletions src/resolveStringLiteral.js
Expand Up @@ -10,13 +10,9 @@ import conditionalClassMerge from './conditionalClassMerge';
import getClassName from './getClassName';
import type {
StyleModuleImportMapType,
HandleMissingStyleNameOptionType
GetClassNameOptionsType
} from './types';

type OptionsType = {|
handleMissingStyleName: HandleMissingStyleNameOptionType
|};

/**
* Updates the className value of a JSX element using a provided styleName attribute.
*/
Expand All @@ -25,7 +21,7 @@ export default (
styleModuleImportMap: StyleModuleImportMapType,
sourceAttribute: JSXAttribute,
destinationName: string,
options: OptionsType
options: GetClassNameOptionsType
): void => {
const resolvedStyleName = getClassName(sourceAttribute.value.value, styleModuleImportMap, options);

Expand Down
3 changes: 3 additions & 0 deletions src/schemas/optionsSchema.json
Expand Up @@ -72,6 +72,9 @@
},
"skip": {
"type": "boolean"
},
"autoResolveMultipleImports": {
"type": "boolean"
}
},
"type": "object"
Expand Down
5 changes: 5 additions & 0 deletions src/types.js
Expand Up @@ -13,3 +13,8 @@ export type GenerateScopedNameType = (localName: string, resourcePath: string) =
export type GenerateScopedNameConfigurationType = GenerateScopedNameType | string;

export type HandleMissingStyleNameOptionType = 'throw' | 'warn' | 'ignore';

export type GetClassNameOptionsType = {|
handleMissingStyleName: HandleMissingStyleNameOptionType,
autoResolveMultipleImports: boolean
|};
@@ -0,0 +1,3 @@
.a {}

.b {}
@@ -0,0 +1 @@
.b {}
@@ -0,0 +1,5 @@
import './foo.css';
import './bar.css';

<div styleName="a"></div>;
<div styleName="b"></div>;
@@ -0,0 +1,12 @@
{
"plugins": [
[
"../../../../src",
{
"generateScopedName": "[name]__[local]",
"autoResolveMultipleImports": true
}
]
],
"throws": "Cannot resolve styleName \"b\" because it is present in multiple imports:\n\n\t./foo.css\n\t./bar.css\n\nYou can resolve this by using a named import, e.g:\n\n\timport foo from \"./foo.css\";\n\t<div styleName=\"foo.b\" />\n\n"
}
@@ -0,0 +1,3 @@
.a {}

.b {}
@@ -0,0 +1 @@
.b {}
@@ -0,0 +1,4 @@
import './foo.css';
import './bar.css';

<div styleName="c"></div>;
@@ -0,0 +1,12 @@
{
"plugins": [
[
"../../../../src",
{
"generateScopedName": "[name]__[local]",
"autoResolveMultipleImports": true
}
]
],
"throws": "Could not resolve the styleName 'c'."
}
@@ -0,0 +1,3 @@
.a {}

.b {}
@@ -0,0 +1 @@
.b {}
@@ -0,0 +1,4 @@
import './foo.css';
import './bar.css';

<div styleName="a"></div>;
@@ -0,0 +1,11 @@
{
"plugins": [
[
"../../../../src",
{
"generateScopedName": "[name]__[local]"
}
]
],
"throws": "Cannot use anonymous style name 'a' with more than one stylesheet import without setting 'autoResolveMultipleImports' to true."
}
@@ -0,0 +1 @@
.a {}
@@ -0,0 +1 @@
.b {}
@@ -0,0 +1,5 @@
import './foo.css';
import './bar.css';

<div styleName="a"></div>;
<div styleName="b"></div>;
@@ -0,0 +1,11 @@
{
"plugins": [
[
"../../../../src",
{
"generateScopedName": "[name]__[local]",
"autoResolveMultipleImports": true
}
]
]
}
@@ -0,0 +1,8 @@
"use strict";

require("./foo.css");

require("./bar.css");

<div className="bar__a"></div>;
<div className="foo__b"></div>;
@@ -0,0 +1 @@
.a {}
@@ -0,0 +1 @@
.b {}
@@ -0,0 +1,8 @@
import './foo.css';
import './bar.css';

const styleNameA = 'a';
const styleNameB = 'b';

<div styleName={styleNameA}></div>;
<div styleName={styleNameB}></div>;

0 comments on commit 23b3560

Please sign in to comment.