Skip to content

Extract CSS to css file/files #6

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

Closed
wants to merge 1 commit into from
Closed
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
202 changes: 135 additions & 67 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
}
Expand Down
14 changes: 5 additions & 9 deletions test/index.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]
]
});
}
Expand All @@ -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', () => {
Expand Down Expand Up @@ -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', {}]
]
});

Expand Down