Archived
Add pre-commit verification hook
This commit is contained in:
Executable
+5
@@ -0,0 +1,5 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
echo "Running pre-commit checks: npm run verify"
|
||||||
|
npm run verify
|
||||||
+2
-1
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
@@ -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] };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user