UNPKG

standardlint

Version:
748 lines (711 loc) 24.6 kB
#!/usr/bin/env node "use strict"; var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( // If the importer is in node compatibility mode or this is not an ESM // file that has been converted to a CommonJS file using a Babel- // compatible transform (i.e. "__esModule" has not been set), then set // "default" to the CommonJS "module.exports" for node compatibility. isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod )); var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { createNewStandardLint: () => createNewStandardLint }); module.exports = __toCommonJS(src_exports); // src/application/getStatusCount.ts var getStatusCount = (status, results) => results.map((result) => { if (result.status === status) return result.status; }).filter((result) => result).length; // src/application/calculatePass.ts function calculatePass(predicatePassResult, severity) { if (!predicatePassResult && severity === "error") return "fail"; if (!predicatePassResult && severity === "warn") return "warn"; return "pass"; } // src/utils/exists.ts var import_node_fs = __toESM(require("fs")); var import_node_path = __toESM(require("path")); function exists(basePath, filePath = "", filetreePaths) { const fullPath = import_node_path.default.join(basePath, filePath); return filetreePaths && filetreePaths.length > 0 ? filetreePaths.includes(fullPath.replace("./", "")) : import_node_fs.default.existsSync(fullPath); } // src/checks/checkForConflictingLockfiles.ts function checkForConflictingLockfiles(severity, basePath, filetreePaths) { const name = "Lock files"; const message = "Check for conflicting lock files"; const npmLockfile = exists(basePath, "package-lock.json", filetreePaths); const yarnLockfile = exists(basePath, "yarn.lock", filetreePaths); const result = !(npmLockfile && yarnLockfile); return { name, status: calculatePass(result, severity), message, path: basePath }; } // src/utils/filterFiles.ts function filterFiles(files, ignorePaths) { return files.filter( (file) => ignorePaths.every((ignorePath) => { const localFilePath = file.replace(process.cwd(), ""); return !localFilePath.includes(ignorePath); }) ); } // src/utils/getAllFiles.ts var import_node_path3 = __toESM(require("path")); // src/utils/isDirectory.ts var import_node_fs2 = __toESM(require("fs")); function isDirectory(path5) { return import_node_fs2.default.statSync(path5).isDirectory(); } // src/utils/readDirectory.ts var import_node_fs3 = __toESM(require("fs")); var import_node_path2 = __toESM(require("path")); function readDirectory(basePath) { const _path = import_node_path2.default.join(basePath); if (exists(_path)) return import_node_fs3.default.readdirSync(_path); return []; } // src/utils/getAllFiles.ts function getAllFiles(directoryPath, arrayOfFiles) { const files = readDirectory(directoryPath); files.forEach((file) => { const filePath = `${directoryPath}/${file}`; if (isDirectory(filePath)) arrayOfFiles = getAllFiles(filePath, arrayOfFiles); else arrayOfFiles.push(import_node_path3.default.join(process.cwd(), "/", filePath)); }); return arrayOfFiles.filter((file) => file); } // src/utils/logDefaultPathMessage.ts function logDefaultPathMessage(checkName, path5) { console.warn( `\u{1F6CE}\uFE0F No custom path assigned to check "${checkName}" - Using default path "${path5}"...` ); } // src/utils/readFile.ts var import_node_fs4 = __toESM(require("fs")); function readFile(file) { return import_node_fs4.default.readFileSync(file, { encoding: "utf8" }); } // src/checks/checkForConsoleUsage.ts function checkForConsoleUsage(severity, basePath, customPath, ignorePaths) { const path5 = customPath || "src"; const name = "Console usage"; const message = "Check for console usage"; if (!customPath) logDefaultPathMessage(name, path5); const files = getAllFiles(`${basePath}/${path5}`, []); const filteredFiles = ignorePaths && ignorePaths.length > 0 ? filterFiles(files, ignorePaths) : files; const regex = /console.(.*)/gi; const includesConsole = filteredFiles.map( (test) => regex.test(readFile(test)) ); const result = !includesConsole.includes(true); return { name, status: calculatePass(result, severity), message, path: path5 }; } // src/utils/getJSONFileContents.ts var import_node_path4 = __toESM(require("path")); function getJSONFileContents(basePath, filePath) { try { const fullPath = import_node_path4.default.join(basePath, filePath); if (exists(fullPath)) return JSON.parse(readFile(fullPath)); return {}; } catch (error) { console.error("Unable to read contents of file...", error); return {}; } } // src/checks/checkForDefinedRelations.ts function checkForDefinedRelations(severity, basePath, customPath) { const path5 = customPath || "manifest.json"; const name = "Relations"; const message = "Check for defined relations"; if (!customPath) logDefaultPathMessage(name, path5); const serviceMetadata = getJSONFileContents( basePath, path5 ); const result = serviceMetadata?.relations && serviceMetadata?.relations.length > 0; return { name, status: calculatePass(result, severity), message, path: path5 }; } // src/checks/checkForDefinedServiceLevelObjectives.ts function checkForDefinedServiceLevelObjectives(severity, basePath, customPath) { const path5 = customPath || "manifest.json"; const name = "SLOs"; const message = "Check for defined Service Level Objectives"; if (!customPath) logDefaultPathMessage(name, path5); const serviceMetadata = getJSONFileContents( basePath, path5 ); const result = serviceMetadata?.slo && serviceMetadata?.slo.length > 0; return { name, status: calculatePass(result, severity), message, path: path5 }; } // src/checks/checkForDefinedTags.ts function checkForDefinedTags(severity, basePath, customPath) { const path5 = customPath || "manifest.json"; const name = "Tags"; const message = "Check for defined tags"; if (!customPath) logDefaultPathMessage(name, path5); const serviceMetadata = getJSONFileContents( basePath, path5 ); const result = serviceMetadata?.spec?.tags && serviceMetadata?.spec?.tags.length > 0; return { name, status: calculatePass(result, severity), message, path: path5 }; } // src/checks/checkForPresenceApiSchema.ts function checkForPresenceApiSchema(severity, basePath, customPath, filetreePaths) { const path5 = customPath || "api/schema.json"; const name = "API schema"; const message = "Check for API schema"; if (!customPath) logDefaultPathMessage(name, path5); const result = exists(basePath, path5, filetreePaths); return { name, status: calculatePass(result, severity), message, path: path5 }; } // src/checks/checkForPresenceChangelog.ts function checkForPresenceChangelog(severity, basePath, filetreePaths) { const path5 = "CHANGELOG.md"; const name = "Changelog"; const message = "Check for CHANGELOG file"; const result = exists(basePath, path5, filetreePaths); return { name, status: calculatePass(result, severity), message, path: path5 }; } // src/checks/checkForPresenceCiConfig.ts function checkForPresenceCiConfig(severity, basePath, customPath, filetreePaths) { const path5 = customPath || ".github/workflows/main.yml"; const name = "CI configuration"; const message = "Check for CI configuration file"; if (!customPath) logDefaultPathMessage(name, path5); const result = exists(basePath, path5, filetreePaths); return { name, status: calculatePass(result, severity), message, path: path5 }; } // src/checks/checkForPresenceCodeowners.ts function checkForPresenceCodeowners(severity, basePath, filetreePaths) { const path5 = "CODEOWNERS"; const name = "Code owners"; const message = "Check for CODEOWNERS file"; const result = exists(basePath, path5, filetreePaths); return { name, status: calculatePass(result, severity), message, path: path5 }; } // src/checks/checkForPresenceContributing.ts function checkForPresenceContributing(severity, basePath, filetreePaths) { const path5 = "CONTRIBUTING.md"; const name = "Contribution information"; const message = "Check for CONTRIBUTING file"; const result = exists(basePath, path5, filetreePaths); return { name, status: calculatePass(result, severity), message, path: path5 }; } // src/checks/checkForPresenceDiagramsFolder.ts function checkForPresenceDiagramsFolder(severity, basePath, customPath, filetreePaths) { const path5 = customPath || "diagrams"; const name = "Diagrams"; const message = "Check for diagrams folder with contents"; if (!customPath) logDefaultPathMessage(name, path5); const result = (() => { const diagramsPath = `${basePath}/${path5}`; if (filetreePaths && filetreePaths.length > 0) return hasDiagramMatches(filetreePaths, diagramsPath.replace("./", "")); if (exists(diagramsPath, "")) return hasDiagramMatches(readDirectory(diagramsPath)); return false; })(); return { name, status: calculatePass(result, severity), message, path: path5 }; } var hasDiagramMatches = (contents, startPath = "") => contents.map( (fileName) => fileName.startsWith(startPath) && fileName.endsWith(".drawio") ).filter((match) => match).length > 0; // src/checks/checkForPresenceIacConfig.ts function checkForPresenceIacConfig(severity, basePath, customPath, filetreePaths) { const path5 = customPath || "serverless.yml"; const name = "IAC configuration"; const message = "Check for Infrastructure-as-Code configuration"; if (!customPath) logDefaultPathMessage(name, path5); const result = exists(basePath, path5, filetreePaths); return { name, status: calculatePass(result, severity), message, path: path5 }; } // src/checks/checkForPresenceLicense.ts function checkForPresenceLicense(severity, basePath, filetreePaths) { const path5 = "LICENSE.md"; const name = "License"; const message = "Check for LICENSE file"; const result = exists(basePath, path5, filetreePaths); return { name, status: calculatePass(result, severity), message, path: path5 }; } // src/checks/checkForPresenceReadme.ts function checkForPresenceReadme(severity, basePath, filetreePaths) { const path5 = "README.md"; const name = "Documentation"; const message = "Check for README file"; const result = exists(basePath, path5, filetreePaths); return { name, status: calculatePass(result, severity), message, path: path5 }; } // src/checks/checkForPresenceSecurity.ts function checkForPresenceSecurity(severity, basePath, filetreePaths) { const path5 = "SECURITY.md"; const name = "Security information"; const message = "Check for SECURITY file"; const result = exists(basePath, path5, filetreePaths); return { name, status: calculatePass(result, severity), message, path: path5 }; } // src/checks/checkForPresenceServiceMetadata.ts function checkForPresenceServiceMetadata(severity, basePath, customPath, filetreePaths) { const path5 = customPath || "manifest.json"; const name = "Service metadata"; const message = "Check for service metadata file"; if (!customPath) logDefaultPathMessage(name, path5); const result = exists(basePath, path5, filetreePaths); return { name, status: calculatePass(result, severity), message, path: path5 }; } // src/checks/checkForPresenceTemplateIssues.ts function checkForPresenceTemplateIssues(severity, basePath, customPath, filetreePaths) { const path5 = customPath || ".github/ISSUE_TEMPLATE/issue.md"; const name = "Issue template"; const message = "Check for GitHub issue template"; if (!customPath) logDefaultPathMessage(name, path5); const result = exists(basePath, path5, filetreePaths); return { name, status: calculatePass(result, severity), message, path: path5 }; } // src/checks/checkForPresenceTemplatePullRequests.ts function checkForPresenceTemplatePullRequests(severity, basePath, customPath, filetreePaths) { const path5 = customPath || ".github/ISSUE_TEMPLATE/pull_request.md"; const name = "PR template"; const message = "Check for GitHub Pull Request template"; if (!customPath) logDefaultPathMessage(name, path5); const result = exists(basePath, path5, filetreePaths); return { name, status: calculatePass(result, severity), message, path: path5 }; } // src/checks/checkForPresenceTests.ts function checkForPresenceTests(severity, basePath, customPath, ignorePaths) { const path5 = customPath || "tests"; const name = "Tests"; const message = "Check for presence of tests"; if (!customPath) logDefaultPathMessage(name, path5); const files = getAllFiles(`${basePath}/${path5}`, []); const filteredFiles = ignorePaths && ignorePaths.length > 0 ? filterFiles(files, ignorePaths) : files; const tests = filteredFiles.filter( (file) => file.endsWith("test.ts") || file.endsWith("spec.ts") || file.endsWith("test.js") || file.endsWith("spec.js") ); const result = tests.length > 0; return { name, status: calculatePass(result, severity), message, path: path5 }; } // src/checks/checkForThrowingPlainErrors.ts function checkForThrowingPlainErrors(severity, basePath, customPath, ignorePaths) { const path5 = customPath || "src"; const name = "Error handling"; const message = "Check for presence of plain (non-custom) errors"; if (!customPath) logDefaultPathMessage(name, path5); const files = getAllFiles(`${basePath}/${path5}`, []); const filteredFiles = ignorePaths && ignorePaths.length > 0 ? filterFiles(files, ignorePaths) : files; const regex = /(throw Error|throw new Error)(.*)/gi; const includesError = filteredFiles.map( (test) => regex.test(readFile(test)) ); const result = !includesError.includes(true); return { name, status: calculatePass(result, severity), message, path: path5 }; } // src/application/errors/errors.ts var MissingChecksError = class extends Error { constructor() { super(); this.name = "MissingChecksError"; const message = "Missing checks!"; this.message = message; } }; var InvalidFiletreeError = class extends Error { constructor() { super(); this.name = "InvalidFiletreeError"; const message = "Invalid filetree provided: Must contain at least 1 string if a filetree is to be used."; this.message = message; } }; // src/utils/writeResultsToDisk.ts var import_node_fs5 = __toESM(require("fs")); function writeResultsToDisk(data) { const fileName = "standardlint.results.json"; import_node_fs5.default.writeFileSync( `${process.cwd()}/${fileName}`, JSON.stringify(data, null, " ") ); } // src/domain/StandardLint.ts function createNewStandardLint(config, filetree) { return new StandardLint(config, filetree); } var StandardLint = class { defaultBasePathFallback = "."; defaultSeverityFallback = "error"; defaultIgnorePathsFallback = []; config; filetree = []; constructor(config, filetree) { this.config = this.makeConfig(config); if (filetree) { if (this.validateFiletree(filetree)) this.filetree = filetree; else throw new InvalidFiletreeError(); } } /** * @description Validate that the filetree is a non-empty array with only strings. */ validateFiletree(filetree) { if (filetree && Array.isArray(filetree) && filetree.length > 0) { if (filetree.every((path5) => typeof path5 === "string")) return true; } return false; } /** * @description Validates and sanitizes user input and returns a valid Configuration. */ makeConfig(configInput) { const basePath = configInput?.basePath && exists(configInput.basePath) ? configInput.basePath : this.defaultBasePathFallback; const defaultSeverity = configInput?.defaultSeverity ? this.getValidatedSeverityLevel(configInput.defaultSeverity) : this.defaultSeverityFallback; const ignorePaths = configInput?.ignorePaths ? this.getSanitizedPaths(configInput.ignorePaths) : this.defaultIgnorePathsFallback; const checkList = Array.isArray(configInput?.checks) ? configInput?.checks : []; const checks = this.getValidatedChecks( checkList, defaultSeverity, ignorePaths ); return { basePath, checks, defaultSeverity }; } /** * @description Validates and sanitizes a requested Severity level. */ getValidatedSeverityLevel(severity) { const validSeverityLevels = ["warn", "error"]; if (validSeverityLevels.includes(severity)) return severity; return this.defaultSeverityFallback; } /** * @description Sanitizes provided paths or return empty array if none is provided. */ getSanitizedPaths(ignorePaths) { if (ignorePaths.length === 0) return []; return ignorePaths.filter((path5) => typeof path5 === "string"); } /** * @description Validates and sanitizes a requested list of checks. * * Provide `defaultSeverity` as it's not yet available in the class `config` object * when running the validation. */ getValidatedChecks(checks, defaultSeverity, ignorePaths) { const validCheckNames = [ "all", "checkForConflictingLockfiles", "checkForConsoleUsage", "checkForDefinedRelations", "checkForDefinedServiceLevelObjectives", "checkForDefinedTags", "checkForPresenceApiSchema", "checkForPresenceChangelog", "checkForPresenceCiConfig", "checkForPresenceCodeowners", "checkForPresenceContributing", "checkForPresenceDiagramsFolder", "checkForPresenceIacConfig", "checkForPresenceLicense", "checkForPresenceReadme", "checkForPresenceSecurity", "checkForPresenceServiceMetadata", "checkForPresenceTemplateIssues", "checkForPresenceTemplatePullRequests", "checkForPresenceTests", "checkForThrowingPlainErrors" ]; const isValidCheckName = (name) => validCheckNames.includes(name); if (checks.includes("all")) { checks = validCheckNames; validCheckNames.shift(); } const validatedChecks = checks.map((check) => { if (typeof check === "string" && isValidCheckName(check)) return { name: check, severity: defaultSeverity, ignorePaths }; if (typeof check === "object" && isValidCheckName(check.name)) return { name: check.name, path: check.path || "", severity: this.getValidatedSeverityLevel( check.severity || defaultSeverity ), ignorePaths }; return { name: "" }; }).filter((check) => check.name); return validatedChecks; } /** * @description Orchestrates the running of all checks. */ check(writeOutputToDisk = false) { if (this.config.checks.length === 0) throw new MissingChecksError(); const results = this.config.checks.map( (check) => this.test(check) ); const checkResults = { passes: getStatusCount("pass", results), warnings: getStatusCount("warn", results), failures: getStatusCount("fail", results), results }; if (writeOutputToDisk) writeResultsToDisk(checkResults); return checkResults; } /** * @description Run test on an individual Check. */ test(check) { const { name, severity, path: path5, ignorePaths } = check; const checksList = { checkForConflictingLockfiles: () => checkForConflictingLockfiles( severity, this.config.basePath, this.filetree ), checkForConsoleUsage: () => checkForConsoleUsage(severity, this.config.basePath, path5, ignorePaths), checkForDefinedRelations: () => checkForDefinedRelations(severity, this.config.basePath, path5), checkForDefinedServiceLevelObjectives: () => checkForDefinedServiceLevelObjectives( severity, this.config.basePath, path5 ), checkForDefinedTags: () => checkForDefinedTags(severity, this.config.basePath, path5), checkForPresenceApiSchema: () => checkForPresenceApiSchema( severity, this.config.basePath, path5, this.filetree ), checkForPresenceChangelog: () => checkForPresenceChangelog( severity, this.config.basePath, this.filetree ), checkForPresenceCiConfig: () => checkForPresenceCiConfig( severity, this.config.basePath, path5, this.filetree ), checkForPresenceCodeowners: () => checkForPresenceCodeowners( severity, this.config.basePath, this.filetree ), checkForPresenceContributing: () => checkForPresenceContributing( severity, this.config.basePath, this.filetree ), checkForPresenceDiagramsFolder: () => checkForPresenceDiagramsFolder( severity, this.config.basePath, path5, this.filetree ), checkForPresenceIacConfig: () => checkForPresenceIacConfig( severity, this.config.basePath, path5, this.filetree ), checkForPresenceLicense: () => checkForPresenceLicense(severity, this.config.basePath, this.filetree), checkForPresenceReadme: () => checkForPresenceReadme(severity, this.config.basePath, this.filetree), checkForPresenceSecurity: () => checkForPresenceSecurity(severity, this.config.basePath, this.filetree), checkForPresenceServiceMetadata: () => checkForPresenceServiceMetadata( severity, this.config.basePath, path5, this.filetree ), checkForPresenceTemplateIssues: () => checkForPresenceTemplateIssues( severity, this.config.basePath, path5, this.filetree ), checkForPresenceTemplatePullRequests: () => checkForPresenceTemplatePullRequests( severity, this.config.basePath, path5, this.filetree ), checkForPresenceTests: () => checkForPresenceTests( severity, this.config.basePath, path5, ignorePaths ), checkForThrowingPlainErrors: () => checkForThrowingPlainErrors( severity, this.config.basePath, path5, ignorePaths ) }; const result = checksList[name](); this.logResult(result); return result; } /** * @description Outputs a log with the check result. */ logResult(checkResult) { const { status, name } = checkResult; if (status === "pass") console.log("\u2705 PASS:", name); if (status === "warn") console.warn("\u26A0\uFE0F WARN:", name); if (status === "fail") console.error("\u274C FAIL:", name); } }; // src/index.ts function main() { const isRunFromCommandLine = process.argv[1]?.includes( "node_modules/.bin/standardlint" ); if (!isRunFromCommandLine) return; const writeOutputToDisk = process.argv[2]?.includes("--output"); try { console.log("Running StandardLint..."); const config = exists("standardlint.json") ? getJSONFileContents(process.cwd(), "standardlint.json") : {}; const standardlint = createNewStandardLint(config); const results = standardlint.check(); if (writeOutputToDisk) writeResultsToDisk(results); } catch (error) { console.error(error); } } main(); // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { createNewStandardLint });