Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add autoResolveMultiImports option #234

Merged
merged 3 commits into from Feb 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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>;