standardlint
Version:
Extensible standards linter and auditor.
748 lines (711 loc) • 24.6 kB
JavaScript
;
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
});