diff --git a/demo/package.json b/demo/package.json index deb75b0..be31c1a 100644 --- a/demo/package.json +++ b/demo/package.json @@ -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" diff --git a/demo/src/components/JSXExpressionStyleResolution.js b/demo/src/components/JSXExpressionStyleResolution.js new file mode 100644 index 0000000..a70c9bc --- /dev/null +++ b/demo/src/components/JSXExpressionStyleResolution.js @@ -0,0 +1,15 @@ +import React from 'react'; +import classnames from 'classnames'; +import './table.css'; + +export default () => { + var cname = classnames({'row': true}); + + return
+
+
A2
+
B2
+
C2
+
+
; +}; diff --git a/demo/src/index.js b/demo/src/index.js index ff0febf..aa82139 100644 --- a/demo/src/index.js +++ b/demo/src/index.js @@ -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(
+
, document.getElementById('main')); diff --git a/demo/webpack.config.js b/demo/webpack.config.js index 4a63762..a3b23dc 100644 --- a/demo/webpack.config.js +++ b/demo/webpack.config.js @@ -23,7 +23,7 @@ module.exports = { plugins: [ 'transform-react-jsx', [ - 'react-css-modules', + require('../dist/index.js').default, { context } diff --git a/package.json b/package.json index 32f4660..d1501a7 100644 --- a/package.json +++ b/package.json @@ -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" } diff --git a/src/getClassName.js b/src/getClassName.js index 421787a..fd5a20e 100644 --- a/src/getClassName.js +++ b/src/getClassName.js @@ -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; } } diff --git a/src/index.js b/src/index.js index 2c32aa8..091898f 100644 --- a/src/index.js +++ b/src/index.js @@ -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 @@ -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')); @@ -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; @@ -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 - } - ); } } }, @@ -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 + }) + } + }; + } } } }; diff --git a/src/requireCssModule.js b/src/requireCssModule.js index 283718e..76234cc 100644 --- a/src/requireCssModule.js +++ b/src/requireCssModule.js @@ -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; @@ -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; @@ -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]; }; diff --git a/src/resolveClassnameCallExpression.js b/src/resolveClassnameCallExpression.js new file mode 100644 index 0000000..3fdb5b6 --- /dev/null +++ b/src/resolveClassnameCallExpression.js @@ -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); +}; diff --git a/src/schemas/optionsSchema.json b/src/schemas/optionsSchema.json index e14e525..7aa3656 100644 --- a/src/schemas/optionsSchema.json +++ b/src/schemas/optionsSchema.json @@ -52,7 +52,7 @@ "type": "boolean" }, "handleMissingStyleName": { - "enum": ["throw", "warn", "ignore"] + "enum": ["throw", "warn", "ignore", "retain"] }, "attributeNames": { "additionalProperties": false, @@ -69,6 +69,9 @@ } }, "type": "object" + }, + "defaultCssFile": { + "type": "string" } }, "type": "object"