diff --git a/README.md b/README.md index 2f82ebe..f4e47e0 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -banner +banner [![CI](https://github.com/DoneDeal0/codefather/actions/workflows/ci.yml/badge.svg)](https://github.com/DoneDeal0/codefather/actions/workflows/ci.yml) @@ -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 @@ -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 @@ -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.* +
# GITHUB ACTION diff --git a/cli/index.ts b/cli/index.ts index fcefb95..079b036 100755 --- a/cli/index.ts +++ b/cli/index.ts @@ -1,5 +1,4 @@ #!/usr/bin/env node import { runCheck } from "./run-check/index.js"; -export * from "@shared/models"; runCheck(); diff --git a/package-lock.json b/package-lock.json index c762e9f..cad91bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "@actions/github": "^6.0.1", "@octokit/rest": "^22.0.0", - "tsx": "^4.20.3" + "esbuild": "^0.25.8" }, "bin": { "codefather": "dist/index.js", @@ -6971,6 +6971,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -7046,7 +7047,10 @@ "version": "4.10.1", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "resolve-pkg-maps": "^1.0.0" }, @@ -13463,7 +13467,10 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, "license": "MIT", + "optional": true, + "peer": true, "funding": { "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } @@ -14909,7 +14916,10 @@ "version": "4.20.3", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.3.tgz", "integrity": "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ==", + "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" diff --git a/package.json b/package.json index b395390..ebf30d4 100644 --- a/package.json +++ b/package.json @@ -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", @@ -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", @@ -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", @@ -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", @@ -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" } } diff --git a/shared/loader/import-data-buffer.ts b/shared/loader/import-data-buffer.ts new file mode 100644 index 0000000..19494bc --- /dev/null +++ b/shared/loader/import-data-buffer.ts @@ -0,0 +1,3 @@ +export async function importDataBuffer(dataUrl: string) { + return import(dataUrl); +} diff --git a/shared/loader/index.ts b/shared/loader/index.ts index 6130ee8..7bcd087 100644 --- a/shared/loader/index.ts +++ b/shared/loader/index.ts @@ -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 { try { @@ -13,9 +14,13 @@ export async function loadConfig(): Promise { 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; } diff --git a/shared/loader/loader.test.ts b/shared/loader/loader.test.ts index 22c0f66..ad0b756 100644 --- a/shared/loader/loader.test.ts +++ b/shared/loader/loader.test.ts @@ -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, @@ -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 () => { @@ -25,7 +34,6 @@ 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 () => { @@ -33,22 +41,13 @@ describe("loadConfig", () => { 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?" + ); }); }); diff --git a/shared/parser/index.ts b/shared/parser/index.ts index a9de620..fb799e5 100644 --- a/shared/parser/index.ts +++ b/shared/parser/index.ts @@ -4,7 +4,7 @@ export function safeJSONParse(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." ); } } diff --git a/tsup.config.ts b/tsup.config.ts index b471ab9..598ad4a 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -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" }), }, ]);