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",
"pack:dry": "npm pack --dry-run",
"prepublishOnly": "npm run verify",
"publish:npm": "npm publish --access public"
"publish:npm": "npm publish --access public",
"prepare": "node scripts/install-git-hooks.mjs"
},
"dependencies": {
"@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,
gitStale: this.state.gitStale,
activeTerminalCount: this.state.activeTerminalCount,
onRefreshFiles: () => this.files.refreshFiles(),
onExpandDir: (path: string) => this.files.expandDir(path),
onSelectFile: (path: string) => this.files.selectFile(path),
onRefreshGit: () => this.git.refreshGit(),
onSelectDiff: (path: string) => this.git.selectDiff(path),
onRefreshFiles: () => { void this.files.refreshFiles(); },
onExpandDir: (path: string) => { void this.files.expandDir(path); },
onSelectFile: (path: string) => { void this.files.selectFile(path); },
onRefreshGit: () => { void this.git.refreshGit(); },
onSelectDiff: (path: string) => { void this.git.selectDiff(path); },
};
}
@@ -4,21 +4,21 @@ import { activateSelectableRow, activateSelectableRowFromKeyboard } from "./sele
describe("selectable row activation", () => {
it("activates rows from non-interactive click targets", () => {
const action = vi.fn();
activateSelectableRow(eventWithPath({ matches: () => false }), action);
activateSelectableRow(eventWithPath(matchTarget(() => false)), action);
expect(action).toHaveBeenCalledOnce();
});
it("preserves contributed links and other interactive elements", () => {
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();
});
it("activates rows from Enter and Space", () => {
const enterAction = vi.fn();
const spaceAction = vi.fn();
const enter = keyboardEventWithPath("Enter", { matches: () => false });
const space = keyboardEventWithPath(" ", { matches: () => false });
const enter = keyboardEventWithPath("Enter", matchTarget(() => false));
const space = keyboardEventWithPath(" ", matchTarget(() => false));
activateSelectableRowFromKeyboard(enter, enterAction);
activateSelectableRowFromKeyboard(space, spaceAction);
@@ -31,7 +31,7 @@ describe("selectable row activation", () => {
it("does not activate rows from keyboard events inside interactive elements", () => {
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);
@@ -40,10 +40,18 @@ describe("selectable row activation", () => {
});
});
function eventWithPath(target: Pick<Element, "matches">): MouseEvent {
return { composedPath: () => [target] } as unknown as MouseEvent;
type EventWithPath = Pick<Event, "composedPath">;
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> } {
return { key, preventDefault: vi.fn(), composedPath: () => [target] } as unknown as KeyboardEvent & { preventDefault: ReturnType<typeof vi.fn> };
function eventWithPath(target: MatchTarget): EventWithPath {
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']",
].join(",");
export function isFromInteractiveElement(event: Event): boolean {
return event.composedPath().some((target) => isElementLike(target) && target.matches(interactiveSelector));
type ComposedPathEvent = Pick<Event, "composedPath">;
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 {
if (typeof Element !== "undefined") return target instanceof Element;
return typeof (target as Partial<Element>).matches === "function";
function targetMatches(target: EventTarget, selector: string): boolean {
if (typeof Element !== "undefined" && target instanceof Element) return target.matches(selector);
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;
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 (isFromInteractiveElement(event)) return;
event.preventDefault();