-
Notifications
You must be signed in to change notification settings - Fork 49
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
Added css to css module script #277
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
const fs = require("fs"); | ||
const path = require("path"); | ||
|
||
/** | ||
* List of blacklisted classes representing classNames not to convert | ||
* | ||
*/ | ||
const blackListedClasses = [ | ||
"row", | ||
"cards-pf", | ||
"pficon ", | ||
"pficon-info", | ||
"fa", | ||
"fa-database", | ||
"fa-file", | ||
"btn", | ||
"btn-large", | ||
"login-pf-signup", | ||
"text-light", | ||
"no-flex", | ||
"row-cards-pf", | ||
"container-fluid", | ||
"container-cards-pf", | ||
"pf-c-button", | ||
"pf-m-primary", | ||
"pf-m-link", | ||
"pf-c-button__icon", | ||
"pf-m-start", | ||
"fas", | ||
"fa-user", | ||
]; | ||
|
||
const filesToChange = function (dirPath, fileList) { | ||
const files = fs.readdirSync(dirPath); | ||
|
||
let arrayOfFiles = fileList || []; | ||
|
||
files.forEach((file) => { | ||
if (fs.statSync(`${dirPath }/${ file}`).isDirectory()) { | ||
arrayOfFiles = filesToChange(`${dirPath }/${ file}`, arrayOfFiles); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a good use of recursion. |
||
} else if (file.split(".").slice(-1)[0] === "jsx") { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Notice that you have an "if/else if" here, which implies that your function is actually doing more than one thing, and it might be helpful to split it into a function for each thing it's doing. Here, I would say that the function
One option would be to use a library (or a different programming language): https://github.com/isaacs/node-glob There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. try to use the functions which are available to you, such as |
||
arrayOfFiles.push(path.join(dirPath, "/", file)); | ||
} | ||
}); | ||
return arrayOfFiles; | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here's how I would've done it: const fs = require('fs');
const path = require('path');
/**
* @param {string} startingPath an existing path
* @returns list of all subpaths under startingPath, including itself
*/
function findAllPaths(startingPath) {
if (fs.statSync(startingPath).isDirectory()) {
const subpaths = fs.readdirSync(startingPath).map((subpath) => {
return findAllPaths(path.join(startingPath, subpath));
});
return Array.prototype.concat.apply([startingPath], subpaths);
}
return [startingPath];
}
// this helper function here has BOTH a self-explanatory name AND a simple implementation,
// so I am choosing to not add documentation
function filterByFileExtension(paths, suffix) {
return paths.filter((p) => {
return fs.statSync(p).isFile() && p.endsWith(suffix);
});
}
// example
const exampleDirectory = 'ChRIS_store_ui/src';
filterByFileExtension(findAllPaths(exampleDirectory), '.jsx').forEach(p => {
console.log(p);
}); I've identified the individual units of functionality of this task and created a helper for each one. This way, each function is individually testable, so if either one is doing the wrong thing, it's much easier to identify where the bug is in my code, and also the fix for my code will be much smaller. It can also be easier for others to read, understand, and review my code, because the names of my functions are descriptive and there is docstring where necessary. |
||
|
||
// const pathToComponents = path.resolve(__dirname, '../src/components') | ||
// const arrayOfFiles = filesToChange(pathToComponents); | ||
|
||
/** | ||
* Converts .css to .module.css file format and CSS classes to use CSS module object syntax | ||
* | ||
* @param filePath path to the files to be changed | ||
* @return files converted to CSS module object syntax | ||
*/ | ||
function convertFile(filePath) { | ||
fs.readFile(filePath, "utf8", (err, textData) => { | ||
const cssFileName = filePath.replace(".jsx", ".css"); | ||
const cssModuleName = filePath.replace(".jsx", ".module.css"); | ||
fs.rename(cssFileName, cssModuleName, () => {}); | ||
let data = textData.replace( | ||
`import './${cssFileName.split("/").slice(-1)}'`, | ||
`import styles from './${cssModuleName.split("/").slice(-1)}'` | ||
); | ||
data = convertCssClasses(data, blackListedClasses); | ||
fs.writeFileSync(filePath, data, { encoding: "utf8", flag: "w" }); | ||
}); | ||
} | ||
|
||
/** | ||
* Convert all classes in a className to use CSS module object syntax. | ||
* | ||
* @param data string containing JSX file contents | ||
* @param blackListedClasses list of strings representing class names not to convert | ||
* @returns a classname using CSS module object syntax | ||
*/ | ||
export function convertCssClasses(data, blackListedClasses) { | ||
return data.replace(/(?<=className=)".+?"/g, (classNames) => { | ||
const className = classNames.slice(1, -1).split(/[, ]+/); | ||
let res; | ||
if (className.length > 1) { | ||
res = "{`"; | ||
className.forEach((cls, index) => { | ||
if (Number(index) !== 0) { | ||
res += " "; | ||
} | ||
if (blackListedClasses.includes(cls)) { | ||
res += cls; | ||
} else { | ||
res += `\${styles['${cls}']}`; | ||
} | ||
}); | ||
res += "`}"; | ||
} else if (blackListedClasses.includes(className[0])) { | ||
res = `{\`${className[0]}\`}`; | ||
} else { | ||
res = "{`$"; | ||
res += `{styles['${className[0]}']}`; | ||
res += "`}"; | ||
} | ||
return res; | ||
}); | ||
} | ||
|
||
/** | ||
* Convert all CSS classes to use object syntax as if they were imported from a CSS module. | ||
* | ||
* @param data string containing JSX file contents | ||
* @param blackListedClasses list of strings representing class names not to convert | ||
* @return data where CSS classes were changed to use CSS module object syntax | ||
*/ | ||
export function processFile(data, blackListedClasses) { | ||
return data.replace(/(?<=className=)".+?"/g, ); | ||
} | ||
|
||
/** | ||
* Convert a className to use CSS module syntax. | ||
* | ||
* @param className a class from the className of a JSX object | ||
* @param blackListedClasses list of strings representing class names not to convert | ||
* @returns className using CSS module syntax | ||
*/ | ||
export function convertCssString(classNames, blackListedClasses) { | ||
const className = classNames.slice(1, -1).split(/[, ]+/); | ||
let res; | ||
if (className.length > 1) { | ||
res = "{`"; | ||
className.forEach((cls, index) => { | ||
if (Number(index) !== 0) { | ||
res += " "; | ||
} | ||
if (blackListedClasses.includes(cls)) { | ||
res += cls; | ||
} else { | ||
res += `\${styles['${ cls }']}`; | ||
} | ||
}); | ||
res += "`}"; | ||
} else if (blackListedClasses.includes(className[0])) { | ||
res = `{\`${ className[0] }\`}`; | ||
} else { | ||
res = "{`$"; | ||
res += `{styles['${className[0]}']}`; | ||
res += "`}"; | ||
} | ||
return res; | ||
} | ||
|
||
/** | ||
* Convert a className to use CSS module syntax. | ||
* | ||
* @param className a class from the className of a JSX object | ||
* @param cssModuleName name of imported CSS module | ||
* @returns className using CSS module syntax | ||
*/ | ||
export function convertSingleCssClass(className, cssModuleName) { | ||
return `${cssModuleName}['${className}']` | ||
} | ||
|
||
|
||
// arrayOfFiles.forEach(async (filePath) => convertFile(filePath)); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import {convertCssString, convertCssClasses, convertSingleCssClass} from "./cssToCssModule"; | ||
|
||
describe('css module conversion script', () => { | ||
it('should work with one class', () => { | ||
const exampleData = '<div className="hello-world" />'; | ||
const expected = "<div className={`${styles['hello-world']}`} />"; | ||
const actual = convertCssClasses(exampleData, []); | ||
expect(actual).toBe(expected); | ||
}); | ||
|
||
|
||
it('should work with muliple class names', () => { | ||
const exampleData = '"hello-world bottom-footer"'; | ||
const expected = "{`${styles['hello-world']} ${styles['bottom-footer']}`}"; | ||
const actual = convertCssString(exampleData, []); | ||
expect(actual).toBe(expected); | ||
}); | ||
|
||
it('should not process blacklisted class names', () => { | ||
const exampleData = '<div className="hello-world bottom-footer" />'; | ||
const expected = "<div className={`hello-world ${styles['bottom-footer']}`} />"; | ||
const actual = convertCssClasses(exampleData, ['hello-world']); | ||
expect(actual).toBe(expected); | ||
}); | ||
|
||
it('should not process multiple blacklisted classNames', () => { | ||
const exampleData = '"fa-file fa bottom-footer"'; | ||
const expected = "{`fa-file fa ${styles['bottom-footer']}`}"; | ||
const actual = convertCssString(exampleData, ['fa-file', 'fa']); | ||
expect(actual).toBe(expected); | ||
}); | ||
|
||
it('should not process blacklisted classNames', () => { | ||
const exampleData = '"fa-file fa"'; | ||
const expected = "{`fa-file fa`}"; | ||
const actual = convertCssString(exampleData, ['fa-file', 'fa']); | ||
expect(actual).toBe(expected); | ||
}); | ||
|
||
it('should be able to convert a single class', () => { | ||
const exampleData = '"bottom-footer"'; | ||
const expected = "{`${styles['bottom-footer']}`}"; | ||
const actual = convertCssString(exampleData, []); | ||
expect(actual).toBe(expected); | ||
}); | ||
|
||
|
||
it('should be able to convert a single class', () => { | ||
const actual1 = convertSingleCssClass("hello-world", "styles"); | ||
expect(actual1).toBe("styles['hello-world']"); | ||
const actual2 = convertSingleCssClass("footer-text", "secondStyles"); | ||
expect(actual2).toBe("secondStyles['footer-text']"); | ||
}); | ||
|
||
}); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
const fs = require("fs"); | ||
const path = require("path"); | ||
|
||
const blackListedClasses = [ | ||
"row", | ||
"cards-pf", | ||
"pficon ", | ||
"pficon-info", | ||
"fa", | ||
"fa-database", | ||
"fa-file", | ||
"btn", | ||
"btn-large", | ||
"login-pf-signup", | ||
"text-light", | ||
"no-flex", | ||
"row-cards-pf", | ||
"container-fluid", | ||
"container-cards-pf", | ||
"pf-c-button", | ||
"pf-m-primary", | ||
"pf-m-link", | ||
"pf-c-button__icon", | ||
"pf-m-start", | ||
"fas", | ||
"fa-user", | ||
]; | ||
|
||
const filesToChange = function (dirPath, fileList) { | ||
const files = fs.readdirSync(dirPath); | ||
|
||
let arrayOfFiles = fileList || []; | ||
|
||
files.forEach((file) => { | ||
if (fs.statSync(`${dirPath }/${ file}`).isDirectory()) { | ||
arrayOfFiles = filesToChange(`${dirPath }/${ file}`, arrayOfFiles); | ||
} else if (file.split(".").slice(-1)[0] === "jsx") { | ||
arrayOfFiles.push(path.join(dirPath, "/", file)); | ||
} | ||
}); | ||
return arrayOfFiles; | ||
}; | ||
|
||
const pathToComponents = path.resolve(__dirname, '../components') | ||
const arrayOfFiles = filesToChange(pathToComponents); | ||
|
||
function convertFile(filePath) { | ||
fs.readFile(filePath, "utf8", (err, textData) => { | ||
const cssFileName = filePath.replace(".jsx", ".css"); | ||
const cssModuleName = filePath.replace(".jsx", ".module.css"); | ||
fs.rename(cssFileName, cssModuleName, () => {}); | ||
let data = textData.replace( | ||
`import './${cssFileName.split("/").slice(-1)}'`, | ||
`import styles from './${cssModuleName.split("/").slice(-1)}'` | ||
); | ||
data = data.replace(/(?<=className=)".+?"/g, (classNames) => { | ||
const className = classNames.slice(1, -1).split(/[, ]+/); | ||
let res; | ||
if (className.length > 1) { | ||
res = "{`"; | ||
className.forEach((cls, index) => { | ||
if (Number(index) !== 0) { | ||
res += " "; | ||
} | ||
if (blackListedClasses.includes(cls)) { | ||
res += cls; | ||
} else { | ||
res += `\${styles['${ cls }']}`; | ||
} | ||
}); | ||
res += "`}"; | ||
} else if (blackListedClasses.includes(className[0])) { | ||
res = `{\`${ className[0] }\`}`; | ||
} else { | ||
res = "{`$"; | ||
res += `{styles['${className[0]}']}`; | ||
res += "`}"; | ||
} | ||
return res; | ||
}); | ||
fs.writeFileSync(filePath, data, { encoding: "utf8", flag: "w" }); | ||
}); | ||
} | ||
|
||
arrayOfFiles.forEach(async (filePath) => convertFile(filePath)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When you make an argument "optional" it's actually a union-type between being "something" or
null
. Union types can be complicated because you must implement logic to handle both types. For these reasons, it is important that, whenever you have a union type or optional argument, you should provide some documentation indicating it as such.