diff --git a/package-lock.json b/package-lock.json index 62307fe..23c6d3c 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", diff --git a/package.json b/package.json index e50b9a6..af1ce49 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", @@ -57,6 +59,10 @@ "vitest": "^4.1.10", "wrangler": "^4.118.0" }, + "overrides": { + "@oxlint/plugins": "$@oxlint/plugins", + "oxlint": "$oxlint" + }, "engines": { "node": ">=24.11.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 (