bob-group-frontend-coding-standards
Advanced tools
Comparing version 1.0.3 to 1.0.4
472
index.ts
const fs = require("fs"); | ||
const path = require("path"); | ||
const yargs = require("yargs"); | ||
const utils = require("./src/utils"); | ||
const namingConventionUtils = require("./src/namingConventionUtils"); | ||
const commentUtils = require("./src/commentUtils"); | ||
const importUtils = require("./src/importUtils"); | ||
@@ -53,58 +57,5 @@ const args = yargs | ||
const camelCaseRegex = /^[a-z][A-Za-z0-9]*$/; | ||
const upperCamelCaseRegex = /^[A-Z][A-Za-z0-9]*$/; | ||
const upperSnakeCaseRegex = /^[A-Z0-9_]+$/; | ||
let allImportNames: string[] = []; | ||
let setupIconsContent: any; | ||
function keyToHumanReadable(key: string | undefined): string { | ||
if (!key) return ""; | ||
// @ts-ignore | ||
let keyHumanReadable = key.replaceAll("_", " "); | ||
keyHumanReadable = keyHumanReadable.replaceAll("sender", "collection"); | ||
keyHumanReadable = keyHumanReadable.replaceAll("receiver", "delivery"); | ||
keyHumanReadable = keyHumanReadable.replaceAll("-", " "); | ||
// camel case to sentence case | ||
keyHumanReadable = keyHumanReadable.replace(/([A-Z])/g, " $1").trim(); | ||
let sentenceCaseKey = | ||
keyHumanReadable.charAt(0).toUpperCase() + | ||
keyHumanReadable.slice(1).toLowerCase(); | ||
sentenceCaseKey = sentenceCaseKey.replaceAll("Bob box", "Bob Box"); | ||
sentenceCaseKey = sentenceCaseKey.replaceAll("Bob pay", "Bob Pay"); | ||
sentenceCaseKey = sentenceCaseKey.replaceAll("Bob go", "Bob Go"); | ||
return sentenceCaseKey; | ||
} | ||
function writeOutput( | ||
type: "success" | "error" | "warning" | "info", | ||
content: any | ||
) { | ||
let colors = { | ||
success: "\x1b[32m", | ||
error: "\x1b[31m", | ||
warning: "\x1b[33m", | ||
info: "\x1b[36m", | ||
}; | ||
console.log(colors[type], content); | ||
} | ||
function isInterfaceFile(filePath: string) { | ||
return filePath.indexOf("src/interfaces") > -1; | ||
} | ||
function isComponentFile(data: string, filePath: string) { | ||
return ( | ||
filePath.endsWith(".tsx") && | ||
!isInterfaceFile(filePath) && | ||
(filePath.indexOf("/pages/") > -1 || | ||
filePath.indexOf("/components/") > -1) && | ||
data.indexOf("function render") > -1 | ||
); | ||
} | ||
function processFileContents(folderPath: any, file: any) { | ||
@@ -127,3 +78,5 @@ return new Promise((resolve, reject) => { | ||
} else { | ||
// Automatic updates to files | ||
/* --------------------------------*/ | ||
/* AUTOMATIC UPDATES */ | ||
/* --------------------------------*/ | ||
data = replaceBracketPattern(data); | ||
@@ -134,6 +87,9 @@ data = addRenderMethodsComment(data, filePath); | ||
// // data = makeCommentsSentenceCase(data); // todo needs more testing | ||
// // Checks for files | ||
/* --------------------------------*/ | ||
/* CHECKS */ | ||
/* --------------------------------*/ | ||
checkStateVariableNamingConventions(data, file, filePath); | ||
checkVariableNamingConventions(data, file, filePath); | ||
// // checkShowModalNamingConventions(data, file, filePath); // todo needs to be defined better | ||
// checkShowModalNamingConventions(data, file, filePath); // todo needs to be defined better | ||
checkForRenderFunction(data, filePath); | ||
@@ -143,6 +99,6 @@ checkForBooleanTruthyDetection(data, filePath); | ||
checkForgottenTodos(data, filePath); | ||
if (isInterfaceFile(filePath)) { | ||
if (utils.isInterfaceFile(filePath)) { | ||
checkInterfaceNamingConventions(data, file); | ||
} | ||
if (isComponentFile(data, filePath)) { | ||
if (utils.isComponentFile(data, filePath)) { | ||
checkComponentNamingConventions(data, file, filePath); | ||
@@ -176,23 +132,37 @@ } | ||
function replaceBracketPattern(data: string) { | ||
// CRITERIA: Object string props should not have curly braces if no logical operations are performed on the string | ||
const regex = /={"([^+}]+)"}/g; | ||
try { | ||
// CRITERIA: Object string props should not have curly braces if no logical operations are performed on the string | ||
const regex = /={"([^+}]+)"}/g; | ||
const replacedContent = data.replace(regex, '="$1"'); | ||
return replacedContent; | ||
data = data.replace(regex, '="$1"'); | ||
} catch (e) { | ||
utils.writeOutput("error", `Could not replace bracket pattern: ${e}`); | ||
} | ||
return data; | ||
} | ||
function checkForRenderFunction(data: string, filePath: string) { | ||
// CRITERIA: All components should have a render function | ||
if ( | ||
isComponentFile(data, filePath) && | ||
data.indexOf("function render()") === -1 | ||
) { | ||
warnings.filesMissingRenderFunction.push({ file: filePath }); | ||
try { | ||
// CRITERIA: All components should have a render function | ||
if ( | ||
utils.isComponentFile(data, filePath) && | ||
data.indexOf("function render()") === -1 | ||
) { | ||
warnings.filesMissingRenderFunction.push({ file: filePath }); | ||
} | ||
} catch (e) { | ||
utils.writeOutput("error", `Could not check for render function: ${e}`); | ||
} | ||
} | ||
function checkForBooleanTruthyDetection(data: string, filePath: string) { | ||
// CRITERIA: Prefer boolean truthy detection Boolean(x) over double !! | ||
if (data.indexOf("!!") > -1) { | ||
warnings.incorrectTruthy.push({ file: filePath }); | ||
try { | ||
// CRITERIA: Prefer boolean truthy detection Boolean(x) over double !! | ||
if (data.indexOf("!!") > -1) { | ||
warnings.incorrectTruthy.push({ file: filePath }); | ||
} | ||
} catch (e) { | ||
utils.writeOutput( | ||
"error", | ||
`Could not check for boolean truthy detection: ${e}` | ||
); | ||
} | ||
@@ -202,5 +172,9 @@ } | ||
function checkForClassComponent(data: string, filePath: string) { | ||
// CRITERIA: Make use of functional components instead of class components | ||
if (data.indexOf("extends Component") > -1) { | ||
warnings.classComponents.push({ file: filePath }); | ||
try { | ||
// CRITERIA: Make use of functional components instead of class components | ||
if (data.indexOf("extends Component") > -1) { | ||
warnings.classComponents.push({ file: filePath }); | ||
} | ||
} catch (e) { | ||
utils.writeOutput("error", `Could not check for class component: ${e}`); | ||
} | ||
@@ -210,21 +184,19 @@ } | ||
function checkInterfaceNamingConventions(data: string, file: string) { | ||
// CRITERIA: Interface file name should follow the pattern <someInterfaceName>.interface.ts | ||
const interfaceFileNamePattern = /^[a-z][A-Za-z]*\.interface\.ts$/; | ||
try { | ||
// CRITERIA: Interface file name should follow the pattern <someInterfaceName>.interface.ts | ||
if (!namingConventionUtils.isValidInterfaceFileName(file)) { | ||
errors.incorrectInterfaceFileNames.push({ file }); | ||
} | ||
if (!interfaceFileNamePattern.test(file)) { | ||
errors.incorrectInterfaceFileNames.push({ file }); | ||
// CRITERIA: Interface name should follow the pattern I<SomeInterfaceName> | ||
let interfaceName = utils.getInterfaceName(data); | ||
if (!namingConventionUtils.isValidInterfaceName(interfaceName)) { | ||
errors.incorrectInterfaceNames.push({ file, error: interfaceName }); | ||
} | ||
} catch (e) { | ||
utils.writeOutput( | ||
"error", | ||
`Could not check interface naming conventions: ${e}` | ||
); | ||
} | ||
// CRITERIA: Interface name should follow the pattern I<SomeInterfaceName> | ||
const interfaceNamePattern = /^I[A-Z][a-zA-Z]*$/; | ||
let interfaceNameStartIndex = | ||
data.indexOf("export interface ") + "export interface ".length; | ||
let substring = data.slice(interfaceNameStartIndex); | ||
let interfaceNameEndIndex = substring.indexOf(" "); | ||
let interfaceName = substring.slice(0, interfaceNameEndIndex); | ||
if (!interfaceNamePattern.test(interfaceName)) { | ||
errors.incorrectInterfaceNames.push({ file, error: interfaceName }); | ||
} | ||
} | ||
@@ -237,33 +209,37 @@ | ||
) { | ||
if (filePath.indexOf("/pages") > -1) { | ||
// CRITERIA: Page component file name should follow the pattern <SomePageName>Page.tsx | ||
const pageComponentFileNamePattern = /^[A-Z][A-Za-z0-9]*\Page\.tsx$/; | ||
if (!pageComponentFileNamePattern.test(file)) { | ||
errors.incorrectComponentFileNames.push({ file }); | ||
try { | ||
if (filePath.indexOf("/pages") > -1) { | ||
// CRITERIA: Page component file name should follow the pattern <SomePageName>Page.tsx | ||
if (!namingConventionUtils.isValidPageComponentFileName(file)) { | ||
errors.incorrectComponentFileNames.push({ file }); | ||
} | ||
} else if (filePath.indexOf("/components") > -1) { | ||
// CRITERIA: Component file name should follow the pattern <SomeComponentName>.tsx | ||
if (!namingConventionUtils.isValidComponentFileName(file)) { | ||
errors.incorrectComponentFileNames.push({ file }); | ||
} | ||
} | ||
} else if (filePath.indexOf("/components") > -1) { | ||
// CRITERIA: Component file name should follow the pattern <SomeComponentName>.tsx | ||
const componentFileNamePattern = /^[A-Z][A-Za-z0-9]*\.tsx$/; | ||
if (!componentFileNamePattern.test(file)) { | ||
errors.incorrectComponentFileNames.push({ file }); | ||
} | ||
} | ||
if (filePath.indexOf("/pages") > -1 || filePath.indexOf("/components") > -1) { | ||
// CRITERIA: Component name should be upper camel case | ||
const componentNamePattern = /function\s+([a-zA-Z0-9]*)\(/; | ||
let match = componentNamePattern.exec(data); | ||
if (match) { | ||
let componentName = match[1]; | ||
if (!upperCamelCaseRegex.test(componentName)) { | ||
errors.incorrectComponentNames.push({ file, error: componentName }); | ||
if ( | ||
filePath.indexOf("/pages") > -1 || | ||
filePath.indexOf("/components") > -1 | ||
) { | ||
// CRITERIA: Component name should be upper camel case | ||
let componentName = utils.getComponentName(data); | ||
if (componentName) { | ||
if (!namingConventionUtils.isValidComponentName(componentName)) { | ||
errors.incorrectComponentNames.push({ file, error: componentName }); | ||
} | ||
} else { | ||
errors.incorrectComponentNames.push({ | ||
file, | ||
error: "No component name found", | ||
}); | ||
} | ||
} else { | ||
errors.incorrectComponentNames.push({ | ||
file, | ||
error: "No component name found", | ||
}); | ||
} | ||
} catch (e) { | ||
utils.writeOutput( | ||
"error", | ||
`Could not check component naming conventions: ${e}` | ||
); | ||
} | ||
@@ -277,24 +253,23 @@ } | ||
) { | ||
// CRITERIA: State variables should be camel case | ||
const stateVariableRegex = /(?<=\[\s*)(\w+)(?=\s*,\s*set\w+\s*\])/gm; | ||
const variableNames = []; | ||
let match; | ||
while ((match = stateVariableRegex.exec(data)) !== null) { | ||
variableNames.push(match[1]); | ||
try { | ||
// CRITERIA: State variables should be camel case | ||
let variableNames: string[] = utils.getStateVariables(data); | ||
variableNames.forEach((variableName) => { | ||
if ( | ||
!namingConventionUtils.isValidStateVariableName(variableName) && | ||
variableName !== file.split(".tsx").join("") && | ||
!(filePath.includes("/Routes") && variableName.includes("Page")) | ||
) { | ||
warnings.incorrectlyNamedStateVariables.push({ | ||
file: filePath, | ||
error: variableName, | ||
}); | ||
} | ||
}); | ||
} catch (e) { | ||
utils.writeOutput( | ||
"error", | ||
`Could not check state variable naming conventions: ${e}` | ||
); | ||
} | ||
variableNames.forEach((variableName) => { | ||
if ( | ||
!camelCaseRegex.test(variableName) && | ||
variableName !== file.split(".tsx").join("") && | ||
!(filePath.includes("/Routes") && variableName.includes("Page")) | ||
) { | ||
warnings.incorrectlyNamedStateVariables.push({ | ||
file: filePath, | ||
error: variableName, | ||
}); | ||
} | ||
}); | ||
} | ||
@@ -307,25 +282,24 @@ | ||
) { | ||
// CRITERIA: Variables should be camel case or upper snake case | ||
const variableRegex = /\b(?:let|const|var)\s+([a-zA-Z_$][a-zA-Z0-9_$]*)\b/g; | ||
try { | ||
// CRITERIA: Variables should be camel case or upper snake case | ||
let variableNames: string[] = utils.getVariables(data); | ||
const variableNames = []; | ||
let match; | ||
while ((match = variableRegex.exec(data)) !== null) { | ||
variableNames.push(match[1]); | ||
variableNames.forEach((variableName) => { | ||
if ( | ||
!namingConventionUtils.isValidVariableName(variableName) && | ||
variableName !== file.split(".tsx").join("") && | ||
!(filePath.includes("/Routes") && variableName.includes("Page")) | ||
) { | ||
warnings.incorrectlyNamedVariables.push({ | ||
file: filePath, | ||
error: variableName, | ||
}); | ||
} | ||
}); | ||
} catch (e) { | ||
utils.writeOutput( | ||
"error", | ||
`Could not check variable naming conventions: ${e}` | ||
); | ||
} | ||
variableNames.forEach((variableName) => { | ||
if ( | ||
!camelCaseRegex.test(variableName) && | ||
!upperSnakeCaseRegex.test(variableName) && | ||
variableName !== file.split(".tsx").join("") && | ||
!(filePath.includes("/Routes") && variableName.includes("Page")) | ||
) { | ||
warnings.incorrectlyNamedVariables.push({ | ||
file: filePath, | ||
error: variableName, | ||
}); | ||
} | ||
}); | ||
} | ||
@@ -338,21 +312,28 @@ | ||
) { | ||
// CRITERIA: When naming state variables to show/hide modals, make use of const [modalToShow, setModalToShow] = useState<string>("") or const [shouldShowXModal, setShouldShowXModal] = useState<boolean>(false) | ||
const stateVariableRegex = /(?<=\[\s*)(\w+)(?=\s*,\s*set\w+\s*\])/gm; | ||
try { | ||
// CRITERIA: When naming state variables to show/hide modals, make use of const [modalToShow, setModalToShow] = useState<string>("") or const [shouldShowXModal, setShouldShowXModal] = useState<boolean>(false) | ||
const stateVariableRegex = /(?<=\[\s*)(\w+)(?=\s*,\s*set\w+\s*\])/gm; | ||
let match; | ||
let match; | ||
const shouldShowXModalRegex = /^shouldShow[A-Z][a-zA-Z]*Modal$/; | ||
const shouldShowXModalRegex = /^shouldShow[A-Z][a-zA-Z]*Modal$/; | ||
while ((match = stateVariableRegex.exec(data)) !== null) { | ||
let stateVariableName = match[1]; | ||
if ( | ||
stateVariableName.toLowerCase().includes("modal") && | ||
!shouldShowXModalRegex.test(stateVariableName) && | ||
stateVariableName !== "modalToShow" | ||
) { | ||
warnings.incorrectlyNamedShowModalVariables.push({ | ||
file: filePath, | ||
error: stateVariableName, | ||
}); | ||
while ((match = stateVariableRegex.exec(data)) !== null) { | ||
let stateVariableName = match[1]; | ||
if ( | ||
stateVariableName.toLowerCase().includes("modal") && | ||
!shouldShowXModalRegex.test(stateVariableName) && | ||
stateVariableName !== "modalToShow" | ||
) { | ||
warnings.incorrectlyNamedShowModalVariables.push({ | ||
file: filePath, | ||
error: stateVariableName, | ||
}); | ||
} | ||
} | ||
} catch (e) { | ||
utils.writeOutput( | ||
"error", | ||
`Could not check modal naming conventions: ${e}` | ||
); | ||
} | ||
@@ -362,16 +343,9 @@ } | ||
function addRenderMethodsComment(data: string, filePath: string) { | ||
// CRITERIA: All components should have a comment indicating where the render methods section starts | ||
let renderMethodsCommentText = `/* --------------------------------*/ | ||
/* RENDER METHODS */ | ||
/* --------------------------------*/`; | ||
if (isComponentFile(data, filePath)) { | ||
if (data.indexOf("RENDER METHODS") === -1) { | ||
let firstRenderFunctionIndex = data.indexOf("function render"); | ||
if (firstRenderFunctionIndex > -1) { | ||
const part1 = data.slice(0, firstRenderFunctionIndex); | ||
const part2 = data.slice(firstRenderFunctionIndex); | ||
data = part1 + renderMethodsCommentText + "\n" + "\n" + part2; | ||
} | ||
try { | ||
// CRITERIA: All components should have a comment indicating where the render methods section starts | ||
if (utils.isComponentFile(data, filePath)) { | ||
data = commentUtils.addRenderMethodsComment(data); | ||
} | ||
} catch (e) { | ||
utils.writeOutput("error", `Could not add render methods comment: ${e}`); | ||
} | ||
@@ -383,21 +357,6 @@ | ||
function fixLodashImports(data: string, filePath: string) { | ||
let importAll = 'import _ from "lodash";'; | ||
if (data.includes(importAll)) { | ||
const lodashFunctionRegex = /_\.\w+[A-Za-z]*\(/g; | ||
const lodashFunctions = data.match(lodashFunctionRegex); | ||
let functionNames: string[] = []; | ||
lodashFunctions?.forEach((functionString: string) => { | ||
let functionName = functionString.substring(2, functionString.length - 1); | ||
functionNames.push(functionName); | ||
data = data.replace(functionString, functionName + "("); | ||
let newImport = `import ${functionName} from "lodash/${functionName}";`; | ||
if (!data.includes(newImport)) { | ||
// prevent newImport from being added multiple times | ||
data = data.replace(importAll, `${importAll}\n${newImport}`); | ||
} | ||
}); | ||
data = data.replace(importAll, ""); | ||
try { | ||
data = importUtils.fixLodashImports(data); | ||
} catch (e) { | ||
utils.writeOutput("error", `Could not fix lodash imports: ${e}`); | ||
} | ||
@@ -408,34 +367,23 @@ | ||
function kebabToUpperCase(str: string) { | ||
// Split the string into individual words | ||
const words = str.split("-"); | ||
// Capitalize each word (except the first one) | ||
const upperCamelCaseWords = words.map((word) => { | ||
return word.charAt(0).toUpperCase() + word.slice(1); | ||
}); | ||
// Join the words and return the upper camel case string | ||
return upperCamelCaseWords.join(""); | ||
} | ||
function listMissingFontawesomeImports(data: string) { | ||
let regex = /icon="[^"]*"/g; | ||
try { | ||
let importNames = utils.getFontawesomeImportNames(data); | ||
importNames?.forEach((importName: string) => { | ||
if ( | ||
!allImportNames.includes(importName) && | ||
!setupIconsContent.includes(importName) | ||
) { | ||
errors.missingFontawesomeIconImports.push({ | ||
error: `Missing import for ${importName}`, | ||
}); | ||
allImportNames.push(importName); | ||
} | ||
}); | ||
} catch (e) { | ||
utils.writeOutput( | ||
"error", | ||
`Could not list missing Fontawesome imports: ${e}` | ||
); | ||
} | ||
let matches = data.match(regex); | ||
matches?.forEach((match: string) => { | ||
let iconString = match.replace('icon="', "").replace('"', ""); | ||
let importName = "fa" + kebabToUpperCase(iconString); | ||
if ( | ||
!allImportNames.includes(importName) && | ||
!setupIconsContent.includes(importName) | ||
) { | ||
errors.missingFontawesomeIconImports.push({ | ||
error: `Missing import for ${importName}`, | ||
}); | ||
allImportNames.push(importName); | ||
} | ||
}); | ||
return data; | ||
@@ -477,29 +425,11 @@ } | ||
function checkForgottenTodos(data: string, filePath: string) { | ||
const regex = /\bTODO\b/gi; | ||
let matches = data.match(regex); | ||
if (matches) { | ||
warnings.forgottenTodos.push({ file: filePath }); | ||
try { | ||
if (commentUtils.containsTodo(data)) { | ||
warnings.forgottenTodos.push({ file: filePath }); | ||
} | ||
} catch (e) { | ||
utils.writeOutput("error", `Could not check forgotten todos: ${e}`); | ||
} | ||
} | ||
function logErrors( | ||
type: "error" | "warning", | ||
errorSectionName: string, | ||
errors: IErrorObject[] | ||
) { | ||
let char = "-"; | ||
writeOutput(type, char.repeat(errorSectionName.length)); | ||
writeOutput(type, errorSectionName.toUpperCase()); | ||
writeOutput(type, char.repeat(errorSectionName.length)); | ||
errors.forEach((err) => { | ||
console.log( | ||
"\t" + | ||
(err.file ?? "") + | ||
(err.error && err.file ? " - " : "") + | ||
(err.error ? `${err.error}` : "") | ||
); | ||
}); | ||
} | ||
async function getSetupIconsContent() { | ||
@@ -521,6 +451,6 @@ return new Promise((resolve, reject) => { | ||
if (!args.folderPath) { | ||
writeOutput("error", "No path specified"); | ||
utils.writeOutput("error", "No path specified"); | ||
return; | ||
} | ||
writeOutput("info", "Running code checker..."); | ||
utils.writeOutput("info", "Running code checker..."); | ||
return getSetupIconsContent().then(async () => { | ||
@@ -537,3 +467,3 @@ return readTSXFilesRecursively(folderPath) | ||
errorArray.length > 0 && | ||
logErrors("error", keyToHumanReadable(key), errorArray); | ||
utils.logErrors("error", utils.keyToHumanReadable(key), errorArray); | ||
}); | ||
@@ -544,10 +474,14 @@ Object.keys(warnings).forEach((key) => { | ||
warningArray.length > 0 && | ||
logErrors("warning", keyToHumanReadable(key), warningArray); | ||
utils.logErrors( | ||
"warning", | ||
utils.keyToHumanReadable(key), | ||
warningArray | ||
); | ||
}); | ||
if (errorCount === 0) { | ||
writeOutput("info", "Done running code checker."); | ||
utils.writeOutput("info", "Done running code checker."); | ||
process.exit(0); | ||
} else { | ||
writeOutput("error", "Done running code checker."); | ||
utils.writeOutput("error", "Done running code checker."); | ||
process.exit(1); | ||
@@ -557,3 +491,3 @@ } | ||
.catch((err) => { | ||
writeOutput("error", err); | ||
utils.writeOutput("error", err); | ||
process.exit(1); | ||
@@ -560,0 +494,0 @@ }); |
{ | ||
"name": "bob-group-frontend-coding-standards", | ||
"version": "1.0.3", | ||
"version": "1.0.4", | ||
"description": "Script to check that frontend code follows coding standards", | ||
"main": "index.js", | ||
"main": "dist/index.js", | ||
"devDependencies": { | ||
@@ -7,0 +7,0 @@ "@types/node": "^20.9.0", |
@@ -8,3 +8,4 @@ { | ||
"strict": true, | ||
"skipLibCheck": true | ||
"skipLibCheck": true, | ||
"outDir": "./dist" | ||
}, | ||
@@ -11,0 +12,0 @@ "types": ["node"], |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
49246
14
1267
1