diff --git a/package.json b/package.json index a0fcbfa..a8c6820 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "babel-plugin-css-modules-transform", - "version": "1.4.0", + "version": "1.6.2", "description": "Transform required css modules so one can use generated class names.", "main": "build/index.js", "scripts": { diff --git a/src/index.js b/src/index.js index abbbb71..6cc6c48 100644 --- a/src/index.js +++ b/src/index.js @@ -10,6 +10,16 @@ const defaultOptions = { generateScopedName: '[name]__[local]___[hash:base64:5]' }; +function updateStyleSheetPath(pathStringLiteral, importPathFormatter) { + if (!importPathFormatter) { return pathStringLiteral; } + + return { + ...pathStringLiteral, + value: importPathFormatter(pathStringLiteral.value) + }; +} + + function findExpressionStatementChild(path, t) { const parent = path.parentPath; if (!parent) { @@ -113,6 +123,7 @@ export default function transformCssModules({ types: t }) { // this is not a css-require-ook config delete currentConfig.extractCss; delete currentConfig.keepImport; + delete currentConfig.importPathFormatter; // match file extensions, speeds up transform by creating one // RegExp ahead of execution time @@ -139,6 +150,9 @@ export default function transformCssModules({ types: t }) { Object.keys(requireHooksOptions).forEach(key => { // skip undefined options if (currentConfig[key] === undefined) { + if (key === 'importPathFormatter' && thisPluginOptions && thisPluginOptions[key]) { + thisPluginOptions[key] = requireHooksOptions[key](thisPluginOptions[key]); + } return; } @@ -191,7 +205,7 @@ export default function transformCssModules({ types: t }) { t.expressionStatement( t.callExpression( t.identifier('require'), - [t.stringLiteral(value)] + [updateStyleSheetPath(t.stringLiteral(value), thisPluginOptions.importPathFormatter)] ) ), varDeclaration @@ -227,7 +241,7 @@ export default function transformCssModules({ types: t }) { t.expressionStatement( t.callExpression( t.identifier('require'), - [t.stringLiteral(stylesheetPath)] + [updateStyleSheetPath(t.stringLiteral(stylesheetPath), thisPluginOptions.importPathFormatter)] ) ) ); diff --git a/src/options_resolvers/importPathFormatter.js b/src/options_resolvers/importPathFormatter.js new file mode 100644 index 0000000..94064f8 --- /dev/null +++ b/src/options_resolvers/importPathFormatter.js @@ -0,0 +1,23 @@ +import { isFunction, isModulePath, requireLocalFileOrNodeModule } from '../utils'; + +/** + * Resolves importPathFormatter option + * + * @param {String|Function} value + * @returns {Function} + */ +export default function importPathFormatter(value/* , currentConfig */) { + if (isFunction(value)) { + return value; + } else if (isModulePath(value)) { + const requiredOption = requireLocalFileOrNodeModule(value); + + if (!isFunction(requiredOption)) { + throw new Error(`Configuration file for 'importPathFormatter' is not exporting a function`); + } + + return requiredOption; + } + + throw new Error(`Configuration 'importPathFormatter' is not a function nor a valid module path`); +} diff --git a/src/options_resolvers/index.js b/src/options_resolvers/index.js index 46456c3..92ba1f5 100644 --- a/src/options_resolvers/index.js +++ b/src/options_resolvers/index.js @@ -11,4 +11,6 @@ export { default as preprocessCss } from './preprocessCss'; export { default as processCss } from './processCss'; export { default as processorOpts } from './processorOpts'; export { default as rootDir } from './rootDir'; +export { default as resolve } from './resolve'; export { default as use } from './use'; +export { default as importPathFormatter } from './importPathFormatter'; diff --git a/src/options_resolvers/processorOpts.js b/src/options_resolvers/processorOpts.js index c4188a7..450129c 100644 --- a/src/options_resolvers/processorOpts.js +++ b/src/options_resolvers/processorOpts.js @@ -1,7 +1,7 @@ import { isModulePath, isPlainObject, requireLocalFileOrNodeModule } from '../utils'; /** - * Resolves processOpts option for css-modules-require-hook + * Resolves processorOpts option for css-modules-require-hook * * @param {String|Function} value * diff --git a/src/options_resolvers/resolve.js b/src/options_resolvers/resolve.js new file mode 100644 index 0000000..007064d --- /dev/null +++ b/src/options_resolvers/resolve.js @@ -0,0 +1,53 @@ +import { isAbsolute } from 'path'; +import { statSync } from 'fs'; +import { isBoolean, isPlainObject, isString } from '../utils'; + +/** + * Resolves resolve option for css-modules-require-hook + * + * @param {*} value + * @returns {Object} + */ +export default function resolve(value/* , currentConfig */) { + if (!isPlainObject(value)) { + throw new Error(`Configuration 'resolve' is not an object`); + } + + if ('alias' in value && !isPlainObject(value.alias)) { + throw new Error(`Configuration 'resolve.alias' is not an object`); + } + + if ('extensions' in value) { + if (!Array.isArray(value.extensions)) { + throw new Error(`Configuration 'resolve.extensions' is not an array`); + } + + value.extensions.map((option, index) => { + if (!isString(option)) { + throw new Error(`Configuration 'resolve.extensions[${index}]' is not a string`); + } + }); + } + + if ('modules' in value) { + if (!Array.isArray(value.modules)) { + throw new Error(`Configuration 'resolve.modules' is not an array`); + } + + value.modules.map((option, index) => { + if (!isAbsolute(option) || !statSync(option).isDirectory()) { + throw new Error(`Configuration 'resolve.modules[${index}]' is not containing a valid absolute path`); + } + }); + } + + if ('mainFile' in value && !isString(value.mainFile)) { + throw new Error(`Configuration 'resolve.mainFile' is not a string`); + } + + if ('preserveSymlinks' in value && !isBoolean(value.preserveSymlinks)) { + throw new Error(`Configuration 'resolve.preserveSymlinks' is not a boolean`); + } + + return value; +} diff --git a/src/options_resolvers/rootDir.js b/src/options_resolvers/rootDir.js index 65d0b06..117ca7e 100644 --- a/src/options_resolvers/rootDir.js +++ b/src/options_resolvers/rootDir.js @@ -14,7 +14,7 @@ export default function rootDir(value/* , currentConfig */) { } if (!isAbsolute(value) || !statSync(value).isDirectory()) { - throw new Error(`Configuration 'rootDir' is not containg a valid absolute path`); + throw new Error(`Configuration 'rootDir' is not containing a valid absolute path`); } return value; diff --git a/src/utils/requireLocalFileOrNodeModule.js b/src/utils/requireLocalFileOrNodeModule.js index c421272..f2d1436 100644 --- a/src/utils/requireLocalFileOrNodeModule.js +++ b/src/utils/requireLocalFileOrNodeModule.js @@ -13,7 +13,11 @@ export default function requireLocalFileOrNodeModule(path) { // first try to require local file return require(localFile); } catch (e) { - // try to require node_module - return require(path); + if (e.code === 'MODULE_NOT_FOUND') { + // try to require node_module + return require(path); + } + + throw e; } } diff --git a/test/index.spec.js b/test/index.spec.js index 8c03854..d8a8efd 100644 --- a/test/index.spec.js +++ b/test/index.spec.js @@ -109,12 +109,13 @@ describe('babel-plugin-css-modules-transform', () => { }); it('should write a multiple css files using import', () => { - expect(transform('fixtures/import.js', { + expect(transform(`${__dirname}/fixtures/import.js`, { extractCss: { dir: `${__dirname}/output/`, - filename: '[name].css', - relativeRoot: `${__dirname}` - } + filename: '[path]/[name].css', + relativeRoot: __dirname + }, + extensions: ['.scss', '.css'] }).code).to.be.equal(readExpected('fixtures/import.expected.js')); expect(readExpected(`${__dirname}/output/parent.css`)) diff --git a/test/options_resolvers/resolve.spec.js b/test/options_resolvers/resolve.spec.js new file mode 100644 index 0000000..4bbbee0 --- /dev/null +++ b/test/options_resolvers/resolve.spec.js @@ -0,0 +1,71 @@ +import { expect } from 'chai'; + +import resolve from '../../src/options_resolvers/resolve'; + +describe('options_resolvers/resolve', () => { + it('should throw if resolve is not an object', () => { + expect( + () => resolve([]) + ).to.throw(); + }); + + it('should throw if resolve.alias is not an object', () => { + expect( + () => resolve({ alias: [] }) + ).to.throw(); + }); + + it('should throw if resolve.extensions is not an array', () => { + expect( + () => resolve({ extensions: {} }) + ).to.throw(); + }); + + it('should throw if resolve.modules is not an array', () => { + expect( + () => resolve({ modules: {} }) + ).to.throw(); + }); + + it('should throw if resolve.modules.* is not an absolute directory or does not exist', () => { + expect( + () => resolve({ modules: ['/test/this/not/exists'] }) + ).to.throw(); + + expect( + () => resolve('./') + ).to.throw(); + }); + + it('should throw if resolve.mainFile is not a string', () => { + expect( + () => resolve({ mainFile: {} }) + ).to.throw(); + }); + + it('should throw if resolve.preserveSymlinks is not a boolean', () => { + expect( + () => resolve({ preserveSymlinks: 1 }) + ).to.throw(); + }); + + it('works if resolve.alias is an object', () => { + expect(() => resolve({ alias: {} })).to.not.throw(); + }); + + it('works if resolve.extensions is an array of strings', () => { + expect(() => resolve({ extensions: ['a', 'b'] })).to.not.throw(); + }); + + it('works if resolve.modules is an array of valid file paths', () => { + expect(() => resolve({ modules: [__dirname] })).to.not.throw(); + }); + + it('works if resolve.mainFile is a string', () => { + expect(() => resolve({ mainFile: 'aa' })).to.not.throw(); + }); + + it('works if resolve.preserveSymlinks is a boolean', () => { + expect(() => resolve({ preserveSymlinks: true })).to.not.throw(); + }); +});