Skip to content

Add JSX Expression resolution support #1

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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Aug 20, 2018
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions demo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
},
"dependencies": {
"babel-plugin-react-css-modules": "^2.1.3",
"classnames": "^2.2.6",
"react": "^15.4.1",
"react-dom": "^15.4.1",
"webpack": "^2.2.0-rc.3"
Expand Down
15 changes: 15 additions & 0 deletions demo/src/components/JSXExpressionStyleResolution.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';
import classnames from 'classnames';
import './table.css';

export default () => {
var cname = classnames({'row': true});

return <div className={classnames('table')}>
<div className={cname}>
<div styleName='cell'>A2</div>
<div styleName='cell'>B2</div>
<div styleName='cell'>C2</div>
</div>
</div>;
};
2 changes: 2 additions & 0 deletions demo/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import ReactDom from 'react-dom';
import AnonymouseStyleResolution from './components/AnonymouseStyleResolution';
import NamedStyleResolution from './components/NamedStyleResolution';
import RuntimeStyleResolution from './components/RuntimeStyleResolution';
import JSXExpressionStyleResolution from './components/JSXExpressionStyleResolution';

ReactDom.render(<div>
<AnonymouseStyleResolution />
<NamedStyleResolution />
<JSXExpressionStyleResolution />
<RuntimeStyleResolution />
</div>, document.getElementById('main'));
2 changes: 1 addition & 1 deletion demo/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ module.exports = {
plugins: [
'transform-react-jsx',
[
'react-css-modules',
require('../dist/index.js').default,
{
context
}
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,15 @@
"name": "babel-plugin-react-css-modules",
"repository": {
"type": "git",
"url": "https://github.com/gajus/babel-plugin-react-css-modules"
"url": "https://github.com/supriya-raj/babel-plugin-react-css-modules"
},
"scripts": {
"build": "rm -fr ./dist && NODE_ENV=production babel ./src --out-dir ./dist --source-maps --copy-files && npm run build-helper",
"build-helper": "mkdir -p ./dist/browser && NODE_ENV=production babel ./src/getClassName.js --out-file ./dist/browser/getClassName.js --source-maps --no-babelrc --plugins transform-es2015-modules-commonjs,transform-flow-strip-types --presets es2015",
"lint": "eslint ./src && flow",
"precommit": "npm run test && npm run lint",
"test": " NODE_ENV=test mocha --require babel-core/register"
"test": " NODE_ENV=test mocha --require babel-core/register",
"prepare": "npm install;npm run build"
},
"version": "1.0.0"
}
5 changes: 3 additions & 2 deletions src/getClassName.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,11 @@ export default (styleNameValue: string, styleModuleImportMap: StyleModuleImportM
if (!styleModuleMap[styleName]) {
if (handleMissingStyleName === 'throw') {
throw new Error('Could not resolve the styleName \'' + styleName + '\'.');
}
if (handleMissingStyleName === 'warn') {
} else if (handleMissingStyleName === 'warn') {
// eslint-disable-next-line no-console
console.warn('Could not resolve the styleName \'' + styleName + '\'.');
} else if (handleMissingStyleName === 'retain') {
return styleName;
}
}

Expand Down
122 changes: 67 additions & 55 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@ import ajvKeywords from 'ajv-keywords';
import Ajv from 'ajv';
import optionsSchema from './schemas/optionsSchema.json';
import optionsDefaults from './schemas/optionsDefaults';
import createObjectExpression from './createObjectExpression';
import requireCssModule from './requireCssModule';
import resolveStringLiteral from './resolveStringLiteral';
import replaceJsxExpressionContainer from './replaceJsxExpressionContainer';
import resolveClassnameCallExpression from './resolveClassnameCallExpression';

const ajv = new Ajv({
// eslint-disable-next-line id-match
Expand All @@ -31,44 +30,44 @@ export default ({
}) => {
const filenameMap = {};

const setupFileForRuntimeResolution = (path, filename) => {
const programPath = path.findParent((parentPath) => {
return parentPath.isProgram();
});

filenameMap[filename].importedHelperIndentifier = programPath.scope.generateUidIdentifier('getClassName');
filenameMap[filename].styleModuleImportMapIdentifier = programPath.scope.generateUidIdentifier('styleModuleImportMap');

programPath.unshiftContainer(
'body',
t.importDeclaration(
[
t.importDefaultSpecifier(
filenameMap[filename].importedHelperIndentifier
)
],
t.stringLiteral('babel-plugin-react-css-modules/dist/browser/getClassName')
)
);

const firstNonImportDeclarationNode = programPath.get('body').find((node) => {
return !t.isImportDeclaration(node);
});

firstNonImportDeclarationNode.insertBefore(
t.variableDeclaration(
'const',
[
t.variableDeclarator(
filenameMap[filename].styleModuleImportMapIdentifier,
createObjectExpression(t, filenameMap[filename].styleModuleImportMap)
)
]
)
);
// eslint-disable-next-line no-console
// console.log('setting up', filename, util.inspect(filenameMap,{depth: 5}))
};
// const setupFileForRuntimeResolution = (path, filename) => {
// const programPath = path.findParent((parentPath) => {
// return parentPath.isProgram();
// });

// filenameMap[filename].importedHelperIndentifier = programPath.scope.generateUidIdentifier('getClassName');
// filenameMap[filename].styleModuleImportMapIdentifier = programPath.scope.generateUidIdentifier('styleModuleImportMap');

// programPath.unshiftContainer(
// 'body',
// t.importDeclaration(
// [
// t.importDefaultSpecifier(
// filenameMap[filename].importedHelperIndentifier
// )
// ],
// t.stringLiteral('babel-plugin-react-css-modules/dist/browser/getClassName')
// )
// );

// const firstNonImportDeclarationNode = programPath.get('body').find((node) => {
// return !t.isImportDeclaration(node);
// });

// firstNonImportDeclarationNode.insertBefore(
// t.variableDeclaration(
// 'const',
// [
// t.variableDeclarator(
// filenameMap[filename].styleModuleImportMapIdentifier,
// createObjectExpression(t, filenameMap[filename].styleModuleImportMap)
// )
// ]
// )
// );
// // eslint-disable-next-line no-console
// // console.log('setting up', filename, util.inspect(filenameMap,{depth: 5}))
// };

const addWebpackHotModuleAccept = (path) => {
const test = t.memberExpression(t.identifier('module'), t.identifier('hot'));
Expand Down Expand Up @@ -140,6 +139,22 @@ export default ({
return {
inherits: babelPluginJsxSyntax,
visitor: {
CallExpression (path: *, stats: *): void {
const filename = stats.file.opts.filename;

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

if (t.isIdentifier(path.node.callee, {name: 'classnames'}) && !t.isJSXExpressionContainer(path.parentPath.node)) {
resolveClassnameCallExpression(
path,
stats,
filenameMap[filename].styleModuleImportMap,
{
handleMissingStyleName
}
);
}
},
ImportDeclaration (path: *, stats: *): void {
if (notForPlugin(path, stats)) {
return;
Expand Down Expand Up @@ -209,21 +224,6 @@ export default ({
handleMissingStyleName
}
);
} else if (t.isJSXExpressionContainer(attribute.value)) {
if (!filenameMap[filename].importedHelperIndentifier) {
setupFileForRuntimeResolution(path, filename);
}
replaceJsxExpressionContainer(
t,
path,
attribute,
destinationName,
filenameMap[filename].importedHelperIndentifier,
filenameMap[filename].styleModuleImportMapIdentifier,
{
handleMissingStyleName
}
);
}
}
},
Expand All @@ -240,6 +240,18 @@ export default ({
filenameMap[filename] = {
styleModuleImportMap: {}
};

if (stats.opts.defaultCssFile) {
filenameMap[filename] = {
styleModuleImportMap: {
default: requireCssModule(resolve(stats.opts.defaultCssFile), {
context: stats.opts.context,
filetypes: stats.opts.filetypes || {},
generateScopedName: stats.opts.generateScopedName
})
}
};
}
}
}
};
Expand Down
9 changes: 8 additions & 1 deletion src/requireCssModule.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ type FiletypesConfigurationType = {
[key: string]: FiletypeOptionsType
};

// Cache all tokens generated for each file
const fileTokensCache = {};

const getFiletypeOptions = (cssSourceFilePath: string, filetypes: FiletypesConfigurationType): ?FiletypeOptionsType => {
const extension = cssSourceFilePath.substr(cssSourceFilePath.lastIndexOf('.'));
const filetype = filetypes ? filetypes[extension] : null;
Expand Down Expand Up @@ -94,6 +97,9 @@ type OptionsType = {|
|};

export default (cssSourceFilePath: string, options: OptionsType): StyleModuleMapType => {
if (fileTokensCache[cssSourceFilePath]) {
return fileTokensCache[cssSourceFilePath];
}
// eslint-disable-next-line prefer-const
let runner;

Expand Down Expand Up @@ -132,6 +138,7 @@ export default (cssSourceFilePath: string, options: OptionsType): StyleModuleMap
];

runner = postcss(plugins);
fileTokensCache[cssSourceFilePath] = getTokens(runner, cssSourceFilePath, filetypeOptions);

return getTokens(runner, cssSourceFilePath, filetypeOptions);
return fileTokensCache[cssSourceFilePath];
};
40 changes: 40 additions & 0 deletions src/resolveClassnameCallExpression.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// @flow

import {
isStringLiteral,
isObjectExpression
} from 'babel-types';
import getClassName from './getClassName';
import type {
StyleModuleImportMapType,
HandleMissingStyleNameOptionType
} from './types';

type OptionsType = {|
handleMissingStyleName: HandleMissingStyleNameOptionType
|};

/**
* Updates the className value of a JSX element using a provided styleName attribute.
*/
export default (
path: *,
stats: *,
styleModuleImportMap: StyleModuleImportMapType,
options: OptionsType): void => {
const replaceCallArguments = function (callExpressionArguments) {
for (const argument of callExpressionArguments) {
if (isStringLiteral(argument)) {
argument.value = getClassName(argument.value, styleModuleImportMap, options);
} else if (isObjectExpression(argument)) {
for (const property of argument.properties) {
if (isStringLiteral(property.key)) {
property.key.value = getClassName(property.key.value, styleModuleImportMap, options);
}
}
}
}
};

replaceCallArguments(path.node.arguments);
};
5 changes: 4 additions & 1 deletion src/schemas/optionsSchema.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
"type": "boolean"
},
"handleMissingStyleName": {
"enum": ["throw", "warn", "ignore"]
"enum": ["throw", "warn", "ignore", "retain"]
},
"attributeNames": {
"additionalProperties": false,
Expand All @@ -69,6 +69,9 @@
}
},
"type": "object"
},
"defaultCssFile": {
"type": "string"
}
},
"type": "object"
Expand Down