From 30b1bb12ee257a62cc858bb2a8ec7a47fc719515 Mon Sep 17 00:00:00 2001 From: Daniel Griesser Date: Sun, 16 Aug 2026 00:22:01 +0200 Subject: [PATCH 1/2] chore(lint): adopt anti-slop rules Vendor the anti-slop Oxlint plugin and enable its full rule set across application, worker, processor, migration, and test code.\n\nReplace broad runtime narrowing and unsafe assertions with explicit JSON boundary parsing, precise contracts, and structurally typed test doubles so the stricter policy starts from a clean baseline. --- package-lock.json | 575 +++++++++++++++--- package.json | 4 +- processor/video-processor.mjs | 4 +- scripts/migrate/cli.ts | 27 +- scripts/migrate/import.ts | 19 +- scripts/migrate/reconcile.ts | 157 +++-- scripts/migrate/transform.ts | 117 ++-- scripts/setup-worktree.mjs | 8 +- src/app/components/ProjectForm.tsx | 5 +- src/app/player/audio.ts | 19 +- src/app/player/controller.ts | 10 +- src/app/queries/api.ts | 11 +- src/app/session.tsx | 4 +- src/app/video/ProjectVideoPanel.tsx | 4 +- src/app/video/upload.ts | 51 +- src/shared/json.ts | 33 + src/shared/profile.ts | 15 +- src/worker/integrations/google-oauth.ts | 16 +- src/worker/integrations/historical-source.ts | 4 +- src/worker/middleware/auth.ts | 6 +- src/worker/repositories/administration.ts | 10 +- src/worker/repositories/groups.ts | 6 +- src/worker/routes/auth.ts | 4 +- src/worker/routes/session.ts | 10 +- src/worker/routes/videos.ts | 71 ++- src/worker/services/administration-input.ts | 42 +- src/worker/services/errors.ts | 12 +- src/worker/services/project-input.ts | 32 +- src/worker/services/videos.ts | 12 +- src/worker/workflows/video-processing.ts | 85 +-- test/admin/admin.test.ts | 16 +- test/app/ProjectForm.test.tsx | 10 +- test/app/administration.test.tsx | 23 +- test/app/routes.test.tsx | 36 +- test/auth/auth.test.ts | 3 +- test/auth/fixture.ts | 8 +- test/e2e/readiness-fixture.ts | 2 +- test/migration/migration.test.ts | 16 +- test/player/audio.test.tsx | 37 +- test/projects/projects.test.ts | 16 +- test/video-ui/video-ui.test.tsx | 14 +- test/video/video.test.ts | 56 +- test/voting/voting.test.ts | 13 +- tools/oxlint/anti-slop/index.ts | 41 ++ .../rules/no-chained-type-assertions.ts | 77 +++ .../no-conditional-empty-object-spread.ts | 49 ++ .../rules/no-known-value-widening.ts | 247 ++++++++ .../anti-slop/rules/no-module-mocking.ts | 91 +++ .../anti-slop/rules/no-object-parameters.ts | 126 ++++ .../anti-slop/rules/no-reflect-apply.ts | 28 + .../oxlint/anti-slop/rules/no-reflect-get.ts | 28 + .../anti-slop/rules/no-runtime-typeof.ts | 67 ++ .../rules/no-shape-in-symbol-names.ts | 39 ++ .../anti-slop/rules/no-unknown-parameters.ts | 83 +++ .../anti-slop/rules/no-unknown-returns.ts | 115 ++++ .../rules/no-unknown-type-aliases.ts | 70 +++ .../rules/no-unsafe-dictionary-type.ts | 134 ++++ .../anti-slop/rules/no-widen-then-assert.ts | 366 +++++++++++ ...quire-safety-comment-for-type-assertion.ts | 62 ++ .../anti-slop/shared/dictionary-types.ts | 502 +++++++++++++++ .../shared/lexical-type-parameters.ts | 61 ++ .../oxlint/anti-slop/shared/reflect-method.ts | 35 ++ vite.config.ts | 50 +- 63 files changed, 3405 insertions(+), 489 deletions(-) create mode 100644 src/shared/json.ts create mode 100644 tools/oxlint/anti-slop/index.ts create mode 100644 tools/oxlint/anti-slop/rules/no-chained-type-assertions.ts create mode 100644 tools/oxlint/anti-slop/rules/no-conditional-empty-object-spread.ts create mode 100644 tools/oxlint/anti-slop/rules/no-known-value-widening.ts create mode 100644 tools/oxlint/anti-slop/rules/no-module-mocking.ts create mode 100644 tools/oxlint/anti-slop/rules/no-object-parameters.ts create mode 100644 tools/oxlint/anti-slop/rules/no-reflect-apply.ts create mode 100644 tools/oxlint/anti-slop/rules/no-reflect-get.ts create mode 100644 tools/oxlint/anti-slop/rules/no-runtime-typeof.ts create mode 100644 tools/oxlint/anti-slop/rules/no-shape-in-symbol-names.ts create mode 100644 tools/oxlint/anti-slop/rules/no-unknown-parameters.ts create mode 100644 tools/oxlint/anti-slop/rules/no-unknown-returns.ts create mode 100644 tools/oxlint/anti-slop/rules/no-unknown-type-aliases.ts create mode 100644 tools/oxlint/anti-slop/rules/no-unsafe-dictionary-type.ts create mode 100644 tools/oxlint/anti-slop/rules/no-widen-then-assert.ts create mode 100644 tools/oxlint/anti-slop/rules/require-safety-comment-for-type-assertion.ts create mode 100644 tools/oxlint/anti-slop/shared/dictionary-types.ts create mode 100644 tools/oxlint/anti-slop/shared/lexical-type-parameters.ts create mode 100644 tools/oxlint/anti-slop/shared/reflect-method.ts diff --git a/package-lock.json b/package-lock.json index 62307fe..13dc7a2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "devDependencies": { "@cloudflare/vite-plugin": "^1.50.0", "@cloudflare/vitest-pool-workers": "^0.20.1", + "@oxlint/plugins": "1.78.0", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", "@types/node": "^26.1.2", @@ -30,6 +31,7 @@ "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.5", "jsdom": "^26.1.0", + "oxlint": "1.78.0", "tsx": "^4.23.5", "typescript": "^7.0.2", "vite": "^8.2.0", @@ -1900,9 +1902,9 @@ ] }, "node_modules/@oxlint/binding-android-arm-eabi": { - "version": "1.75.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.75.0.tgz", - "integrity": "sha512-lutovtFzJqlRaqpZrCqSSGaHZzl9nIxxpjLzhSRLunN6dCLylj0uzlCyQGaQDIys7rrv8kVXiFO+R4Zpn0bX7g==", + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.78.0.tgz", + "integrity": "sha512-Bu819lmAfZMUHErrpe0cEWj3iaefuUODHSU8+UbXy67V/r7/7f4K3FL0NmbD85E+wiFLDYuhP8Zlv0XnVeXshw==", "cpu": [ "arm" ], @@ -1917,9 +1919,9 @@ } }, "node_modules/@oxlint/binding-android-arm64": { - "version": "1.75.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.75.0.tgz", - "integrity": "sha512-hXI0hDgHkw4w5nfru72aG7y+2iQJmC4waH/KV6H/hbgA6yAP5jYNx0P9yug15Hs0tWl/+mda3Jjn/2gmDT48tw==", + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.78.0.tgz", + "integrity": "sha512-CDfxZgB61B7buRdY2FJoAYYPPXCZ1EoC1LKscnC5dg3kjobdxiconvAvvN1BmHyW4PyFT3jRLDag/BY/roSNBQ==", "cpu": [ "arm64" ], @@ -1934,9 +1936,9 @@ } }, "node_modules/@oxlint/binding-darwin-arm64": { - "version": "1.75.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.75.0.tgz", - "integrity": "sha512-D91BWbK/dMYfCcrghspPIuKs2D9LF4Z/OabVSQjw1AO6PWxArD7teDA48bm0ySFqWDaPVqmQRl5GMWNglTXyrQ==", + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.78.0.tgz", + "integrity": "sha512-2Y2U9Ahrz+OO0Ej88f9SJYq51/jUBp1Mc7iZu0ukrbeeZ3gpRGfzIFnoqfHDY96xr0GEfNrPUBFEy0nN5aD7HA==", "cpu": [ "arm64" ], @@ -1951,9 +1953,9 @@ } }, "node_modules/@oxlint/binding-darwin-x64": { - "version": "1.75.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.75.0.tgz", - "integrity": "sha512-02mpwzf12BonZ6PT0TuQoomvEh2kVl2WGBIKWezCyToIS+rYkQZ6GXnARBAl9A4Ovm2V+Xe7M4KretyqmmcnJQ==", + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.78.0.tgz", + "integrity": "sha512-rpych6eJq6m9jDRypTEaPD1xysaEW5h9+xuxhGK/QhOg+/xaqPZrCrTNoIl/f3nEjuJeCEmstNDlrE9rJi/3/g==", "cpu": [ "x64" ], @@ -1968,9 +1970,9 @@ } }, "node_modules/@oxlint/binding-freebsd-x64": { - "version": "1.75.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.75.0.tgz", - "integrity": "sha512-qZJgLnDaBsiL5YESx2t/TZ8eXkL9fEkKoXEdzegROhlz9A0lgyGnZ0dAzJrh7LJAHQl2K9RdRueN2s/9N7+odg==", + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.78.0.tgz", + "integrity": "sha512-IcMGrQT3QizkOESUJd5et+rOhVqSkNDfNik1cvrKDqIbzqx9KMtRswpFgkCuNTSwylCFLKhGUu8KmqY1ZnC0Dg==", "cpu": [ "x64" ], @@ -1985,9 +1987,9 @@ } }, "node_modules/@oxlint/binding-linux-arm-gnueabihf": { - "version": "1.75.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.75.0.tgz", - "integrity": "sha512-7XlaWA5BJD3XpCfrEqjEe6Zseeb14S7QGa304XfwKignRaKQ+eIj775BQ7nIslggWickl4IsPUFqJ+/gAyNHVg==", + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.78.0.tgz", + "integrity": "sha512-/uLdoJ0IXE6vo/0f0LKjinQAp+re+VMaCWaNT8ENIv2EOCkSsc8SGaflXAuW0Jua2dq5+GLVWm1NQK7P3UFSNQ==", "cpu": [ "arm" ], @@ -2002,9 +2004,9 @@ } }, "node_modules/@oxlint/binding-linux-arm-musleabihf": { - "version": "1.75.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.75.0.tgz", - "integrity": "sha512-av6Tpv8yrcMMMOadOqENBhlsLRcGFXXwoQ0hzHhsmS9FJ4Wioy8we427GbcMe2XTxmL2e60T67H1Dyr3up+tAA==", + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.78.0.tgz", + "integrity": "sha512-7xi4Wb/O8NRJhLoUXmDJMUVpNYvB5kefdhFU1Jb8rtae4QoXlTiLwI14X4YvAXVZLNZChP8m5qO9SQAlWQTbkQ==", "cpu": [ "arm" ], @@ -2019,9 +2021,9 @@ } }, "node_modules/@oxlint/binding-linux-arm64-gnu": { - "version": "1.75.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.75.0.tgz", - "integrity": "sha512-WcUhd8fHT5plrA14lANevl+hOl815mVI5t2hU21oFWrZKFXIVV/Sr4rWQV0NzSvzBupbMLNc5ErEA6Ehxh5jMg==", + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.78.0.tgz", + "integrity": "sha512-4hFW0+fVXa3OIh1Y4A5SPkmvI4wuuBSrCVKzOyE7PTjhc7yEqZ1pmvEEeS5Lj/MaqvegFxXyF33N+6jkehxdyg==", "cpu": [ "arm64" ], @@ -2039,9 +2041,9 @@ } }, "node_modules/@oxlint/binding-linux-arm64-musl": { - "version": "1.75.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.75.0.tgz", - "integrity": "sha512-UWzp5wRHFe/ESO3+eEaxXsTkYTGLYjnTsi/I5neEacXSItQ6WNleapfOAeA4x2b8nyhJ4uQxqvtv9pHv8kWJtQ==", + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.78.0.tgz", + "integrity": "sha512-oC0mvsgBJjlMijSDEhx9KuvR9zYeHXceA9MjbuXB1F8NSR78Yj2unOBrstEvTVaq+pko+kuue6DajC00eqvTdg==", "cpu": [ "arm64" ], @@ -2059,9 +2061,9 @@ } }, "node_modules/@oxlint/binding-linux-ppc64-gnu": { - "version": "1.75.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.75.0.tgz", - "integrity": "sha512-XEVRwGMLKCUKrvhLAz4F6AIh8MJrQVdSZtAmPpRZt9tGPsUnamPOcl3dS/ZQzJnar/Ymgc//+xho0L60Emzuxg==", + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.78.0.tgz", + "integrity": "sha512-XAllT5SUZS+ohjuZ3/5S0cwe0r7eboiuigeStCZ5DXRYx/2KVM2UvQXvAfyzXEimtQjAB7cDQ2YxDe2Zl2WNQQ==", "cpu": [ "ppc64" ], @@ -2079,9 +2081,9 @@ } }, "node_modules/@oxlint/binding-linux-riscv64-gnu": { - "version": "1.75.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.75.0.tgz", - "integrity": "sha512-mAG4DUXqfLC8cTjMD2kt3jDmVzFREYtDyeLNdLdsCcBc4Zbl2EMuiFektGBilQwkNjYnMvCqJs55U+Hyb+b+jw==", + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.78.0.tgz", + "integrity": "sha512-trucMER/0QtecoXvc1y/UVqE3kwJipDwrx4oHfj+nNm3dq2zjP44WT0CfHNDPM3G1DXIkx/gY6lAD21NSCZVhA==", "cpu": [ "riscv64" ], @@ -2099,9 +2101,9 @@ } }, "node_modules/@oxlint/binding-linux-riscv64-musl": { - "version": "1.75.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.75.0.tgz", - "integrity": "sha512-95hrAvriAlI+pekSomTFIn0+bawMDlDwTNVmdjsFusTHyL2JWh7TWvRNG/Lkim72uN8OiCcO9wcaC6omLP5E3w==", + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.78.0.tgz", + "integrity": "sha512-cm3O4F/HQbdzOUX5mKHqG5KDL6E5w0pnlZ+fbBy2rmLryPOowkuLagFHTopQsEIpjcaZoPOrL+BmmAytAG9HFg==", "cpu": [ "riscv64" ], @@ -2119,9 +2121,9 @@ } }, "node_modules/@oxlint/binding-linux-s390x-gnu": { - "version": "1.75.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.75.0.tgz", - "integrity": "sha512-4b6f2+FrtruAESrCqIKcrarzfrSx+wk2QNcp+RT91/Prc+pMQMAfyZ1rG1c3tFQNl8Bc616tx40uNXyxNBRPbQ==", + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.78.0.tgz", + "integrity": "sha512-33wRf6HqGNsybJ3qX4cGaQN2ODPxNmc1rMa0mrTmx3eFq1VzOnvQooi9bIGVYakW8a/wmqVx1mgsUm8R2xfTiw==", "cpu": [ "s390x" ], @@ -2139,9 +2141,9 @@ } }, "node_modules/@oxlint/binding-linux-x64-gnu": { - "version": "1.75.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.75.0.tgz", - "integrity": "sha512-nshAhrUvXFUWOvqQ2soIw7HFNWvpvEV4o0cYSqPtzLiPF5gKyYTDOOTJ6Rn8g8K/iGvPIrbDA4v8+5MvnjJrrg==", + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.78.0.tgz", + "integrity": "sha512-rRdISSYegj6VganMZ9tjRjijowfHJ09IZU01i0toBAqr6n5LEtwHq2IeS4FjW2RoskOHlb6efB26H5izYb3GEQ==", "cpu": [ "x64" ], @@ -2159,9 +2161,9 @@ } }, "node_modules/@oxlint/binding-linux-x64-musl": { - "version": "1.75.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.75.0.tgz", - "integrity": "sha512-e4jNxLKnxLC6sYBQRxrI2pgIIxnmMtF8U/VwNYcjTT/CLS+spH624cYVnj07bTKwaEWT37/e025isOs6j/0xqA==", + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.78.0.tgz", + "integrity": "sha512-GmsP4rW0xTL6u5CVdcDsaN5Fbc7hBc382Wmar1kttbnwSEviM+rSINKOMQ+UQ6iH+AGwC+8gaAiwu134Tgh6Lg==", "cpu": [ "x64" ], @@ -2179,9 +2181,9 @@ } }, "node_modules/@oxlint/binding-openharmony-arm64": { - "version": "1.75.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.75.0.tgz", - "integrity": "sha512-hZ2lH+1qLf/DiEP9UWuQTK2JWj/BgvMB4jhIV4SmNU1wfEiYYX4TynQyAZXx0j9X4qRYizAL042SKaV+8ynh4w==", + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.78.0.tgz", + "integrity": "sha512-sy9yeYuADc8a+n4TLBayzMCZiHPW78DcIFVpOXTmdKHWQeM9xe5uzkqIIZmi326D5hY9XVwacipEB1p7tQjPAg==", "cpu": [ "arm64" ], @@ -2196,9 +2198,9 @@ } }, "node_modules/@oxlint/binding-win32-arm64-msvc": { - "version": "1.75.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.75.0.tgz", - "integrity": "sha512-Ilj6PNzGDS3bCU0MSJH7Msh0NhH+T/mRp2shwg+q+GHeVlPwP5LEboW96aW+3kVKFk6zYZy1Xi5pZkqZh6X8KQ==", + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.78.0.tgz", + "integrity": "sha512-rjc2hF1KfMi8fZj1X/m3AmnHbdsF3rL0v6KQg0Uc880Yb2khjz+3U14sfdZ7jWTpRnN1m1NQa/TT7uU9lJWPrA==", "cpu": [ "arm64" ], @@ -2213,9 +2215,9 @@ } }, "node_modules/@oxlint/binding-win32-ia32-msvc": { - "version": "1.75.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.75.0.tgz", - "integrity": "sha512-QVit2nOEOiPhkmsrksPSkoGCdnZRNkspt8fwoYyP09te1VEbnSj4LAxua4rc8FKTmWkySVe05j8iz9GXYfF1AQ==", + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.78.0.tgz", + "integrity": "sha512-zcuXFVrEFHIafRfkCQT8w/Xe41o07ozl/vwHq7p94vB29xVzsB0sZGYORU1jhcYKv3Lr0J3HbJ2T4fHH5rWmvA==", "cpu": [ "ia32" ], @@ -2230,9 +2232,9 @@ } }, "node_modules/@oxlint/binding-win32-x64-msvc": { - "version": "1.75.0", - "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.75.0.tgz", - "integrity": "sha512-DSxnNkBUAYARPwJtR12Ig3deWr8w0H997xP6jy33i+e0SyYJw8FKuz4+cZtpmPEhQmvlPJE3X/2vNxDmLkd/rA==", + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.78.0.tgz", + "integrity": "sha512-Sb5ocmLSuYeOuXd+CFOToGKp/gjXUEWDnvIGwhnh8aq8wY4TMmEnKnvbogSW7RdMZv77JSARduS7/gv+khYEjA==", "cpu": [ "x64" ], @@ -2247,9 +2249,9 @@ } }, "node_modules/@oxlint/plugins": { - "version": "1.73.0", - "resolved": "https://registry.npmjs.org/@oxlint/plugins/-/plugins-1.73.0.tgz", - "integrity": "sha512-OhgMQeMmZA0dcFcX4/priaJZWdFECxiClgq6mRX6aatZEcV9PbKC3P3/v8U1hVjviT1i5U+vR8lAtBV6m4FXAA==", + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/@oxlint/plugins/-/plugins-1.78.0.tgz", + "integrity": "sha512-Ypt8KeRYw+4jUtlPirfcHWMrn5ms12VrrFPD+Mds477/7tJxG1Kcz2Yrg2nVcTQEUx/GdlhS+BUg1kmxNm04Ug==", "dev": true, "license": "MIT", "engines": { @@ -5866,9 +5868,9 @@ } }, "node_modules/oxlint": { - "version": "1.75.0", - "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.75.0.tgz", - "integrity": "sha512-m9WzjRcRYA/uqIZDa9tclrieoPJ/ln1QYTKdFx6NUOs8uY5DiHlIwRQoCrHT6OM6O3ww3l2skY5gO7G7ZphE7g==", + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.78.0.tgz", + "integrity": "sha512-QgQePuxIqKOzo1KSjG2EnITEeWvWnKAm77eq8nrMtf6AGoA+zyGc4PFYtDNJSD25g/ibOwfQ851hZ4/SPkMVoA==", "dev": true, "license": "MIT", "bin": { @@ -5881,25 +5883,25 @@ "url": "https://github.com/sponsors/Boshen" }, "optionalDependencies": { - "@oxlint/binding-android-arm-eabi": "1.75.0", - "@oxlint/binding-android-arm64": "1.75.0", - "@oxlint/binding-darwin-arm64": "1.75.0", - "@oxlint/binding-darwin-x64": "1.75.0", - "@oxlint/binding-freebsd-x64": "1.75.0", - "@oxlint/binding-linux-arm-gnueabihf": "1.75.0", - "@oxlint/binding-linux-arm-musleabihf": "1.75.0", - "@oxlint/binding-linux-arm64-gnu": "1.75.0", - "@oxlint/binding-linux-arm64-musl": "1.75.0", - "@oxlint/binding-linux-ppc64-gnu": "1.75.0", - "@oxlint/binding-linux-riscv64-gnu": "1.75.0", - "@oxlint/binding-linux-riscv64-musl": "1.75.0", - "@oxlint/binding-linux-s390x-gnu": "1.75.0", - "@oxlint/binding-linux-x64-gnu": "1.75.0", - "@oxlint/binding-linux-x64-musl": "1.75.0", - "@oxlint/binding-openharmony-arm64": "1.75.0", - "@oxlint/binding-win32-arm64-msvc": "1.75.0", - "@oxlint/binding-win32-ia32-msvc": "1.75.0", - "@oxlint/binding-win32-x64-msvc": "1.75.0" + "@oxlint/binding-android-arm-eabi": "1.78.0", + "@oxlint/binding-android-arm64": "1.78.0", + "@oxlint/binding-darwin-arm64": "1.78.0", + "@oxlint/binding-darwin-x64": "1.78.0", + "@oxlint/binding-freebsd-x64": "1.78.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.78.0", + "@oxlint/binding-linux-arm-musleabihf": "1.78.0", + "@oxlint/binding-linux-arm64-gnu": "1.78.0", + "@oxlint/binding-linux-arm64-musl": "1.78.0", + "@oxlint/binding-linux-ppc64-gnu": "1.78.0", + "@oxlint/binding-linux-riscv64-gnu": "1.78.0", + "@oxlint/binding-linux-riscv64-musl": "1.78.0", + "@oxlint/binding-linux-s390x-gnu": "1.78.0", + "@oxlint/binding-linux-x64-gnu": "1.78.0", + "@oxlint/binding-linux-x64-musl": "1.78.0", + "@oxlint/binding-openharmony-arm64": "1.78.0", + "@oxlint/binding-win32-arm64-msvc": "1.78.0", + "@oxlint/binding-win32-ia32-msvc": "1.78.0", + "@oxlint/binding-win32-x64-msvc": "1.78.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", @@ -6942,6 +6944,415 @@ "url": "https://github.com/sponsors/Boshen" } }, + "node_modules/vite-plus/node_modules/@oxlint/binding-android-arm-eabi": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm-eabi/-/binding-android-arm-eabi-1.75.0.tgz", + "integrity": "sha512-lutovtFzJqlRaqpZrCqSSGaHZzl9nIxxpjLzhSRLunN6dCLylj0uzlCyQGaQDIys7rrv8kVXiFO+R4Zpn0bX7g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite-plus/node_modules/@oxlint/binding-android-arm64": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-android-arm64/-/binding-android-arm64-1.75.0.tgz", + "integrity": "sha512-hXI0hDgHkw4w5nfru72aG7y+2iQJmC4waH/KV6H/hbgA6yAP5jYNx0P9yug15Hs0tWl/+mda3Jjn/2gmDT48tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite-plus/node_modules/@oxlint/binding-darwin-arm64": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-arm64/-/binding-darwin-arm64-1.75.0.tgz", + "integrity": "sha512-D91BWbK/dMYfCcrghspPIuKs2D9LF4Z/OabVSQjw1AO6PWxArD7teDA48bm0ySFqWDaPVqmQRl5GMWNglTXyrQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite-plus/node_modules/@oxlint/binding-darwin-x64": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-darwin-x64/-/binding-darwin-x64-1.75.0.tgz", + "integrity": "sha512-02mpwzf12BonZ6PT0TuQoomvEh2kVl2WGBIKWezCyToIS+rYkQZ6GXnARBAl9A4Ovm2V+Xe7M4KretyqmmcnJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite-plus/node_modules/@oxlint/binding-freebsd-x64": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-freebsd-x64/-/binding-freebsd-x64-1.75.0.tgz", + "integrity": "sha512-qZJgLnDaBsiL5YESx2t/TZ8eXkL9fEkKoXEdzegROhlz9A0lgyGnZ0dAzJrh7LJAHQl2K9RdRueN2s/9N7+odg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite-plus/node_modules/@oxlint/binding-linux-arm-gnueabihf": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.75.0.tgz", + "integrity": "sha512-7XlaWA5BJD3XpCfrEqjEe6Zseeb14S7QGa304XfwKignRaKQ+eIj775BQ7nIslggWickl4IsPUFqJ+/gAyNHVg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite-plus/node_modules/@oxlint/binding-linux-arm-musleabihf": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm-musleabihf/-/binding-linux-arm-musleabihf-1.75.0.tgz", + "integrity": "sha512-av6Tpv8yrcMMMOadOqENBhlsLRcGFXXwoQ0hzHhsmS9FJ4Wioy8we427GbcMe2XTxmL2e60T67H1Dyr3up+tAA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite-plus/node_modules/@oxlint/binding-linux-arm64-gnu": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.75.0.tgz", + "integrity": "sha512-WcUhd8fHT5plrA14lANevl+hOl815mVI5t2hU21oFWrZKFXIVV/Sr4rWQV0NzSvzBupbMLNc5ErEA6Ehxh5jMg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite-plus/node_modules/@oxlint/binding-linux-arm64-musl": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.75.0.tgz", + "integrity": "sha512-UWzp5wRHFe/ESO3+eEaxXsTkYTGLYjnTsi/I5neEacXSItQ6WNleapfOAeA4x2b8nyhJ4uQxqvtv9pHv8kWJtQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite-plus/node_modules/@oxlint/binding-linux-ppc64-gnu": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.75.0.tgz", + "integrity": "sha512-XEVRwGMLKCUKrvhLAz4F6AIh8MJrQVdSZtAmPpRZt9tGPsUnamPOcl3dS/ZQzJnar/Ymgc//+xho0L60Emzuxg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite-plus/node_modules/@oxlint/binding-linux-riscv64-gnu": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-gnu/-/binding-linux-riscv64-gnu-1.75.0.tgz", + "integrity": "sha512-mAG4DUXqfLC8cTjMD2kt3jDmVzFREYtDyeLNdLdsCcBc4Zbl2EMuiFektGBilQwkNjYnMvCqJs55U+Hyb+b+jw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite-plus/node_modules/@oxlint/binding-linux-riscv64-musl": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-riscv64-musl/-/binding-linux-riscv64-musl-1.75.0.tgz", + "integrity": "sha512-95hrAvriAlI+pekSomTFIn0+bawMDlDwTNVmdjsFusTHyL2JWh7TWvRNG/Lkim72uN8OiCcO9wcaC6omLP5E3w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite-plus/node_modules/@oxlint/binding-linux-s390x-gnu": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.75.0.tgz", + "integrity": "sha512-4b6f2+FrtruAESrCqIKcrarzfrSx+wk2QNcp+RT91/Prc+pMQMAfyZ1rG1c3tFQNl8Bc616tx40uNXyxNBRPbQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite-plus/node_modules/@oxlint/binding-linux-x64-gnu": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.75.0.tgz", + "integrity": "sha512-nshAhrUvXFUWOvqQ2soIw7HFNWvpvEV4o0cYSqPtzLiPF5gKyYTDOOTJ6Rn8g8K/iGvPIrbDA4v8+5MvnjJrrg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite-plus/node_modules/@oxlint/binding-linux-x64-musl": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-linux-x64-musl/-/binding-linux-x64-musl-1.75.0.tgz", + "integrity": "sha512-e4jNxLKnxLC6sYBQRxrI2pgIIxnmMtF8U/VwNYcjTT/CLS+spH624cYVnj07bTKwaEWT37/e025isOs6j/0xqA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite-plus/node_modules/@oxlint/binding-openharmony-arm64": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-openharmony-arm64/-/binding-openharmony-arm64-1.75.0.tgz", + "integrity": "sha512-hZ2lH+1qLf/DiEP9UWuQTK2JWj/BgvMB4jhIV4SmNU1wfEiYYX4TynQyAZXx0j9X4qRYizAL042SKaV+8ynh4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite-plus/node_modules/@oxlint/binding-win32-arm64-msvc": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.75.0.tgz", + "integrity": "sha512-Ilj6PNzGDS3bCU0MSJH7Msh0NhH+T/mRp2shwg+q+GHeVlPwP5LEboW96aW+3kVKFk6zYZy1Xi5pZkqZh6X8KQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite-plus/node_modules/@oxlint/binding-win32-ia32-msvc": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.75.0.tgz", + "integrity": "sha512-QVit2nOEOiPhkmsrksPSkoGCdnZRNkspt8fwoYyP09te1VEbnSj4LAxua4rc8FKTmWkySVe05j8iz9GXYfF1AQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite-plus/node_modules/@oxlint/binding-win32-x64-msvc": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/@oxlint/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.75.0.tgz", + "integrity": "sha512-DSxnNkBUAYARPwJtR12Ig3deWr8w0H997xP6jy33i+e0SyYJw8FKuz4+cZtpmPEhQmvlPJE3X/2vNxDmLkd/rA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/vite-plus/node_modules/@oxlint/plugins": { + "version": "1.73.0", + "resolved": "https://registry.npmjs.org/@oxlint/plugins/-/plugins-1.73.0.tgz", + "integrity": "sha512-OhgMQeMmZA0dcFcX4/priaJZWdFECxiClgq6mRX6aatZEcV9PbKC3P3/v8U1hVjviT1i5U+vR8lAtBV6m4FXAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/vite-plus/node_modules/oxlint": { + "version": "1.75.0", + "resolved": "https://registry.npmjs.org/oxlint/-/oxlint-1.75.0.tgz", + "integrity": "sha512-m9WzjRcRYA/uqIZDa9tclrieoPJ/ln1QYTKdFx6NUOs8uY5DiHlIwRQoCrHT6OM6O3ww3l2skY5gO7G7ZphE7g==", + "dev": true, + "license": "MIT", + "bin": { + "oxlint": "bin/oxlint" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/Boshen" + }, + "optionalDependencies": { + "@oxlint/binding-android-arm-eabi": "1.75.0", + "@oxlint/binding-android-arm64": "1.75.0", + "@oxlint/binding-darwin-arm64": "1.75.0", + "@oxlint/binding-darwin-x64": "1.75.0", + "@oxlint/binding-freebsd-x64": "1.75.0", + "@oxlint/binding-linux-arm-gnueabihf": "1.75.0", + "@oxlint/binding-linux-arm-musleabihf": "1.75.0", + "@oxlint/binding-linux-arm64-gnu": "1.75.0", + "@oxlint/binding-linux-arm64-musl": "1.75.0", + "@oxlint/binding-linux-ppc64-gnu": "1.75.0", + "@oxlint/binding-linux-riscv64-gnu": "1.75.0", + "@oxlint/binding-linux-riscv64-musl": "1.75.0", + "@oxlint/binding-linux-s390x-gnu": "1.75.0", + "@oxlint/binding-linux-x64-gnu": "1.75.0", + "@oxlint/binding-linux-x64-musl": "1.75.0", + "@oxlint/binding-openharmony-arm64": "1.75.0", + "@oxlint/binding-win32-arm64-msvc": "1.75.0", + "@oxlint/binding-win32-ia32-msvc": "1.75.0", + "@oxlint/binding-win32-x64-msvc": "1.75.0" + }, + "peerDependencies": { + "oxlint-tsgolint": ">=7.0.2001", + "vite-plus": "*" + }, + "peerDependenciesMeta": { + "oxlint-tsgolint": { + "optional": true + }, + "vite-plus": { + "optional": true + } + } + }, "node_modules/vitest": { "version": "4.1.10", "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", diff --git a/package.json b/package.json index e50b9a6..86dc5a1 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "start": "vp dev", "typecheck": "tsc -b", "format:check": "vp fmt --check .", - "lint": "vp lint src/app src/worker scripts test vite.config.ts vitest.config.ts vitest.app.config.ts vitest.migration.config.ts --deny-warnings", + "lint": "vp lint src processor scripts test vite.config.ts vitest.config.ts vitest.app.config.ts vitest.migration.config.ts --deny-warnings", "test": "node scripts/run-worker-tests.mjs && vp test run --config vitest.app.config.ts && vp test run --config vitest.migration.config.ts", "test:worker": "node scripts/run-worker-tests.mjs", "test:app": "vp test run --config vitest.app.config.ts", @@ -43,6 +43,7 @@ "devDependencies": { "@cloudflare/vite-plugin": "^1.50.0", "@cloudflare/vitest-pool-workers": "^0.20.1", + "@oxlint/plugins": "1.78.0", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", "@types/node": "^26.1.2", @@ -50,6 +51,7 @@ "@types/react-dom": "^19.2.3", "@vitejs/plugin-react": "^6.0.5", "jsdom": "^26.1.0", + "oxlint": "1.78.0", "tsx": "^4.23.5", "typescript": "^7.0.2", "vite": "^8.2.0", diff --git a/processor/video-processor.mjs b/processor/video-processor.mjs index 7aa7257..16459fb 100644 --- a/processor/video-processor.mjs +++ b/processor/video-processor.mjs @@ -355,8 +355,8 @@ async function processRequest(payload) { function validatePayload(value) { if ( !value || - typeof value !== 'object' || - typeof value.videoId !== 'string' || + Object.prototype.toString.call(value) !== '[object Object]' || + Object.prototype.toString.call(value.videoId) !== '[object String]' || !/^[a-zA-Z0-9-]{1,128}$/.test(value.videoId) || !Number.isInteger(value.attempt) || value.attempt < 1 diff --git a/scripts/migrate/cli.ts b/scripts/migrate/cli.ts index 50e5dec..c3bbb8d 100644 --- a/scripts/migrate/cli.ts +++ b/scripts/migrate/cli.ts @@ -30,7 +30,7 @@ interface Arguments { async function main() { const args = parseArguments(process.argv.slice(2)); assertExplicitDestination(args.target, args.environment, args.confirmation); - const database = JSON.parse(await readFile(args.database, 'utf8')) as unknown; + const database = JSON.parse(await readFile(args.database, 'utf8')); const manifest = args.storageManifest ? await readStorageManifest(args.storageManifest) : []; @@ -92,13 +92,22 @@ function destination(args: Arguments): ImportOptions { }; } -function parseArguments(argv: string[]): Arguments { - const command = argv.shift() as Arguments['command'] | undefined; - if (!command || !['validate', 'dry-run', 'import', 'reconcile'].includes(command)) { - throw new Error( - 'Usage: cli.ts --database ', - ); +function parseCommand(value: string | undefined): Arguments['command'] { + switch (value) { + case 'validate': + case 'dry-run': + case 'import': + case 'reconcile': + return value; + default: + throw new Error( + 'Usage: cli.ts --database ', + ); } +} + +function parseArguments(argv: string[]): Arguments { + const command = parseCommand(argv.shift()); const flags = new Map(); for (let index = 0; index < argv.length; index += 2) { const flag = argv[index]; @@ -144,7 +153,7 @@ async function output(report: MigrationReport, filename?: string) { if (filename) console.log(`Report: ${path.resolve(filename)}`); } -main().catch((error: unknown) => { - console.error(error instanceof Error ? error.message : error); +main().catch((cause: unknown) => { + console.error(cause instanceof Error ? cause.message : cause); process.exitCode = 1; }); diff --git a/scripts/migrate/import.ts b/scripts/migrate/import.ts index 22f7524..e6730d7 100644 --- a/scripts/migrate/import.ts +++ b/scripts/migrate/import.ts @@ -3,6 +3,7 @@ import {mkdtemp, rm, writeFile} from 'node:fs/promises'; import {tmpdir} from 'node:os'; import path from 'node:path'; +import {isJsonNumber, isJsonString} from '../../src/shared/json'; import type {MigrationData, MigrationReport} from './types'; export type Destination = 'local' | 'cloudflare'; @@ -180,7 +181,9 @@ export function migrationSql(data: MigrationData, destination: Destination = 'lo return `${statements.join('\n')}\n`; } -function sql(strings: TemplateStringsArray, ...values: unknown[]) { +type SqlValue = boolean | null | number | string | undefined; + +function sql(strings: TemplateStringsArray, ...values: SqlValue[]) { return strings.reduce( (result, part, index) => result + part + (index < values.length ? quote(values[index]) : ''), @@ -188,11 +191,10 @@ function sql(strings: TemplateStringsArray, ...values: unknown[]) { ); } -function quote(value: unknown) { +function quote(value: SqlValue) { if (value === null || value === undefined) return 'NULL'; - if (typeof value === 'number') return String(value); - if (typeof value !== 'string') - throw new TypeError('SQL values must be strings or numbers'); + if (isJsonNumber(value)) return String(value); + if (!isJsonString(value)) throw new TypeError('SQL values must be strings or numbers'); return `'${value.replaceAll("'", "''")}'`; } @@ -228,8 +230,7 @@ function wrangler(args: string[], config?: string) { ); } -function commandError(error: unknown) { - if (error && typeof error === 'object' && 'stderr' in error) - return String(error.stderr).trim(); - return error instanceof Error ? error.message : String(error); +function commandError(cause: unknown) { + if (cause instanceof Error && 'stderr' in cause) return String(cause.stderr).trim(); + return cause instanceof Error ? cause.message : String(cause); } diff --git a/scripts/migrate/reconcile.ts b/scripts/migrate/reconcile.ts index 262eb20..d430d1b 100644 --- a/scripts/migrate/reconcile.ts +++ b/scripts/migrate/reconcile.ts @@ -11,7 +11,7 @@ import { } from './types'; import type {Destination, ImportOptions} from './import'; -const tables: Record = { +const tables = { users: 'users', years: 'years', groups: 'groups', @@ -22,12 +22,21 @@ const tables: Record = { votes: 'votes', awards: 'awards', media: 'media', -}; +} satisfies Record; -export function transformedCounts(data: MigrationData): Record { - return Object.fromEntries( - entityNames.map((name) => [name, data[name].length]), - ) as Record; +export function transformedCounts(data: MigrationData) { + return { + users: data.users.length, + years: data.years.length, + groups: data.groups.length, + projects: data.projects.length, + projectMembers: data.projectMembers.length, + awardCategories: data.awardCategories.length, + projectNominations: data.projectNominations.length, + votes: data.votes.length, + awards: data.awards.length, + media: data.media.length, + } satisfies Record; } export function destinationCounts(options: ImportOptions, data: MigrationData) { @@ -38,11 +47,11 @@ export function destinationCounts(options: ImportOptions, data: MigrationData) { const counts: Partial> = {}; const sourceCounts: Partial> = {}; for (const {row} of parsed.flatMap(({results}) => results)) { - const value = JSON.parse(row) as { + const value: { entity: EntityName; kind: 'all' | 'source'; count: number; - }; + } = JSON.parse(row); const target = value.kind === 'all' ? counts : sourceCounts; target[value.entity] = (target[value.entity] ?? 0) + value.count; } @@ -55,7 +64,8 @@ function executeLocalCountQueries(options: ImportOptions, data: MigrationData) { writeFileSync(sqlFile, destinationCountSql(data), {mode: 0o600}); try { const output = wranglerCountCommand(options, ['--file', sqlFile]); - return parseWranglerJson(output) as Array<{results: Array<{row: string}>}>; + const parsed: Array<{results: Array<{row: string}>}> = parseWranglerJson(output); + return parsed; } finally { rmSync(directory, {recursive: true, force: true}); } @@ -64,7 +74,8 @@ function executeLocalCountQueries(options: ImportOptions, data: MigrationData) { function executeRemoteCountQueries(options: ImportOptions, data: MigrationData) { return destinationCountStatements(data).flatMap((statement) => { const output = wranglerCountCommand(options, ['--command', statement]); - return parseWranglerJson(output) as Array<{results: Array<{row: string}>}>; + const parsed: Array<{results: Array<{row: string}>}> = parseWranglerJson(output); + return parsed; }); } @@ -129,41 +140,101 @@ export function reconcileCounts(report: MigrationReport) { } } -function sourceCountQueries(name: EntityName, data: MigrationData) { - const rows = data[name] as unknown as Array>; - if (!rows.length) { - return [`SELECT json_object('entity','${name}','kind','source','count',0) row`]; +function sourceCountQueries(name: EntityName, data: MigrationData): string[] { + switch (name) { + case 'users': + return singleSourceCountQueries( + name, + 'source_uid', + data.users.map((row) => row.sourceUid), + ); + case 'years': + return singleSourceCountQueries( + name, + 'id', + data.years.map((row) => row.id), + ); + case 'groups': + return singleSourceCountQueries( + name, + 'source_id', + data.groups.map((row) => row.sourceId), + ); + case 'projects': + return singleSourceCountQueries( + name, + 'source_id', + data.projects.map((row) => row.sourceId), + ); + case 'projectMembers': + return pairSourceCountQueries( + name, + 'user_id', + data.projectMembers.map((row) => [row.projectId, row.userId]), + ); + case 'awardCategories': + return singleSourceCountQueries( + name, + 'source_id', + data.awardCategories.map((row) => row.sourceId), + ); + case 'projectNominations': + return pairSourceCountQueries( + name, + 'award_category_id', + data.projectNominations.map((row) => [row.projectId, row.awardCategoryId]), + ); + case 'votes': + return singleSourceCountQueries( + name, + 'source_id', + data.votes.map((row) => row.sourceId), + ); + case 'awards': + return singleSourceCountQueries( + name, + 'source_id', + data.awards.map((row) => row.sourceId), + ); + case 'media': + return singleSourceCountQueries( + name, + 'source_id', + data.media.map((row) => row.sourceId), + ); } - return chunks(rows, 100).map((chunk) => sourceCountQuery(name, chunk)); -} - -function sourceCountQuery(name: EntityName, rows: Array>) { - const column = - name === 'projectMembers' || name === 'projectNominations' - ? null - : name === 'years' - ? 'id' - : name === 'users' - ? 'source_uid' - : 'source_id'; - const sourceProperty = - name === 'years' ? 'id' : name === 'users' ? 'sourceUid' : 'sourceId'; - if (column) { - const values = rows.map((row) => `(${quote(row[sourceProperty])})`).join(','); - return `WITH expected(value) AS (VALUES ${values}) +} + +function singleSourceCountQueries(name: EntityName, column: string, values: string[]) { + if (!values.length) return [emptySourceCountQuery(name)]; + return chunks(values, 100).map((chunk) => { + const expected = chunk.map((value) => `(${quote(value)})`).join(','); + return `WITH expected(value) AS (VALUES ${expected}) SELECT json_object('entity','${name}','kind','source','count',COUNT(*)) row FROM ${tables[name]} destination JOIN expected ON destination.${column}=expected.value`; - } - const secondColumn = name === 'projectMembers' ? 'user_id' : 'award_category_id'; - const secondProperty = name === 'projectMembers' ? 'userId' : 'awardCategoryId'; - const values = rows - .map((row) => `(${quote(row.projectId)},${quote(row[secondProperty])})`) - .join(','); - return `WITH expected(project_id, related_id) AS (VALUES ${values}) - SELECT json_object('entity','${name}','kind','source','count',COUNT(*)) row - FROM ${tables[name]} destination JOIN expected - ON destination.project_id=expected.project_id - AND destination.${secondColumn}=expected.related_id`; + }); +} + +function pairSourceCountQueries( + name: EntityName, + secondColumn: string, + values: Array<[string, string]>, +) { + if (!values.length) return [emptySourceCountQuery(name)]; + return chunks(values, 100).map((chunk) => { + const expected = chunk + .map(([projectId, relatedId]) => `(${quote(projectId)},${quote(relatedId)})`) + .join(','); + return `WITH expected(project_id, related_id) AS (VALUES ${expected}) + SELECT json_object('entity','${name}','kind','source','count',COUNT(*)) row + FROM ${tables[name]} destination JOIN expected + ON destination.project_id=expected.project_id + AND destination.${secondColumn}=expected.related_id`; + }); +} + +function emptySourceCountQuery(name: EntityName) { + return `SELECT json_object('entity','${name}','kind','source','count',0) row`; } function chunks(values: T[], size: number) { @@ -172,7 +243,7 @@ function chunks(values: T[], size: number) { ); } -function quote(value: unknown) { +function quote(value: T) { return `'${String(value).replaceAll("'", "''")}'`; } diff --git a/scripts/migrate/transform.ts b/scripts/migrate/transform.ts index f388b87..12d6756 100644 --- a/scripts/migrate/transform.ts +++ b/scripts/migrate/transform.ts @@ -3,15 +3,19 @@ import {readFile} from 'node:fs/promises'; import path from 'node:path'; import { - entityNames, + isJsonNumber, + isJsonObject, + isJsonString, + type JsonInput, + type JsonObject, +} from '../../src/shared/json'; +import { type EntityName, type MigrationData, type MigrationIssue, type StorageManifestEntry, } from './types'; -type JsonObject = Record; - export interface TransformResult { data: MigrationData; issues: MigrationIssue[]; @@ -27,8 +31,8 @@ export interface TransformResult { }>; } -export async function transformFirebaseExport( - value: unknown, +export async function transformFirebaseExport( + value: T, manifest: StorageManifestEntry[] = [], storageRoot?: string, ): Promise { @@ -37,10 +41,18 @@ export async function transformFirebaseExport( const data = emptyData(); const issues: MigrationIssue[] = []; reportUnknownKeys(root, new Set(['users', 'years']), '', issues); - const sourceCounts = Object.fromEntries(entityNames.map((name) => [name, 0])) as Record< - EntityName, - number - >; + const sourceCounts = { + users: 0, + years: 0, + groups: 0, + projects: 0, + projectMembers: 0, + awardCategories: 0, + projectNominations: 0, + votes: 0, + awards: 0, + media: 0, + } satisfies Record; const manifestByPath = new Map(); for (const [index, entry] of manifest.entries()) { const sourcePath = normalizeSourcePath(entry.path); @@ -268,7 +280,7 @@ export async function transformFirebaseExport( data.projectNominations.push({ projectId, awardCategoryId: categoryId, - position: (positionIndex + 1) as 1 | 2, + position: positionIndex === 0 ? 1 : 2, }); } @@ -511,29 +523,30 @@ function safeStorageFile(root: string, relative: string) { return candidate.startsWith(`${absoluteRoot}${path.sep}`) ? candidate : null; } -function object(value: unknown): JsonObject | null { - return value !== null && typeof value === 'object' && !Array.isArray(value) - ? (value as JsonObject) - : null; +function object(value: T): JsonObject | null { + return isJsonObject(value) ? value : null; } -function entries(value: unknown, at: string, issues: MigrationIssue[]) { - if (value === undefined || value === null || value === '') - return [] as Array<[string, unknown]>; +function entries( + value: JsonInput, + at: string, + issues: MigrationIssue[], +): Array<[string, JsonInput]> { + if (value === undefined || value === null || value === '') return []; const record = object(value); if (!record) { issue(issues, 'error', 'INVALID_COLLECTION', at, 'expected an object map'); - return [] as Array<[string, unknown]>; + return []; } return Object.entries(record); } -function text(value: unknown) { - return typeof value === 'string' && value.trim() ? value.trim() : null; +function text(value: JsonInput) { + return isJsonString(value) && value.trim() ? value.trim() : null; } -function timestamp(value: unknown) { - return typeof value === 'number' && Number.isFinite(value) +function timestamp(value: JsonInput) { + return isJsonNumber(value) && Number.isFinite(value) ? new Date(value).toISOString() : '1970-01-01T00:00:00.000Z'; } @@ -583,42 +596,58 @@ function emptyData(): MigrationData { } function detectDuplicates(data: MigrationData, issues: MigrationIssue[]) { - for (const name of entityNames) { - const rows = data[name] as unknown as Array>; - const seen = new Set(); - for (const [index, row] of rows.entries()) { - const key = - name === 'projectMembers' - ? `${stringValue(row.projectId)}:${stringValue(row.userId)}` - : name === 'projectNominations' - ? `${stringValue(row.projectId)}:${stringValue(row.awardCategoryId)}` - : stringValue(row.id); - if (seen.has(key)) issue(issues, 'error', 'DUPLICATE_ID', `${name}/${index}`, key); - seen.add(key); - } - } + checkDuplicates('users', data.users, (row) => row.id, issues); + checkDuplicates('years', data.years, (row) => row.id, issues); + checkDuplicates('groups', data.groups, (row) => row.id, issues); + checkDuplicates('projects', data.projects, (row) => row.id, issues); + checkDuplicates( + 'projectMembers', + data.projectMembers, + (row) => `${row.projectId}:${row.userId}`, + issues, + ); + checkDuplicates('awardCategories', data.awardCategories, (row) => row.id, issues); + checkDuplicates( + 'projectNominations', + data.projectNominations, + (row) => `${row.projectId}:${row.awardCategoryId}`, + issues, + ); + checkDuplicates('votes', data.votes, (row) => row.id, issues); + checkDuplicates('awards', data.awards, (row) => row.id, issues); + checkDuplicates('media', data.media, (row) => row.id, issues); } -function stringValue(value: unknown) { - return typeof value === 'string' ? value : ''; +function checkDuplicates( + name: EntityName, + rows: T[], + keyFor: (row: T) => string, + issues: MigrationIssue[], +) { + const seen = new Set(); + for (const [index, row] of rows.entries()) { + const key = keyFor(row); + if (seen.has(key)) issue(issues, 'error', 'DUPLICATE_ID', `${name}/${index}`, key); + seen.add(key); + } } export async function readStorageManifest( filename: string, ): Promise { - const parsed: unknown = JSON.parse(await readFile(filename, 'utf8')); + const parsed: JsonInput = JSON.parse(await readFile(filename, 'utf8')); if (!Array.isArray(parsed)) throw new Error('Storage manifest must be a JSON array'); return parsed.map((value, index) => { const entry = object(value); - if (!entry || typeof entry.path !== 'string') { + if (!entry || !isJsonString(entry.path)) { throw new Error(`Storage manifest entry ${index} must contain a path`); } return { path: entry.path, - file: typeof entry.file === 'string' ? entry.file : undefined, - size: typeof entry.size === 'number' ? entry.size : undefined, - sha256: typeof entry.sha256 === 'string' ? entry.sha256 : undefined, - contentType: typeof entry.contentType === 'string' ? entry.contentType : undefined, + file: isJsonString(entry.file) ? entry.file : undefined, + size: isJsonNumber(entry.size) ? entry.size : undefined, + sha256: isJsonString(entry.sha256) ? entry.sha256 : undefined, + contentType: isJsonString(entry.contentType) ? entry.contentType : undefined, }; }); } diff --git a/scripts/setup-worktree.mjs b/scripts/setup-worktree.mjs index a3231e2..ff62e21 100644 --- a/scripts/setup-worktree.mjs +++ b/scripts/setup-worktree.mjs @@ -128,7 +128,7 @@ async function masterWorktree() { ), ); const master = worktrees.find((worktree) => worktree.branch === 'refs/heads/master'); - if (typeof master?.worktree !== 'string') { + if (Object.prototype.toString.call(master?.worktree) !== '[object String]') { throw new Error( 'The master branch must have a checked-out worktree to seed local state', ); @@ -148,7 +148,8 @@ async function filesMatch(left, right) { ]); return leftContent.equals(rightContent); } catch (error) { - if (error && typeof error === 'object' && error.code === 'ENOENT') return false; + if (error instanceof Error && 'code' in error && error.code === 'ENOENT') + return false; throw error; } } @@ -157,7 +158,8 @@ async function exists(value) { return stat(value).then( () => true, (error) => { - if (error && typeof error === 'object' && error.code === 'ENOENT') return false; + if (error instanceof Error && 'code' in error && error.code === 'ENOENT') + return false; throw error; }, ); diff --git a/src/app/components/ProjectForm.tsx b/src/app/components/ProjectForm.tsx index 28f34cd..2c2ae11 100644 --- a/src/app/components/ProjectForm.tsx +++ b/src/app/components/ProjectForm.tsx @@ -237,7 +237,10 @@ export function ProjectForm({
{ - if (!event.currentTarget.contains(event.relatedTarget as Node | null)) { + if ( + !(event.relatedTarget instanceof Node) || + !event.currentTarget.contains(event.relatedTarget) + ) { setMemberResultsOpen(false); setHighlightedMember(-1); } diff --git a/src/app/player/audio.ts b/src/app/player/audio.ts index d2f7d23..08ae8e8 100644 --- a/src/app/player/audio.ts +++ b/src/app/player/audio.ts @@ -15,11 +15,21 @@ export interface PlayerAudioGraph { close(): Promise; } +export interface PlayerAudioContext { + state: AudioContextState; + destination: AudioDestinationNode; + createDynamicsCompressor(): DynamicsCompressorNode; + createMediaElementSource(element: HTMLMediaElement): MediaElementAudioSourceNode; + createGain(): GainNode; + resume(): Promise; + close(): Promise; +} + export function createPlayerAudioGraph( elements: [HTMLVideoElement, HTMLVideoElement], - AudioContextClass: typeof AudioContext = window.AudioContext, + createContext: () => PlayerAudioContext = () => new window.AudioContext(), ): PlayerAudioGraph { - const context = new AudioContextClass(); + const context = createContext(); const limiter = context.createDynamicsCompressor(); limiter.threshold.value = -3; limiter.knee.value = 6; @@ -28,13 +38,14 @@ export function createPlayerAudioGraph( limiter.release.value = 0.25; limiter.connect(context.destination); - const gains = elements.map((element) => { + const gainFor = (element: HTMLVideoElement) => { const source = context.createMediaElementSource(element); const gain = context.createGain(); source.connect(gain); gain.connect(limiter); return gain; - }) as [GainNode, GainNode]; + }; + const gains: [GainNode, GainNode] = [gainFor(elements[0]), gainFor(elements[1])]; return { async resume() { diff --git a/src/app/player/controller.ts b/src/app/player/controller.ts index 5ebf413..3cb6872 100644 --- a/src/app/player/controller.ts +++ b/src/app/player/controller.ts @@ -144,8 +144,8 @@ export function createScreeningController({ notify(); void elements[active] .play() - .catch((error: unknown) => - fail(error instanceof Error ? error.message : 'playback could not start'), + .catch((cause: unknown) => + fail(cause instanceof Error ? cause.message : 'playback could not start'), ); }, titleDurationMs); } @@ -210,10 +210,10 @@ export function createScreeningController({ clearTransitionTimers(); elements[active].pause(); index = nextIndex; - active = (nextIndex % 2) as 0 | 1; + active = nextIndex % 2 === 0 ? 0 : 1; await audio.resume(); - await playCurrent().catch((error: unknown) => - fail(error instanceof Error ? error.message : 'playback could not start'), + await playCurrent().catch((cause: unknown) => + fail(cause instanceof Error ? cause.message : 'playback could not start'), ); } diff --git a/src/app/queries/api.ts b/src/app/queries/api.ts index 38e4476..ea20390 100644 --- a/src/app/queries/api.ts +++ b/src/app/queries/api.ts @@ -15,7 +15,7 @@ export async function apiRequest(path: string, init?: RequestInit): Promise(path: string, init?: RequestInit): Promise; + if (response.status === 204) { + // SAFETY: API callers use `void` for endpoints whose documented response is 204. + return undefined as T; + } + return response.json(); } -export function jsonRequest(method: string, body: unknown): RequestInit { +export function jsonRequest(method: string, body: T): RequestInit { return { method, headers: {'Content-Type': 'application/json'}, diff --git a/src/app/session.tsx b/src/app/session.tsx index 19dda1d..3d8355c 100644 --- a/src/app/session.tsx +++ b/src/app/session.tsx @@ -54,7 +54,7 @@ export function SessionProvider({children}: {children: ReactNode}) { fetch('/api/session', {signal: controller.signal}) .then(async (response) => { if (response.ok) { - const result = (await response.json()) as SessionResponse; + const result: SessionResponse = await response.json(); setSession({ status: 'authenticated', user: result.user, @@ -64,7 +64,7 @@ export function SessionProvider({children}: {children: ReactNode}) { return; } - const result = (await response.json()) as ApiErrorResponse; + const result: ApiErrorResponse = await response.json(); if (response.status === 401) { setSession({status: 'unauthenticated', user: null, reason: authError}); } else if (response.status === 403 || result.error.code === 'AUTH_FORBIDDEN') { diff --git a/src/app/video/ProjectVideoPanel.tsx b/src/app/video/ProjectVideoPanel.tsx index 2ab5f46..293d9df 100644 --- a/src/app/video/ProjectVideoPanel.tsx +++ b/src/app/video/ProjectVideoPanel.tsx @@ -203,12 +203,12 @@ export function ProjectVideoPanel(props: { } function VideoStatusCard({video}: {video: ProjectVideo}) { - const labels: Record = { + const labels = { queued: 'queued for processing', processing: 'processing video', ready: 'ready to watch', failed: 'needs attention', - }; + } satisfies Record; return (