Skip to content

Commit a29e698

Browse files
committed
chore: add postinstall script
1 parent 4606ed3 commit a29e698

File tree

8 files changed

+523
-393
lines changed

8 files changed

+523
-393
lines changed

bin/cli.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,15 @@ cli
1515
.example('git log -1 --pretty=%B | gitlint')
1616
.option('--verbose', 'Enable verbose output', { default: defaultConfig.verbose })
1717
.option('--config <file>', 'Path to config file')
18+
.option('--edit', 'Read commit message from a file (used by git hooks)')
1819
.action(async (files: string[], options) => {
1920
let commitMessage = ''
2021

2122
// Get commit message from files passed as arguments
22-
if (files.length > 0) {
23+
if (options.edit && files.length > 0) {
24+
commitMessage = readCommitMessageFromFile(files[0])
25+
}
26+
else if (files.length > 0) {
2327
commitMessage = readCommitMessageFromFile(files[0])
2428
}
2529
// Or from stdin if piped

build.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,10 @@ await Bun.build({
66
plugins: [dts()],
77
target: 'node',
88
})
9+
10+
await Bun.build({
11+
entrypoints: ['bin/cli.ts'],
12+
outdir: './dist/bin',
13+
plugins: [dts()],
14+
target: 'bun',
15+
})

bun.lock

Lines changed: 358 additions & 383 deletions
Large diffs are not rendered by default.

docs/.vitepress/components.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// @ts-nocheck
33
// Generated by unplugin-vue-components
44
// Read more: https://github.com/vuejs/core/pull/3399
5+
// biome-ignore lint: disable
56
export {}
67

78
/* prettier-ignore */

package.json

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,24 +59,29 @@
5959
"dev:docs": "bun --bun vitepress dev docs",
6060
"build:docs": "bun --bun vitepress build docs",
6161
"preview:docs": "bun --bun vitepress preview docs",
62-
"typecheck": "bun --bun tsc --noEmit"
62+
"typecheck": "bun --bun tsc --noEmit",
63+
"postinstall": "bun run scripts/postinstall.ts"
6364
},
6465
"devDependencies": {
6566
"@stacksjs/docs": "^0.70.23",
6667
"@stacksjs/eslint-config": "^4.10.2-beta.3",
6768
"@types/bun": "^1.2.12",
6869
"bumpp": "^10.1.0",
70+
"bun-git-hooks": "^0.2.13",
6971
"bun-plugin-dtsx": "^0.21.12",
7072
"bunfig": "^0.8.5",
7173
"changelogen": "^0.6.1",
72-
"lint-staged": "^15.5.2",
73-
"simple-git-hooks": "^2.13.0",
7474
"typescript": "^5.8.3"
7575
},
76+
"git-hooks": {
77+
"pre-commit": {
78+
"staged-lint": {
79+
"*.{js,ts,json,yaml,yml,md}": "bunx --bun eslint . --fix"
80+
}
81+
},
82+
"commit-msg": "bunx gitlint"
83+
},
7684
"overrides": {
7785
"unconfig": "0.3.10"
78-
},
79-
"lint-staged": {
80-
"*.{js,ts}": "bunx --bun eslint . --fix"
8186
}
8287
}

scripts/postinstall.ts

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
#!/usr/bin/env bun
2+
3+
import { mkdir, readFile, symlink } from 'node:fs/promises'
4+
import { join } from 'node:path'
5+
import process from 'node:process'
6+
7+
/**
8+
* Reads the nearest package.json and returns the package name.
9+
* @param rootDir Directory to start searching from (defaults to process.cwd())
10+
*/
11+
async function getPackageName(rootDir: string = process.cwd()): Promise<string | undefined> {
12+
try {
13+
const pkgPath = join(rootDir, 'package.json')
14+
const pkgJson = await readFile(pkgPath, 'utf8')
15+
const pkg = JSON.parse(pkgJson)
16+
return pkg.name
17+
}
18+
catch (err) {
19+
console.error(`[ERROR] Failed to get package name: ${err}`)
20+
// Optionally, walk up directories if not found, or just return undefined
21+
return undefined
22+
}
23+
}
24+
25+
async function getBinNames(rootDir: string = process.cwd()): Promise<string[] | undefined> {
26+
try {
27+
const pkgPath = join(rootDir, 'package.json')
28+
const pkgJson = await readFile(pkgPath, 'utf8')
29+
const pkg = JSON.parse(pkgJson)
30+
if (!pkg.bin)
31+
return undefined
32+
if (typeof pkg.bin === 'string') {
33+
// If bin is a string, the bin name is the package name
34+
return [pkg.name]
35+
}
36+
if (typeof pkg.bin === 'object') {
37+
// If bin is an object, the keys are the bin names
38+
return Object.keys(pkg.bin)
39+
}
40+
return undefined
41+
}
42+
catch (err) {
43+
console.error(`[ERROR] Failed to get bin names: ${err}`)
44+
return undefined
45+
}
46+
}
47+
48+
/*
49+
* Transforms the <project>/node_modules/bun-git-hooks to <project>
50+
*/
51+
async function getProjectRootDirectoryFromNodeModules(projectPath: string): Promise<string | undefined> {
52+
function _arraysAreEqual(a1: any[], a2: any[]) {
53+
return JSON.stringify(a1) === JSON.stringify(a2)
54+
}
55+
56+
const packageName = await getPackageName()
57+
if (packageName === undefined) {
58+
console.error('[ERROR] Failed to get package name')
59+
return undefined
60+
}
61+
62+
const binNames = await getBinNames()
63+
if (binNames === undefined) {
64+
console.error('[ERROR] Failed to get bin names')
65+
return undefined
66+
}
67+
68+
const projDir = projectPath.split(/[\\/]/) // <- would split both on '/' and '\'
69+
70+
const indexOfStoreDir = projDir.indexOf('.store')
71+
if (indexOfStoreDir > -1) {
72+
return projDir.slice(0, indexOfStoreDir - 1).join('/')
73+
}
74+
75+
// Handle .bin case for any bin name
76+
if (
77+
projDir.length > 3
78+
&& projDir[projDir.length - 3] === 'node_modules'
79+
&& projDir[projDir.length - 2] === '.bin'
80+
&& binNames.includes(projDir[projDir.length - 1])
81+
) {
82+
return projDir.slice(0, -3).join('/')
83+
}
84+
85+
// Existing node_modules check
86+
if (projDir.length > 2
87+
&& _arraysAreEqual(projDir.slice(-2), ['node_modules', packageName])) {
88+
return projDir.slice(0, -2).join('/')
89+
}
90+
91+
return undefined
92+
}
93+
94+
/**
95+
* Creates the pre-commit from command in config by default
96+
*/
97+
async function postinstall() {
98+
let projectDirectory
99+
100+
/* When script is run after install, the process.cwd() would be like <project_folder>/node_modules/simple-git-hooks
101+
Here we try to get the original project directory by going upwards by 2 levels
102+
If we were not able to get new directory we assume, we are already in the project root */
103+
const parsedProjectDirectory = await getProjectRootDirectoryFromNodeModules(process.cwd())
104+
if (parsedProjectDirectory !== undefined) {
105+
projectDirectory = parsedProjectDirectory
106+
}
107+
else {
108+
projectDirectory = process.cwd()
109+
}
110+
111+
// Link the binary
112+
const binDir = join(projectDirectory, 'node_modules', '.bin')
113+
await mkdir(binDir, { recursive: true })
114+
115+
const sourcePath = join(process.cwd(), 'dist', 'bin', 'cli.js')
116+
117+
const binNames = await getBinNames()
118+
if (binNames === undefined) {
119+
console.error('[ERROR] Failed to get bin names')
120+
return undefined
121+
}
122+
123+
for (const binName of binNames) {
124+
const targetPath = join(binDir, binName)
125+
try {
126+
await symlink(sourcePath, targetPath, 'file')
127+
}
128+
catch (err) {
129+
if ((err as NodeJS.ErrnoException).code !== 'EEXIST') {
130+
console.error(`[ERROR] Failed to link binary: ${err}`)
131+
}
132+
}
133+
}
134+
}
135+
136+
postinstall()

src/parser.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ export function parseCommitMessage(message: string): CommitParsedResult {
88
const header = lines[0] || ''
99

1010
// Parse header to extract type, scope, and subject
11-
const headerMatch = header.match(/^(?<type>\w+)(?:\((?<scope>[^)]+)\))?:(?<subject>.+)$/)
11+
const headerMatch = header.replace(/['"]/g, '').match(/^(?<type>\w+)(?:\((?<scope>[^)]+)\))?: ?(?<subject>.+)$/)
1212

1313
let type = null
1414
let scope = null

src/rules.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ const conventionalCommits: LintRule = {
66
description: 'Enforces conventional commit format',
77
validate: (commitMsg: string) => {
88
// Basic format: <type>[(scope)]: <description>
9-
const pattern = /^(?:build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(?:\([a-z0-9-]+\))?: .+/i
9+
const pattern = /^(?:build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(?:\([a-z0-9-]+\))?: .+$/i
1010

11-
if (!pattern.test(commitMsg.split('\n')[0])) {
11+
const firstLine = commitMsg.split('\n')[0]
12+
13+
if (!pattern.test(firstLine.replace(/['"]/g, ''))) {
1214
return {
1315
valid: false,
1416
message: 'Commit message header does not follow conventional commit format: <type>[(scope)]: <description>',

0 commit comments

Comments
 (0)