diff --git a/README.md b/README.md index b58fecc..131c001 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,7 @@ npm install --save-dev babel-plugin-css-modules-transform "plugins": [ [ "css-modules-transform", { + "extensions": ['.css'], // which files to parse "generateScopedName": "[name]__[local]___[hash:base64:5]", // in case you don't want to use a function "generateScopedName": "./path/to/module-exporting-a-function.js", // in case you want to use a function "generateScopedName": "npm-module-name", diff --git a/src/index.js b/src/index.js index 2752846..479c70f 100644 --- a/src/index.js +++ b/src/index.js @@ -24,115 +24,183 @@ export default function transformCssModules({ types: t }) { return resolve(dir); } + /** + * + * @param {String} filepath javascript file path + * @param {String} cssFile requireed css file path + * @returns {Array} array of class names + */ + function requireCssFile(filepath, cssFile) { + const from = resolveModulePath(filepath); + return require(resolve(from, cssFile)); + } + return { visitor: { - CallExpression(path, { file, opts }) { - const currentConfig = { ...defaultOptions, ...opts }; + Program: { + enter(path, state) { + state.$$css = { + styles: new Map() + }; + + const { extensions = ['.css'] } = state.opts; - // check if there are simple requires and if they are functions - simpleRequires.forEach(key => { - if (typeof currentConfig[key] !== 'string') { - return; + if (!Array.isArray(extensions)) { + throw new Error('Extensions configurations has to be an array'); } - const modulePath = resolve(process.cwd(), currentConfig[key]); + state.$$css.extensions = new RegExp(`(${extensions.join('|').replace('.', '\\.')})$`, 'i'); - // this one can be require or string - if (key === 'generateScopedName') { - try { - // if it is existing file, require it, otherwise use value - currentConfig[key] = require(modulePath); - } catch (e) { - try { - currentConfig[key] = require(currentConfig[key]); - } catch (_e) { - // do nothing, because it is not a valid path - } + // initialize css modules require hook + const currentConfig = { ...defaultOptions, ...state.opts }; + + const pushStylesCreator = (toWrap) => (css, filepath) => { + if (!state.$$css.styles.has(filepath)) { + state.$$css.styles.set(filepath, css); } - if (typeof currentConfig[key] !== 'function' && typeof currentConfig[key] !== 'string') { - throw new Error(`Configuration '${key}' is not a string or function.`); + if (typeof toWrap === 'function') { + return toWrap(css, filepath); } + }; - return; - } + // check if there are simple requires and if they are functions + simpleRequires.forEach(key => { + if (typeof currentConfig[key] !== 'string') { + return; + } - if (currentConfig.hasOwnProperty(key)) { - try { - currentConfig[key] = require(modulePath); - } catch (e) { + const modulePath = resolve(process.cwd(), currentConfig[key]); + + // this one can be require or string + if (key === 'generateScopedName') { try { - currentConfig[key] = require(currentConfig[key]); - } catch (_e) { - // do nothing because it is not a valid path + // if it is existing file, require it, otherwise use value + currentConfig[key] = require(modulePath); + } catch (e) { + try { + currentConfig[key] = require(currentConfig[key]); + } catch (_e) { + // do nothing, because it is not a valid path + } } + + if (typeof currentConfig[key] !== 'function' && typeof currentConfig[key] !== 'string') { + throw new Error(`Configuration '${key}' is not a string or function.`); + } + + return; } - if (typeof currentConfig[key] !== 'function') { - throw new Error(`Module '${modulePath}' does not exist or is not a function.`); + if (currentConfig.hasOwnProperty(key)) { + try { + currentConfig[key] = require(modulePath); + } catch (e) { + try { + currentConfig[key] = require(currentConfig[key]); + } catch (_e) { + // do nothing because it is not a valid path + } + } + + if (typeof currentConfig[key] !== 'function') { + throw new Error(`Module '${modulePath}' does not exist or is not a function.`); + } + + // if processCss is name of configuration key, then wrap it to own function + // otherwise it will be just defined + if (key === 'processCss') { + currentConfig[key] = pushStylesCreator(currentConfig[key]); + } } - } - }); + }); - complexRequires.forEach(key => { - if (!currentConfig.hasOwnProperty(key)) { - return; + // if processCss is not defined, define it + if (typeof currentConfig.processCss === 'undefined') { + currentConfig.processCss = pushStylesCreator(); } - if (!Array.isArray(currentConfig[key])) { - throw new Error(`Configuration '${key}' has to be an array.`); - } + complexRequires.forEach(key => { + if (!currentConfig.hasOwnProperty(key)) { + return; + } + + if (!Array.isArray(currentConfig[key])) { + throw new Error(`Configuration '${key}' has to be an array.`); + } - currentConfig[key].forEach((plugin, index) => { - // first try to load it using npm - try { - currentConfig[key][index] = require(plugin); - } catch (e) { + currentConfig[key].forEach((plugin, index) => { + // first try to load it using npm try { - currentConfig[key][index] = require(resolve(process.cwd(), path)); - } catch (_e) { - // do nothing + currentConfig[key][index] = require(plugin); + } catch (e) { + try { + currentConfig[key][index] = require(resolve(process.cwd(), path)); + } catch (_e) { + // do nothing + } } - } - if (typeof currentConfig[key][index] !== 'function') { - throw new Error(`Configuration '${key}' has to be valid path to a module at index ${index} or it does not export a function.`); - } + if (typeof currentConfig[key][index] !== 'function') { + throw new Error(`Configuration '${key}' has to be valid path to a module at index ${index} or it does not export a function.`); + } - currentConfig[key][index] = currentConfig[key][index](); + currentConfig[key][index] = currentConfig[key][index](); + }); }); - }); - require('css-modules-require-hook')(currentConfig); + require('css-modules-require-hook')(currentConfig); + }, + + /* eslint-disable no-unused-vars */ + exit(path, state) { + // todo extract to files / file (propably needs appending in case of single file because exit is called for each js file) + } + }, + ImportDeclaration(path, { file, $$css: { extensions }}) { + // this method is called between enter and exit, so we can map css to our state + // it is then replaced with require call which will be handled in seconds pass by CallExpression + // CallExpression will then replace it or remove depending on parent node (if is Program or not) + const { value } = path.node.source; + + // do nothing if not known extension + if (!extensions.exec(value)) { + return; + } + + requireCssFile(file.opts.filename, value); + }, + + CallExpression(path, { file, $$css: { extensions } }) { const { callee: { name: calleeName }, arguments: args } = path.node; if (calleeName !== 'require' || !args.length || !t.isStringLiteral(args[0])) { return; } - if (/\.css/i.test(args[0].value)) { - const [ { value: cssPath }] = args; - - // if parent expression is variable declarator, replace right side with tokens - if (!t.isVariableDeclarator(path.parent)) { - throw new Error( - `You can't import css file ${cssPath} to a module scope.` - ); - } + // if we are not requiring css, ignore + if (!extensions.exec(args[0].value)) { + return; + } - const from = resolveModulePath(file.opts.filename); - const tokens = require(resolve(from, cssPath)); + // if parent expression is not a variable declarator, just remove require from file + // we just want to get generated css for our output + const tokens = requireCssFile(file.opts.filename, args[0].value); + // if parent expression is not a Program, replace expression with tokens + if (!t.isExpressionStatement(path.parent)) { /* eslint-disable new-cap */ path.replaceWith(t.ObjectExpression( - Object.keys(tokens).map( - token => t.ObjectProperty( + Object.keys(tokens).map( + token => t.ObjectProperty( t.StringLiteral(token), t.StringLiteral(tokens[token]) ) ) )); + } else { + path.remove(); } } } diff --git a/test/index.spec.js b/test/index.spec.js index 364bd92..9da6366 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -16,7 +16,7 @@ describe('babel-plugin-css-modules-transform', () => { 'transform-object-rest-spread', 'transform-es2015-spread', 'transform-export-extensions', - ['../src/index.js', configuration] + ['../../src/index.js', configuration] ] }); } @@ -25,14 +25,10 @@ describe('babel-plugin-css-modules-transform', () => { return readFileSync(resolve(__dirname, path), 'utf8'); } - it('should throw if we are requiring css module to module scope', () => { - expect(() => transform('fixtures/global.require.js')).to.throw( - /^.+: You can't import css file .+ to a module scope\.$/ - ); + it('should not throw if we are requiring css module to module scope', () => { + expect(() => transform('fixtures/global.require.js')).to.not.throw(); - expect(() => transform('fixtures/global.import.js')).to.throw( - /^.+: You can't import css file .+ to a module scope\.$/ - ); + expect(() => transform('fixtures/global.import.js')).to.not.throw(); }); it('should throw if generateScopeName is not exporting a function', () => { @@ -114,7 +110,7 @@ describe('babel-plugin-css-modules-transform', () => { 'transform-object-rest-spread', 'transform-es2015-spread', 'transform-export-extensions', - ['../src/index.js', {}] + ['../../src/index.js', {}] ] });