diff --git a/.eslintrc.cjs b/.eslintrc.cjs deleted file mode 100644 index f63fe7dcb..000000000 --- a/.eslintrc.cjs +++ /dev/null @@ -1,15 +0,0 @@ -module.exports = { - root: true, - env: { browser: true, es2020: true }, - extends: [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:react-hooks/recommended", - ], - ignorePatterns: ["dist", ".eslintrc.cjs"], - parser: "@typescript-eslint/parser", - plugins: ["react-refresh"], - rules: { - "react-refresh/only-export-components": ["warn", { allowConstantExport: true }], - }, -}; diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..f54a72c5d --- /dev/null +++ b/.gitattributes @@ -0,0 +1,25 @@ +* text=auto eol=lf + +*.bat text eol=crlf +*.cmd text eol=crlf + +*.dll binary +*.exe binary +*.gif binary +*.icns binary +*.ico binary +*.jpeg binary +*.jpg binary +*.mov binary +*.mp3 binary +*.mp4 binary +*.node binary +*.pdf binary +*.png binary +*.ttf binary +*.wasm binary +*.wav binary +*.webm binary +*.woff binary +*.woff2 binary +*.zip binary diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 000000000..45e5f6512 --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,65 @@ +name: Code Quality + +on: + pull_request: + types: [opened, reopened, synchronize, ready_for_review] + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + quality: + name: Repository quality + runs-on: ubuntu-latest + timeout-minutes: 20 + env: + CI: true + + steps: + - name: Checkout code + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: '22' + cache: npm + cache-dependency-path: package-lock.json + + - name: Install dependencies + id: install + run: npm ci --ignore-scripts + + - name: Typecheck + if: ${{ !cancelled() && steps.install.outcome == 'success' }} + run: npx tsc --noEmit + + - name: Lint + if: ${{ !cancelled() && steps.install.outcome == 'success' }} + run: npm run lint + + - name: Check formatting (advisory) + if: ${{ !cancelled() && steps.install.outcome == 'success' }} + continue-on-error: true + run: npm run format:check + + - name: Test + if: ${{ !cancelled() && steps.install.outcome == 'success' }} + run: npm test + + # Main currently has known locale-parity debt covered by PR #710. Keep the + # check visible without blocking unrelated PRs; make it required once that + # existing translation PR (or an equivalent fix) lands. + - name: Check translations (advisory) + if: ${{ !cancelled() && steps.install.outcome == 'success' }} + continue-on-error: true + run: npm run i18n:check diff --git a/biome.json b/biome.json index 30eb475d7..6c8695eb4 100644 --- a/biome.json +++ b/biome.json @@ -1,7 +1,18 @@ { "$schema": "https://biomejs.dev/schemas/2.3.13/schema.json", "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true }, - "files": { "ignoreUnknown": false }, + "files": { + "ignoreUnknown": false, + "includes": [ + "**", + "!!**/dist", + "!!**/dist-electron", + "!!**/dist-ssr", + "!!**/release", + "!!**/.tmp", + "!!**/electron/native/**/build" + ] + }, "formatter": { "enabled": true, "indentStyle": "tab", @@ -92,42 +103,11 @@ "noWith": "error", "useGetterReturn": "error" } - }, - "includes": ["**", "**/dist", "**/.eslintrc.cjs", "**", "**/dist", "**/.eslintrc.cjs"] + } }, "javascript": { "formatter": { "quoteStyle": "double" } }, - "css": { "parser": { "tailwindDirectives": true } }, + "css": { "parser": { "cssModules": true, "tailwindDirectives": true } }, "overrides": [ - { - "includes": ["*.ts", "*.tsx", "*.mts", "*.cts"], - "linter": { - "rules": { - "complexity": { "noArguments": "error" }, - "correctness": { - "noConstAssign": "off", - "noGlobalObjectCalls": "off", - "noInvalidBuiltinInstantiation": "off", - "noInvalidConstructorSuper": "off", - "noSetterReturn": "off", - "noUndeclaredVariables": "off", - "noUnreachable": "off", - "noUnreachableSuper": "off" - }, - "style": { "useConst": "error" }, - "suspicious": { - "noDuplicateClassMembers": "off", - "noDuplicateObjectKeys": "off", - "noDuplicateParameters": "off", - "noFunctionAssign": "off", - "noImportAssign": "off", - "noRedeclare": "off", - "noUnsafeNegation": "off", - "noVar": "error", - "useGetterReturn": "off" - } - } - } - }, { "includes": ["*.ts", "*.tsx", "*.mts", "*.cts"], "linter": { diff --git a/electron/ipc/paths/binaries.test.ts b/electron/ipc/paths/binaries.test.ts index 5b89d9575..361861739 100644 --- a/electron/ipc/paths/binaries.test.ts +++ b/electron/ipc/paths/binaries.test.ts @@ -27,6 +27,14 @@ describe("Windows native helper path resolution", () => { await fs.rm(tempRoot, { recursive: true, force: true }); }); + it("resolves a requested platform tag independently of the host platform", async () => { + const { getNativeArchTag } = await import("./binaries"); + + expect(getNativeArchTag("win32")).toBe( + process.arch === "arm64" ? "win32-arm64" : "win32-x64", + ); + }); + it("prefers the branch-staged helper over a stale local CMake build in dev", async () => { const buildOutputPath = path.join( appPath, diff --git a/electron/ipc/paths/binaries.ts b/electron/ipc/paths/binaries.ts index 739f7df84..3e15f3322 100644 --- a/electron/ipc/paths/binaries.ts +++ b/electron/ipc/paths/binaries.ts @@ -29,24 +29,27 @@ export function getNativeCaptureHelperSourcePath(): string { return resolveUnpackedAppPath("electron", "native", "ScreenCaptureKitRecorder.swift"); } -export function getNativeArchTag(): string { - if (process.platform === "darwin") { +export function getNativeArchTag(platform: NodeJS.Platform = process.platform): string { + if (platform === "darwin") { return process.arch === "arm64" ? "darwin-arm64" : "darwin-x64"; } - if (process.platform === "win32") { + if (platform === "win32") { return process.arch === "arm64" ? "win32-arm64" : "win32-x64"; } - if (process.platform === "linux") { + if (platform === "linux") { return process.arch === "arm64" ? "linux-arm64" : "linux-x64"; } - return `${process.platform}-${process.arch}`; + return `${platform}-${process.arch}`; } -export function getPrebundledNativeHelperPath(binaryName: string): string { - return resolveUnpackedAppPath("electron", "native", "bin", getNativeArchTag(), binaryName); +export function getPrebundledNativeHelperPath( + binaryName: string, + archTag = getNativeArchTag(), +): string { + return resolveUnpackedAppPath("electron", "native", "bin", archTag, binaryName); } export function resolvePreferredWindowsNativeHelperPath( @@ -61,7 +64,7 @@ export function resolvePreferredWindowsNativeHelperPath( "Release", binaryName, ); - const prebundledPath = getPrebundledNativeHelperPath(binaryName); + const prebundledPath = getPrebundledNativeHelperPath(binaryName, getNativeArchTag("win32")); if (app.isPackaged && existsSync(prebundledPath)) { return prebundledPath; diff --git a/electron/ipc/register/assets.ts b/electron/ipc/register/assets.ts index a0b132ea2..f18bf7b6a 100644 --- a/electron/ipc/register/assets.ts +++ b/electron/ipc/register/assets.ts @@ -62,7 +62,7 @@ export function registerAssetHandlers() { await fs.writeFile(thumbPath, jpegData) }) // Keep the queue moving even if one fails - thumbGenerationQueue = generation.catch(() => {}) + thumbGenerationQueue = generation.catch(() => undefined) await generation return { success: true, data: jpegData! } diff --git a/package.json b/package.json index 6357c6667..c9266412d 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,10 @@ "dev": "vite --config vite.config.ts", "postinstall": "node scripts/postinstall.mjs", "build": "npm run build:platform-native-helpers && tsc && vite build --config vite.config.ts && npm run normalize:electron-main-cjs && npm run smoke:electron-main-cjs && electron-builder", - "lint": "biome check .", - "lint:fix": "biome check --write .", + "lint": "biome lint .", + "lint:fix": "biome lint --write .", "format": "biome format --write .", + "format:check": "biome format .", "preview": "vite preview --config vite.config.ts", "rebuild:native": "node ./node_modules/@electron/rebuild/lib/cli.js --force --only uiohook-napi", "build:native-helpers": "node scripts/build-native-helpers.mjs", diff --git a/src/components/launch/SourceSelector.tsx b/src/components/launch/SourceSelector.tsx index 158b53e16..baa4da59d 100644 --- a/src/components/launch/SourceSelector.tsx +++ b/src/components/launch/SourceSelector.tsx @@ -80,7 +80,7 @@ export const SourceSelectorContent = ({ windowSources = [], selectedSource = "Screen", loading = false, - onSourceSelect = () => {}, + onSourceSelect = () => undefined, }: Pick) => { const t = useScopedT("launch"); const renderSourceItem = (source: DesktopSource, index: number) => { diff --git a/src/components/launch/contexts/HudInteractionContext.tsx b/src/components/launch/contexts/HudInteractionContext.tsx index 65a1accb8..66b5159e0 100644 --- a/src/components/launch/contexts/HudInteractionContext.tsx +++ b/src/components/launch/contexts/HudInteractionContext.tsx @@ -1,8 +1,8 @@ -import { createContext, useContext } from "react"; +import { createContext, type MouseEvent, useContext } from "react"; interface HudInteractionContextType { onMouseEnter: () => void; - onMouseLeave: (event: any) => void; + onMouseLeave: (event: MouseEvent) => void; } export const HudInteractionContext = createContext(null); diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx index 499179f39..8de8b685b 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -169,7 +169,6 @@ import { import { clampFocusToStage as clampFocusToStageUtil } from "./videoPlayback/focusUtils"; import { layoutVideoContent as layoutVideoContentUtil, - scalePreviewBorderRadius, } from "./videoPlayback/layoutUtils"; import { updateOverlayIndicator } from "./videoPlayback/overlayUtils"; import { createVideoEventHandlers } from "./videoPlayback/videoEventHandlers"; diff --git a/src/components/video-editor/audio/waveform/waveform.worker.ts b/src/components/video-editor/audio/waveform/waveform.worker.ts index 80403a09b..28e2ebd98 100644 --- a/src/components/video-editor/audio/waveform/waveform.worker.ts +++ b/src/components/video-editor/audio/waveform/waveform.worker.ts @@ -6,7 +6,7 @@ type WaveformWorkerRequest = { interface WorkerContext { onmessage: (e: MessageEvent) => void; - postMessage: (message: any, transfer?: Transferable[]) => void; + postMessage: (message: unknown, transfer?: Transferable[]) => void; } const workerScope = self as unknown as WorkerContext; diff --git a/src/lib/exporter/streamingDecoder.test.ts b/src/lib/exporter/streamingDecoder.test.ts index de831671e..f49e835cd 100644 --- a/src/lib/exporter/streamingDecoder.test.ts +++ b/src/lib/exporter/streamingDecoder.test.ts @@ -67,7 +67,7 @@ describe("StreamingVideoDecoder local media loading", () => { const decoder = new StreamingVideoDecoder(); await decoder.loadMetadata("http://127.0.0.1:43123/video?path=%2Ftmp%2Fcapture.mp4"); - expect((window as any).electronAPI.readLocalFile).not.toHaveBeenCalled(); + expect(window.electronAPI.readLocalFile).not.toHaveBeenCalled(); expect(mockDemuxerLoad).toHaveBeenCalledWith( "http://127.0.0.1:43123/video?path=%2Ftmp%2Fcapture.mp4", ); @@ -77,7 +77,7 @@ describe("StreamingVideoDecoder local media loading", () => { const decoder = new StreamingVideoDecoder(); await decoder.loadMetadata("/tmp/capture.mp4"); - expect((window as any).electronAPI.getLocalMediaUrl).toHaveBeenCalledWith( + expect(window.electronAPI.getLocalMediaUrl).toHaveBeenCalledWith( "/tmp/capture.mp4", ); expect(mockDemuxerLoad).toHaveBeenCalledWith( @@ -90,7 +90,7 @@ describe("StreamingVideoDecoder local media loading", () => { mockDemuxerLoad .mockRejectedValueOnce(new Error("get_media_info failed: Failed after 3 attempts")) .mockResolvedValueOnce(undefined); - (window as any).electronAPI.readLocalFile = vi.fn(async () => ({ + window.electronAPI.readLocalFile = vi.fn(async () => ({ success: true, data: new Uint8Array([1, 2, 3]), })); @@ -103,7 +103,7 @@ describe("StreamingVideoDecoder local media loading", () => { "http://127.0.0.1:4321/video?path=%2Ftmp%2Ffallback.mp4", ); expect(mockDemuxerLoad.mock.calls[1]?.[0]).toBeInstanceOf(File); - expect((window as any).electronAPI.readLocalFile).toHaveBeenCalledWith( + expect(window.electronAPI.readLocalFile).toHaveBeenCalledWith( "/tmp/fallback.mp4", ); });