Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: Checks

on:
pull_request:
push:
branches:
- main

jobs:
checks:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Read Node version from mise.toml
id: node
run: echo "version=$(grep -E '^node\s*=' mise.toml | sed -E 's/.*"([^"]+)".*/\1/')" >>
"$GITHUB_OUTPUT"

- name: Install package manager (from package.json)
run: |
corepack enable
corepack install

- uses: actions/setup-node@v4
with:
node-version: ${{ steps.node.outputs.version }}

- name: Resolve yarn cache folder
id: yarn-config
run: echo "cacheFolder=$(yarn config get cacheFolder)" >> "$GITHUB_OUTPUT"

- name: Restore yarn install cache (node_modules + cacheFolder + install-state)
id: yarn-cache
uses: actions/cache@v4
with:
path: |
node_modules
${{ steps.yarn-config.outputs.cacheFolder }}
.yarn/install-state.gz
key:
yarn-${{ runner.os }}-node${{ steps.node.outputs.version }}-${{ hashFiles('yarn.lock')
}}
restore-keys: |
yarn-${{ runner.os }}-node${{ steps.node.outputs.version }}-

- name: Install deps
env:
YARN_ENABLE_HARDENED_MODE: 'false'
run: |
case "$(yarn --version)" in 1.*) echo 'expected up-to-date yarn version'; exit 1 ;; esac
yarn install --immutable

- name: Format
run: yarn format:check

- name: Lint
run: yarn lint

- name: Typecheck
run: yarn typecheck

- name: Test
run: yarn coverage
8 changes: 0 additions & 8 deletions .husky/commit-msg

This file was deleted.

13 changes: 6 additions & 7 deletions .husky/pre-commit
100644 → 100755
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
#!/bin/sh
# .husky/pre-commit (v7)

# Fix for colours in Windows
export FORCE_COLOR=1

# Check changes that are staged for commit
#!/usr/bin/env sh
yarn lint-staged
yarn typecheck

if command -v gitleaks >/dev/null 2>&1; then
gitleaks protect --staged --no-banner --redact
fi
15 changes: 15 additions & 0 deletions .oxfmtrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"semi": true,
"singleQuote": true,
"trailingComma": "all",
"printWidth": 100,
"proseWrap": "preserve",
"sortPackageJson": false,
"ignorePatterns": [
"**/node_modules/**",
"**/dist/**",
"**/build/**",
"**/coverage/**",
"**/.yarn/**"
]
}
49 changes: 49 additions & 0 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"$schema": "./node_modules/oxlint/configuration_schema.json",
"plugins": ["typescript", "react", "vitest", "jsx-a11y", "unicorn", "oxc"],
"categories": {
"correctness": "error",
"suspicious": "error",
"perf": "error"
},
"rules": {
"vitest/require-mock-type-parameters": "off",
"vitest/valid-title": "off",
"typescript/no-explicit-any": "warn",
"typescript/ban-ts-comment": "off",
"react/react-in-jsx-scope": "off",
"no-bitwise": "off",
"no-shadow": "off",
"no-empty-pattern": "off",
"no-async-promise-executor": "warn",
"unicorn/no-instanceof-builtins": "off",
"unicorn/no-array-sort": "off",
"unicorn/consistent-function-scoping": "off",
"typescript/no-wrapper-object-types": "warn",
"oxc/no-this-in-exported-function": "warn",
"jsx-a11y/no-static-element-interactions": "warn",
"jsx-a11y/click-events-have-key-events": "warn"
},
"overrides": [
{
"files": ["**/*.test.ts", "**/*.test.tsx", "**/*.spec.ts", "**/*.spec.tsx"],
"rules": {
"typescript/no-explicit-any": "off",
"no-unused-vars": "off"
}
}
],
"env": {
"browser": true,
"node": true,
"es2024": true,
"vitest/globals": true
},
"ignorePatterns": [
"**/node_modules/**",
"**/dist/**",
"**/build/**",
"**/coverage/**",
"**/.yarn/**"
]
}
2 changes: 1 addition & 1 deletion .yarnrc.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
approvedGitRepositories:
- "**"
- '**'

compressionLevel: mixed

Expand Down
2 changes: 1 addition & 1 deletion hooks/useFirebaseDrop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export function useFirebaseDrop(
);

const onDrop = useCallback(
(acceptedFiles: File[], fileRejections: FileRejection[], event: DropEvent): void => {
(acceptedFiles: File[], fileRejections: FileRejection[], _event: DropEvent): void => {
fileRejections.map((rejection) =>
console.error(
`failed to upload: ${rejection.file.name}. ${rejection.errors
Expand Down
3 changes: 3 additions & 0 deletions mise.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
[tools]
node = "20.11.1"
yarn = "4.14.1"
actionlint = "latest"
shellcheck = "latest"
gitleaks = "latest"
31 changes: 31 additions & 0 deletions model/DateFormat.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { describe, expect, it } from 'vitest';

import { DateFormat } from './DateFormat';

describe('DateFormat.simpleTimeIn24HourFormat', () => {
it('formats a Date instance as HH:mm', () => {
const date = new Date('2024-01-15T14:30:00');
expect(DateFormat.simpleTimeIn24HourFormat(date)).toBe('14:30');
});

it('formats a numeric timestamp as HH:mm', () => {
const timestamp = new Date('2024-01-15T09:05:00').getTime();
expect(DateFormat.simpleTimeIn24HourFormat(timestamp)).toBe('09:05');
});

it('zero-pads single-digit hours', () => {
expect(DateFormat.simpleTimeIn24HourFormat(new Date('2024-01-15T03:30:00'))).toBe('03:30');
});

it('zero-pads single-digit minutes', () => {
expect(DateFormat.simpleTimeIn24HourFormat(new Date('2024-01-15T14:05:00'))).toBe('14:05');
});

it('renders midnight as 00:00', () => {
expect(DateFormat.simpleTimeIn24HourFormat(new Date('2024-01-15T00:00:00'))).toBe('00:00');
});

it('renders 23:59 correctly', () => {
expect(DateFormat.simpleTimeIn24HourFormat(new Date('2024-01-15T23:59:00'))).toBe('23:59');
});
});
60 changes: 60 additions & 0 deletions model/EventBuffer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import { describe, expect, it } from 'vitest';

import { EventBuffer } from './EventBuffer';

describe('EventBuffer', () => {
it('starts empty', () => {
const buf = new EventBuffer<number>();
expect(buf.isEmpty()).toBe(true);
expect(buf.length).toBe(0);
});

it('reports non-empty after put()', () => {
const buf = new EventBuffer<number>();
buf.put(1);
expect(buf.isEmpty()).toBe(false);
expect(buf.length).toBe(1);
});

it('take() returns items in FIFO order', () => {
const buf = new EventBuffer<string>();
buf.put('a');
buf.put('b');
buf.put('c');
expect(buf.take()).toBe('a');
expect(buf.take()).toBe('b');
expect(buf.take()).toBe('c');
});

it('take() returns undefined and keeps length 0 on an empty buffer', () => {
const buf = new EventBuffer<number>();
expect(buf.take()).toBeUndefined();
expect(buf.length).toBe(0);
});

it('flush() returns all items and empties the buffer', () => {
const buf = new EventBuffer<number>();
buf.put(1);
buf.put(2);
buf.put(3);
expect(buf.flush()).toEqual([1, 2, 3]);
expect(buf.isEmpty()).toBe(true);
expect(buf.length).toBe(0);
});

it('flush() on empty buffer returns an empty array', () => {
const buf = new EventBuffer<number>();
expect(buf.flush()).toEqual([]);
});

it('keeps `length` in sync with `queue.length` across put/take/flush', () => {
const buf = new EventBuffer<number>();
buf.put(1);
buf.put(2);
expect(buf.length).toBe(2);
buf.take();
expect(buf.length).toBe(1);
buf.flush();
expect(buf.length).toBe(0);
});
});
44 changes: 29 additions & 15 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
"@loadable/component": "^5.15.2",
"firebase": "^9.6.1",
"postcss": "^8.4.5",
"prettier": "^2.5.1",
"react": "^18.0.0-rc.0",
"react-dom": "^18.0.0-rc.0",
"react-hot-toast": "^2.1.1",
Expand All @@ -27,23 +26,38 @@
"web-vitals": "^2.1.2"
},
"devDependencies": {
"@commitlint/cli": "^15.0.0",
"@commitlint/config-conventional": "^15.0.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-unicorn": "^39.0.0",
"husky": "^7.0.4",
"lint-staged": "^12.1.4",
"prettier": "^2.5.1"
"@types/node": "^25.6.0",
"@typescript/native-preview": "^7.0.0-dev.20260505.1",
"@vitest/coverage-istanbul": "^4.1.5",
"eslint": "^10.3.0",
"eslint-plugin-unicorn": "^64.0.0",
"husky": "9",
"lint-staged": "^16.4.0",
"oxfmt": "^0.48.0",
"oxlint": "^1.63.0",
"typescript": "^6.0.3",
"vite": "^7",
"vitest": "^4"
},
"scripts": {
"test": "node scripts/ensure-husky.mjs && vitest run",
"test:watch": "vitest",
"coverage": "vitest run --coverage",
"lint": "yarn oxlint --report-unused-disable-directives",
"format": "oxfmt --write",
"format:check": "oxfmt --check",
"typecheck": "tsgo --noEmit",
"typecheck:tsc": "tsc --noEmit",
"setup:hooks": "node scripts/ensure-husky.mjs",
"prepare": "husky"
},
"lint-staged": {
"*.{js,ts,tsx}": [
"prettier --write",
"eslint --fix"
"*.@(ts|tsx|mts|js|jsx|mjs|cjs)": [
"oxlint --fix --quiet",
"oxfmt --write"
],
"*.{json,md,yaml,yml}": [
"prettier --write"
]
"*.@(json|jsonc|json5|md|mdx|yaml|yml|css|scss|sass|html|toml)": "oxfmt --write",
".github/workflows/*.@(yml|yaml)": "actionlint"
},
"packageManager": "yarn@4.14.1"
}
55 changes: 55 additions & 0 deletions scripts/ensure-husky.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/usr/bin/env node
// Self-heals husky git hooks before local dev workflows.
//
// Why this exists: Yarn 4 skips lifecycle scripts (`prepare`, `postinstall`) on
// no-op installs, so `yarn install --immutable` does NOT reinstall hooks once
// `.husky/_/` has been wiped. `.husky/_/` is gitignored, so it is also missing
// in fresh worktrees and after `git clean -fdx`. Without this guard, commits
// silently skip the pre-commit hook (git treats a missing hook file as "no hook").
//
// Behaviour: ~20 ms no-op when hooks are already installed. Skipped in CI and
// when HUSKY=0. Fails loudly (non-zero exit) on real install errors so the
// caller stops before commits are made without hooks.

import { execSync } from 'node:child_process';
import { existsSync } from 'node:fs';

if (process.env.CI || process.env.HUSKY === '0') process.exit(0);

const expectedHooksPath = '.husky/_';
const sentinelHook = '.husky/_/pre-commit';
// husky 9.1+ ships bin.js; husky 9.0 ships bin.mjs. Try both.
const huskyBin = ['node_modules/husky/bin.js', 'node_modules/husky/bin.mjs'].find(existsSync);

let configuredHooksPath = '';
try {
configuredHooksPath = execSync('git config --get core.hooksPath', {
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'ignore'],
}).trim();
} catch {
// not a git repo or config unset — fall through and try to install
}

if (configuredHooksPath === expectedHooksPath && existsSync(sentinelHook)) {
process.exit(0);
}

if (!huskyBin) {
// husky not installed yet (yarn install hasn't run) — silent no-op
process.exit(0);
}

console.log('· installing git hooks (husky self-heal)…');
try {
execSync(`node ${huskyBin}`, { stdio: 'inherit' });
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(
`\n❌ husky install failed: ${message}\n\n` +
` git pre-commit hooks are NOT installed; commits will skip lint/format/tests.\n` +
` Fix the underlying error above, then run \`yarn setup:hooks\` to retry.\n` +
` To bypass this guard temporarily (NOT recommended): HUSKY=0 yarn <cmd>.\n`,
);
process.exit(1);
}
Loading
Loading