Add pre-commit verification hook

This commit is contained in:
Federico Jaramillo Martinez
2026-05-11 11:14:48 +02:00
parent c82486a2e5
commit 020b8f00ca
6 changed files with 55 additions and 22 deletions
+5
View File
@@ -0,0 +1,5 @@
#!/usr/bin/env sh
set -eu
echo "Running pre-commit checks: npm run verify"
npm run verify
+2 -1
View File
@@ -36,7 +36,8 @@
"prepack": "npm run build", "prepack": "npm run build",
"pack:dry": "npm pack --dry-run", "pack:dry": "npm pack --dry-run",
"prepublishOnly": "npm run verify", "prepublishOnly": "npm run verify",
"publish:npm": "npm publish --access public" "publish:npm": "npm publish --access public",
"prepare": "node scripts/install-git-hooks.mjs"
}, },
"dependencies": { "dependencies": {
"@codemirror/lang-css": "^6.3.1", "@codemirror/lang-css": "^6.3.1",
+14
View File
@@ -0,0 +1,14 @@
import { existsSync } from 'node:fs';
import { execFileSync } from 'node:child_process';
if (!existsSync('.git')) {
process.exit(0);
}
try {
execFileSync('git', ['config', 'core.hooksPath', '.githooks'], { stdio: 'inherit' });
console.log('Configured git hooks path: .githooks');
} catch (error) {
console.warn('Could not configure git hooks path. Run: git config core.hooksPath .githooks');
process.exitCode = 0;
}
+5 -5
View File
@@ -282,11 +282,11 @@ export class PiWebApp extends LitElement {
selectedStagedDiff: this.state.selectedStagedDiff, selectedStagedDiff: this.state.selectedStagedDiff,
gitStale: this.state.gitStale, gitStale: this.state.gitStale,
activeTerminalCount: this.state.activeTerminalCount, activeTerminalCount: this.state.activeTerminalCount,
onRefreshFiles: () => this.files.refreshFiles(), onRefreshFiles: () => { void this.files.refreshFiles(); },
onExpandDir: (path: string) => this.files.expandDir(path), onExpandDir: (path: string) => { void this.files.expandDir(path); },
onSelectFile: (path: string) => this.files.selectFile(path), onSelectFile: (path: string) => { void this.files.selectFile(path); },
onRefreshGit: () => this.git.refreshGit(), onRefreshGit: () => { void this.git.refreshGit(); },
onSelectDiff: (path: string) => this.git.selectDiff(path), onSelectDiff: (path: string) => { void this.git.selectDiff(path); },
}; };
} }
@@ -4,21 +4,21 @@ import { activateSelectableRow, activateSelectableRowFromKeyboard } from "./sele
describe("selectable row activation", () => { describe("selectable row activation", () => {
it("activates rows from non-interactive click targets", () => { it("activates rows from non-interactive click targets", () => {
const action = vi.fn(); const action = vi.fn();
activateSelectableRow(eventWithPath({ matches: () => false }), action); activateSelectableRow(eventWithPath(matchTarget(() => false)), action);
expect(action).toHaveBeenCalledOnce(); expect(action).toHaveBeenCalledOnce();
}); });
it("preserves contributed links and other interactive elements", () => { it("preserves contributed links and other interactive elements", () => {
const action = vi.fn(); const action = vi.fn();
activateSelectableRow(eventWithPath({ matches: (selector: string) => selector.includes("a[href]") }), action); activateSelectableRow(eventWithPath(matchTarget((selector: string) => selector.includes("a[href]"))), action);
expect(action).not.toHaveBeenCalled(); expect(action).not.toHaveBeenCalled();
}); });
it("activates rows from Enter and Space", () => { it("activates rows from Enter and Space", () => {
const enterAction = vi.fn(); const enterAction = vi.fn();
const spaceAction = vi.fn(); const spaceAction = vi.fn();
const enter = keyboardEventWithPath("Enter", { matches: () => false }); const enter = keyboardEventWithPath("Enter", matchTarget(() => false));
const space = keyboardEventWithPath(" ", { matches: () => false }); const space = keyboardEventWithPath(" ", matchTarget(() => false));
activateSelectableRowFromKeyboard(enter, enterAction); activateSelectableRowFromKeyboard(enter, enterAction);
activateSelectableRowFromKeyboard(space, spaceAction); activateSelectableRowFromKeyboard(space, spaceAction);
@@ -31,7 +31,7 @@ describe("selectable row activation", () => {
it("does not activate rows from keyboard events inside interactive elements", () => { it("does not activate rows from keyboard events inside interactive elements", () => {
const action = vi.fn(); const action = vi.fn();
const event = keyboardEventWithPath("Enter", { matches: (selector: string) => selector.includes("button") }); const event = keyboardEventWithPath("Enter", matchTarget((selector: string) => selector.includes("button")));
activateSelectableRowFromKeyboard(event, action); activateSelectableRowFromKeyboard(event, action);
@@ -40,10 +40,18 @@ describe("selectable row activation", () => {
}); });
}); });
function eventWithPath(target: Pick<Element, "matches">): MouseEvent { type EventWithPath = Pick<Event, "composedPath">;
return { composedPath: () => [target] } as unknown as MouseEvent; type KeyboardEventWithPath = EventWithPath & Pick<KeyboardEvent, "key" | "preventDefault">;
type MatchTarget = EventTarget & Pick<Element, "matches">;
function matchTarget(matches: Element["matches"]): MatchTarget {
return Object.assign(new EventTarget(), { matches });
} }
function keyboardEventWithPath(key: string, target: Pick<Element, "matches">): KeyboardEvent & { preventDefault: ReturnType<typeof vi.fn> } { function eventWithPath(target: MatchTarget): EventWithPath {
return { key, preventDefault: vi.fn(), composedPath: () => [target] } as unknown as KeyboardEvent & { preventDefault: ReturnType<typeof vi.fn> }; return { composedPath: () => [target] };
}
function keyboardEventWithPath(key: string, target: MatchTarget): KeyboardEventWithPath {
return { key, preventDefault: vi.fn<() => void>(), composedPath: () => [target] };
} }
+12 -7
View File
@@ -10,21 +10,26 @@ const interactiveSelector = [
"[contenteditable='true']", "[contenteditable='true']",
].join(","); ].join(",");
export function isFromInteractiveElement(event: Event): boolean { type ComposedPathEvent = Pick<Event, "composedPath">;
return event.composedPath().some((target) => isElementLike(target) && target.matches(interactiveSelector)); type SelectableKeyboardEvent = ComposedPathEvent & Pick<KeyboardEvent, "key" | "preventDefault">;
export function isFromInteractiveElement(event: ComposedPathEvent): boolean {
return event.composedPath().some((target) => targetMatches(target, interactiveSelector));
} }
function isElementLike(target: EventTarget): target is Element { function targetMatches(target: EventTarget, selector: string): boolean {
if (typeof Element !== "undefined") return target instanceof Element; if (typeof Element !== "undefined" && target instanceof Element) return target.matches(selector);
return typeof (target as Partial<Element>).matches === "function"; if (!("matches" in target)) return false;
const { matches } = target;
return typeof matches === "function" && matches.call(target, selector) === true;
} }
export function activateSelectableRow(event: MouseEvent, action: () => void): void { export function activateSelectableRow(event: ComposedPathEvent, action: () => void): void {
if (isFromInteractiveElement(event)) return; if (isFromInteractiveElement(event)) return;
action(); action();
} }
export function activateSelectableRowFromKeyboard(event: KeyboardEvent, action: () => void): void { export function activateSelectableRowFromKeyboard(event: SelectableKeyboardEvent, action: () => void): void {
if (event.key !== "Enter" && event.key !== " ") return; if (event.key !== "Enter" && event.key !== " ") return;
if (isFromInteractiveElement(event)) return; if (isFromInteractiveElement(event)) return;
event.preventDefault(); event.preventDefault();