fresh-onion
Version:
## What is this?
243 lines • 10.1 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateConfig = validateConfig;
exports.findFileRecursive = findFileRecursive;
exports.readConfig = readConfig;
exports.getTsFilesInLayer = getTsFilesInLayer;
exports.getImportsInTsFile = getImportsInTsFile;
exports.checkLayer = checkLayer;
exports.checkLayers = checkLayers;
exports.main = main;
const promises_1 = require("fs/promises");
const path = __importStar(require("path"));
const typescript_1 = __importDefault(require("typescript"));
let hasErrors = false;
function validateConfig(config) {
const { layers, rules } = config;
const layerNames = Object.keys(layers);
const ruleFromLayers = rules.map((rule) => rule.from);
for (const rule of rules) {
if (!layerNames.includes(rule.from)) {
console.log(`Rule from layer ${rule.from} does not exist`);
hasErrors = true;
return;
}
for (const allowedImport of rule.allowedImports) {
if (!layerNames.includes(allowedImport)) {
console.log(`Rule from layer ${rule.from} allows import from non-existent layer ${allowedImport}`);
hasErrors = true;
return;
}
}
}
for (const layer of layerNames) {
if (!ruleFromLayers.includes(layer)) {
console.log(`Layer ${layer} has no rules`);
hasErrors = true;
return;
}
}
}
function findFileRecursive(dir, targetFile) {
return __awaiter(this, void 0, void 0, function* () {
const entries = yield (0, promises_1.readdir)(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isFile() && entry.name === targetFile) {
return fullPath;
}
if (entry.isDirectory()) {
const found = yield findFileRecursive(fullPath, targetFile);
if (found)
return found;
}
}
return null;
});
}
function readConfig() {
return __awaiter(this, void 0, void 0, function* () {
const fileName = "onion.config.json";
const startDir = process.cwd();
try {
const configPath = yield findFileRecursive(startDir, fileName);
if (!configPath) {
throw new Error(`❌ Could not find ${fileName}`);
}
console.log(`Using config ${configPath}`);
const data = yield (0, promises_1.readFile)(configPath, "utf-8");
const config = JSON.parse(data);
validateConfig(config);
return config;
}
catch (err) {
throw new Error(`Failed to load config: ${err}`);
}
});
}
function getTsFilesInLayer(layerPath, baseDir) {
return __awaiter(this, void 0, void 0, function* () {
const results = [];
const absoluteLayerPath = path.resolve(baseDir, layerPath);
function walk(dir) {
return __awaiter(this, void 0, void 0, function* () {
const entries = yield (0, promises_1.readdir)(dir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
yield walk(fullPath);
}
else if (entry.isFile() &&
entry.name.endsWith(".ts") &&
!entry.name.endsWith(".d.ts")) {
results.push(fullPath);
}
}
});
}
yield walk(absoluteLayerPath);
return results;
});
}
;
function getImportsInTsFile(tsFile, baseDir) {
const sourceCode = typescript_1.default.sys.readFile(tsFile);
if (!sourceCode)
return [];
const sourceFile = typescript_1.default.createSourceFile(tsFile, sourceCode, typescript_1.default.ScriptTarget.Latest, true);
const imports = [];
sourceFile.forEachChild((node) => {
if (typescript_1.default.isImportDeclaration(node) &&
node.moduleSpecifier &&
typescript_1.default.isStringLiteral(node.moduleSpecifier)) {
const importPath = node.moduleSpecifier.text;
if (!importPath.startsWith(".") && !importPath.startsWith("/"))
return;
const tsFileDir = path.dirname(tsFile);
const absoluteImportPath = path.resolve(tsFileDir, importPath);
const normalizedPath = path.relative(baseDir, absoluteImportPath);
const line = typescript_1.default.getLineAndCharacterOfPosition(sourceFile, node.moduleSpecifier.getStart()).line;
const character = typescript_1.default.getLineAndCharacterOfPosition(sourceFile, node.moduleSpecifier.getStart()).character;
imports.push({
normalizedPath,
line,
character
});
}
});
return imports;
}
function checkLayer(layer, config, baseDir) {
return __awaiter(this, void 0, void 0, function* () {
var _a;
const layerPath = config.layers[layer];
const tsFiles = yield getTsFilesInLayer(layerPath, baseDir);
const rule = config.rules.find((rule) => rule.from === layer);
if (!rule) {
console.warn(`No rules defined for layer ${layer}`);
return;
}
for (const tsFile of tsFiles) {
const imports = getImportsInTsFile(tsFile, baseDir);
for (const imported of imports) {
const importedLayer = (_a = Object.entries(config.layers).find(([_, layerPath]) => {
const resolvedPath = path.resolve(baseDir, layerPath);
const absoluteImport = path.resolve(baseDir, imported.normalizedPath);
return absoluteImport.startsWith(resolvedPath);
})) === null || _a === void 0 ? void 0 : _a[0];
if (!importedLayer) {
console.log(`❓ Could not determine layer for import path: ${imported} in ${tsFile}`);
hasErrors = true;
continue;
}
if (importedLayer === layer) {
continue;
}
if (!rule.allowedImports.includes(importedLayer)) {
const filePath = path.relative(baseDir, tsFile) + ":" + imported.line + ":" + imported.character;
const importPath = path.relative(baseDir, imported.normalizedPath) + ".ts";
console.log(`❌ ${layer} (${filePath}) is importing from ${importedLayer} (${importPath})`);
hasErrors = true;
}
}
}
});
}
function checkLayers(config, baseDir) {
return __awaiter(this, void 0, void 0, function* () {
const layers = Object.keys(config.layers);
for (const layer of layers) {
yield checkLayer(layer, config, baseDir);
}
});
}
function main() {
return __awaiter(this, void 0, void 0, function* () {
const config = yield readConfig();
const configPath = yield findFileRecursive(process.cwd(), "onion.config.json");
if (!configPath) {
throw new Error("Config file not found.");
}
const baseDir = path.dirname(configPath);
yield checkLayers(config, baseDir);
if (hasErrors) {
console.log("👎 Rotten 🧅");
process.exit(1);
}
else {
console.log("👍 Fresh 🧅");
process.exit(0);
}
});
}
(() => __awaiter(void 0, void 0, void 0, function* () {
if (require.main === module) {
yield main();
}
}))();
//# sourceMappingURL=lib.js.map