Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<img width="1003" height="377" alt="banner" src="https://res.cloudinary.com/dmjisqsyo/image/upload/v1754334056/banner_cjwgin.png" />
<img width="1003" height="377" alt="banner" src="https://res.cloudinary.com/dmjisqsyo/image/upload/v1755025208/codfather_corrected_af9prv.png" />


[![CI](https://github.com/DoneDeal0/codefather/actions/workflows/ci.yml/badge.svg)](https://github.com/DoneDeal0/codefather/actions/workflows/ci.yml)
Expand Down Expand Up @@ -72,7 +72,7 @@ npm install @donedeal0/codefather --save-dev
- If a `.github/CODEOWNERS` file is present, it will be used to generate the config.
- Accepts two optional flags:
- `json`: generates a json config file instead of a `ts` one.
- `overwrite`: overwrite an existing codefather config.
- `overwrite`: overwrites an existing codefather config.
- example: `npm run codefather-init json overwrite`
- `codefather-github`: similar to `codefather`, but designed to run in a GitHub Action environment

Expand All @@ -87,8 +87,8 @@ You can either add a script shortcut in your `package.json`:
Or directly run the commands with `npx`:

```bash
npx codefather
npx codefather-init
npx codefather
```

## CONFIG
Expand Down Expand Up @@ -188,6 +188,18 @@ git config user.username # return DonCorleone

In a Github Action, `codefather` will use Github's API, so you don't have to worry about the git config.

## How to Write Rules

- Match all files in a folder (recursively): `src/myfolder/`
- Match a specific file: `src/myfolder/file.ts`
- Match files by extension in a folder (glob): `src/folder/*.css`
- Match files by extension in a folder (regex): `/^src\/folder\/.*\.css$/`
- Match any file in any subfolder: `src/**`
- Match dotfiles: `.env`
- Use `*` for single-level matches, `**` for recursive matches

ℹ️ *More examples are available in the test files. Codefather's matching patterns follow classic file matcher rules, like GitHub CODEOWNERS.*

<hr/>

# GITHUB ACTION
Expand Down
1 change: 0 additions & 1 deletion cli/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
#!/usr/bin/env node
import { runCheck } from "./run-check/index.js";
export * from "@shared/models";

runCheck();
12 changes: 11 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 12 additions & 21 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,24 +1,18 @@
{
"name": "@donedeal0/codefather",
"version": "1.0.0",
"version": "1.0.7",
"description": "Codefather protects your codebase by controlling who can change what. Set authorization levels, lock down files, and enforce your rules—offline via CLI or online with GitHub Actions.",
"license": "ISC",
"author": "DoneDeal0",
"files": [
"dist"
],
"main": "dist/index.cjs",
"module": "dist/index.js",
"type": "module",
"types": "dist/index.d.ts",
"publishConfig": {
"access": "public"
},
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
"bin": {
"codefather": "./dist/index.mjs",
"codefather-github": "./dist/scripts/github.mjs",
"codefather-init": "./dist/scripts/init.mjs"
},
"repository": {
"type": "git",
Expand All @@ -30,9 +24,11 @@
"funding": {
"type": "github",
"url": "https://github.com/sponsors/DoneDeal0"
},
"publishConfig": {
"access": "public"
},
"readme": "./README.md",
"type": "module",
"declaration": true,
"keywords": [
"codeowners",
Expand Down Expand Up @@ -74,11 +70,6 @@
"godfather",
"authorization"
],
"bin": {
"codefather": "./dist/index.js",
"codefather-github": "./dist/scripts/github.js",
"codefather-init": "./dist/scripts/init.js"
},
"scripts": {
"build": "tsup",
"codefather-github": "npm run build && node dist/scripts/github.js",
Expand All @@ -94,7 +85,7 @@
"dependencies": {
"@actions/github": "^6.0.1",
"@octokit/rest": "^22.0.0",
"tsx": "^4.20.3"
"esbuild": "^0.25.8"
},
"devDependencies": {
"@commitlint/cli": "^19.8.1",
Expand All @@ -114,7 +105,7 @@
"swc-loader": "^0.2.6",
"ts-node": "^10.9.2",
"tsup": "^8.5.0",
"typescript-eslint": "^8.38.0",
"typescript": "^5.8.3"
"typescript": "^5.8.3",
"typescript-eslint": "^8.38.0"
}
}
3 changes: 3 additions & 0 deletions shared/loader/import-data-buffer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export async function importDataBuffer(dataUrl: string) {
return import(dataUrl);
}
13 changes: 9 additions & 4 deletions shared/loader/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { transform } from "esbuild";
import fs from "fs";
import path from "path";
import { pathToFileURL } from "url";
import { getRandomMessage } from "@shared/messages";
import { MessageType, type CodefatherConfig } from "@shared/models";
import { safeJSONParse } from "@shared/parser";
import { importDataBuffer } from "./import-data-buffer";

export async function loadConfig(): Promise<CodefatherConfig> {
try {
Expand All @@ -13,9 +14,13 @@ export async function loadConfig(): Promise<CodefatherConfig> {
const jsonPath = path.resolve(root, "codefather.json");

if (fs.existsSync(tsPath)) {
const { register } = await import("tsx/esm/api");
register();
const config = await import(pathToFileURL(tsPath).href);
const tsCode = fs.readFileSync(tsPath, "utf-8");
const { code } = await transform(tsCode, {
loader: "ts",
format: "esm",
});
const dataUrl = `data:text/javascript;base64,${Buffer.from(code).toString("base64")}`;
const config = await importDataBuffer(dataUrl);
// a typescript file import may have several 'default' levels depending on the environment
return config?.default?.default || config?.default || config;
}
Expand Down
35 changes: 17 additions & 18 deletions shared/loader/loader.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,21 @@
import { writeFileSync, unlinkSync } from "fs";
import { writeFileSync, unlinkSync, existsSync } from "fs";
import { resolve } from "path";
import { loadConfig } from ".";

jest.mock("./import-data-buffer", () => ({
importDataBuffer: jest.fn(async () => ({
default: { rules: [{ match: ["src/**"], goodfellas: ["sonny"] }] },
})),
}));

const tsConfigPath = resolve(process.cwd(), "codefather.ts");
const jsonConfigPath = resolve(process.cwd(), "codefather.json");

describe("loadConfig", () => {
afterEach(() => {
if (existsSync(tsConfigPath)) unlinkSync(tsConfigPath);
if (existsSync(jsonConfigPath)) unlinkSync(jsonConfigPath);
});
test("returns config when codefather.ts exists", async () => {
writeFileSync(
tsConfigPath,
Expand All @@ -14,7 +24,6 @@ describe("loadConfig", () => {
};`
);
const result = await loadConfig();
unlinkSync(tsConfigPath);
expect(result?.rules?.[0]?.goodfellas).toEqual(["sonny"]);
});
test("returns config when codefather.json exists", async () => {
Expand All @@ -25,30 +34,20 @@ describe("loadConfig", () => {
})
);
const result = await loadConfig();
unlinkSync(jsonConfigPath);
expect(result?.rules?.[0]?.goodfellas).toEqual(["sonny"]);
});
test("throws an error if codefather.json is not properly formatted", async () => {
writeFileSync(
jsonConfigPath,
`{ rules: { match: ["src/**"] goodfellas: ["sonny"] }] }`
);
try {
await loadConfig();
} catch (err) {
unlinkSync(jsonConfigPath);
expect(err instanceof Error ? err.message : err).toBe(
"Your JSON file is invalid. You gotta respect the rules if you want my help."
);
}
await expect(loadConfig()).rejects.toThrow(
"Your codefather.json file is invalid. You gotta respect the rules if you want my help."
);
});
test("throws an error when no codefather.(ts|json) exists", async () => {
try {
await loadConfig();
} catch (err) {
expect(err instanceof Error ? err.message : err).toBe(
"𐄂 The codefather.ts file doesn't exist. Maybe someone whacked it?"
);
}
await expect(loadConfig()).rejects.toThrow(
"𐄂 The codefather.ts file doesn't exist. Maybe someone whacked it?"
);
});
});
2 changes: 1 addition & 1 deletion shared/parser/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ export function safeJSONParse<T>(json: string): T {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
} catch (_) {
throw new Error(
"Your JSON file is invalid. You gotta respect the rules if you want my help."
"Your codefather.json file is invalid. You gotta respect the rules if you want my help."
);
}
}
11 changes: 6 additions & 5 deletions tsup.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,19 @@ export default defineConfig([
"scripts/init": "scripts/init/index.ts",
"scripts/github": "scripts/github/index.ts",
},
format: ["cjs", "esm"],
dts: {
entry: ["cli/index.ts"],
entry: ["shared/models/index.ts"],
resolve: true,
},
splitting: true,
format: ["esm"],
splitting: false,
clean: true,
treeshake: true,
shims: true,
minify: true,
platform: "node",
name: "MAIN",
external: ["tsx"],
name: "CLI",
external: ["esbuild"],
outExtension: () => ({ js: ".mjs" }),
},
]);