dce-dev-wizard
Version:
Wizard for managing development apps at Harvard DCE.
313 lines (264 loc) • 9.3 kB
text/typescript
import fs from 'fs';
import path from 'path';
// Error code regex
const errorCodeRegex = /new ErrorWithCode\(\s*(.+),\s*(.+)\s*\)(;|,)/;
// Get the project directory
const pwd = path.join(process.env.PWD || process.env.CWD || __dirname, '../..');
/*------------------------------------------------------------------------*/
/* -------------------------------- Types ------------------------------- */
/*------------------------------------------------------------------------*/
// An error code and its implementation
type ErrorCode = {
// Parent code
parent: string,
// The error code
code: string,
// The machine-readable name of the error code
prop: string,
// If true, found in any capacity
found: boolean,
// Appearances
appearances: {
// Error message code
messageTemplate: string,
// File path
filePath: string,
}[],
};
/*------------------------------------------------------------------------*/
/* ------------------------------- Helpers ------------------------------ */
/*------------------------------------------------------------------------*/
/**
* Get all paths to the error code types within a folder
* @author Gabe Abrams
* @param parentFolderPath - the path to the parent folder
* @returns an array of paths to the error code types
*/
const getErrorCodesPaths = (parentFolderPath: string): string[] => {
// Get all files in the folder
const files = fs.readdirSync(parentFolderPath);
// For each item in the folder, recurse if folder, analyze if file
const paths: string[] = [];
files.forEach((file) => {
// Get the full path
const fullPath = path.join(parentFolderPath, file);
// If folder, recurse
const isFolder = (
fs
.lstatSync(fullPath)
.isDirectory()
);
if (isFolder) {
// Skip if the "from-server" folder
if (file !== 'from-server') {
// Recurse
paths.push(...getErrorCodesPaths(fullPath));
}
} else {
// Not folder. Analyze
if (file.includes('ErrorCode') && (file.endsWith('.ts') || file.endsWith('.tsx'))) {
// Found an error code file
paths.push(fullPath);
}
}
});
// Return all accumulated paths
return paths;
};
/**
* Read an error code file and return the error codes within it
* @author Gabe Abrams
* @param filePath - the path to the error code file
* @returns an array of error codes
*/
const readErrorCodes = (filePath: string): ErrorCode[] => {
// Read the file
const fileContents = fs.readFileSync(filePath, 'utf8');
// Get the name of the error code file
const parent = (
(filePath.split('/').pop() || 'UnknownErrorCodeParent')
.replace('.tsx', '')
.replace('.ts', '')
);
// Split the file into lines
const lines = fileContents.split('\n');
// Find all error codes
const errorCodes: ErrorCode[] = [];
lines.forEach((line, lineNumber) => {
// Format of an error code:
// DescriptionOfErrorCode = 'CODE',
// Check if this line is an error code
const matches = line.trim().match(/(\w+) = '(\w+)',/);
if (matches) {
// Found an error code
const name = matches[1];
const code = matches[2];
// Add to error codes
errorCodes.push({
parent,
code,
found: false,
prop: name,
appearances: [],
});
}
});
// Return all error codes
return errorCodes;
};
/*------------------------------------------------------------------------*/
/* -------------------------------- Main -------------------------------- */
/*------------------------------------------------------------------------*/
/*----------------------------------------*/
/* ---------- Parse Error Codes --------- */
/*----------------------------------------*/
console.log('\nUpdating error docs...');
// Get error code file paths
const serverPaths = getErrorCodesPaths(path.join(pwd, '/server/src'));
const clientPaths = getErrorCodesPaths(path.join(pwd, '/client/src'));
const errorCodePaths = serverPaths.concat(clientPaths);
// Read each and process the enums
const errorCodes: ErrorCode[] = [];
errorCodePaths.forEach((filePath) => {
errorCodes.push(...readErrorCodes(filePath));
});
/*----------------------------------------*/
/* -------- Validate Error Codes -------- */
/*----------------------------------------*/
// Display fatal error if duplicate error codes exist
const errorCodeSet = new Set<string>();
errorCodes.forEach((errorCode) => {
if (errorCodeSet.has(errorCode.code)) {
console.error(`Duplicate error code defined: ${errorCode.code} in ${errorCode.parent}. Fatal error. Exiting!`);
process.exit(1);
}
});
/*----------------------------------------*/
/* ---------- Find Appearances ---------- */
/*----------------------------------------*/
/**
* Recursively look for appearances of error codes
* @author Gabe Abrams
* @param folderPath - the path to the folder to search
*/
const findAppearances = (folderPath: string) => {
// Format of an error appearance:
// new ErrorWithCode(
// `Message template with ${templateVariables} in it perhaps`,
// ErrorCodeParent.ErrorCodePropName,
// );
// Get all files in the folder
const files = fs.readdirSync(folderPath);
// For each item in the folder, recurse if folder, analyze if file
files.forEach((file) => {
// Get the full path
const fullPath = path.join(folderPath, file);
// If folder, recurse
const isFolder = (
fs
.lstatSync(fullPath)
.isDirectory()
);
if (isFolder) {
// Recurse
findAppearances(fullPath);
} else {
// Not folder. Analyze
if (
(file.endsWith('.ts') || file.endsWith('.tsx'))
&& !errorCodePaths.includes(fullPath)
) {
// Read the file
let fileContents = fs.readFileSync(fullPath, 'utf8');
// Check if found
errorCodes.find((errorCode) => {
if (
fileContents.includes(`${errorCode.parent}.${errorCode.prop},`)
|| fileContents.includes(`${errorCode.parent}.${errorCode.prop};`)
|| fileContents.includes(`${errorCode.parent}.${errorCode.prop} `)
|| fileContents.includes(`${errorCode.parent}.${errorCode.prop})`)
|| fileContents.includes(`${errorCode.parent}.${errorCode.prop}\n`)
) {
errorCode.found = true;
}
});
// Check if this line is an error appearance
let matches = fileContents.trim().match(errorCodeRegex);
while (matches) {
// Found an error appearance
const messageTemplate = matches[1];
const parentAndProp = matches[2];
// Find the error code
const errorCode = errorCodes.find((errorCode) => {
return (
`${errorCode.parent}.${errorCode.prop},` === parentAndProp.trim()
);
});
if (errorCode) {
// Add the appearance
errorCode.appearances.push({
messageTemplate,
filePath: fullPath,
});
}
// Remove this match
fileContents = fileContents.replace(matches[0], '');
matches = fileContents.trim().match(errorCodeRegex);
}
}
}
});
};
// Add appearances in each folder
findAppearances(path.join(pwd, '/server/src'));
findAppearances(path.join(pwd, '/client/src'));
/*------------------------------------------------------------------------*/
/* ----------------------------- Build Docs ----------------------------- */
/*------------------------------------------------------------------------*/
let docsMD = '';
/*----------------------------------------*/
/* -------------- Metadata -------------- */
/*----------------------------------------*/
const pkg = require(path.join(pwd, 'package.json'));
if (!pkg) {
console.log('package.json not found');
process.exit(1);
}
const projectName = pkg.name || 'This Project';
/*----------------------------------------*/
/* ---------------- Intro --------------- */
/*----------------------------------------*/
docsMD = `# Error Cheatsheet for ${projectName}\n`;
docsMD += 'This is an auto-generated document that outlines the friendly error codes that users may encounter when using this app.\n\n';
/*----------------------------------------*/
/* ---------------- Table --------------- */
/*----------------------------------------*/
// Header
docsMD += '\n| Code | Internal Name | Associated Message |\n';
docsMD += '| -------- | -------- | -------- |\n';
// Error codes
errorCodes.forEach((err) => {
// Skip ones that are not in use
if (!err.found) {
return;
}
let associatedMessage = (
err
.appearances
.map((appearance) => {
return `<div>${appearance.messageTemplate.replace('`', '\'')}<div style="font-size: 80%">${appearance.filePath.replace(pwd, '')}</div></div>`;
})
.join('<br>')
);
if (err.appearances.length === 0) {
associatedMessage = 'No additional information';
}
docsMD += `| ${err.code} | ${err.prop} | ${associatedMessage} |\n`;
});
// Finish
fs.writeFileSync(
path.join(pwd, 'docs/ErrorCodeDocs.md'),
docsMD,
'utf-8',
);
console.log('Success! Written to docs/ErrorCodeDocs.md');