From 5337d35bebc94aa3ee9011c4132e04e11db078cf Mon Sep 17 00:00:00 2001 From: Miraj Shah Date: Sat, 20 Jul 2024 17:18:31 +0530 Subject: [PATCH 1/4] rm react & use typescript --- .gitignore | 18 +- {src/assets => assets}/bolt.svg | 0 {src/assets => assets}/cable.svg | 0 {src/assets => assets}/cloud.svg | 0 {src/assets => assets}/cloud_download.svg | 0 {src/assets => assets}/cloud_error.svg | 0 {src/assets => assets}/comma.svg | 0 .../device_exclamation_c3.svg | 0 {src/assets => assets}/device_question_c3.svg | 0 {src/assets => assets}/done.svg | 0 {src/assets => assets}/exclamation.svg | 0 {src/assets => assets}/fastboot-ports.svg | 0 {src/assets => assets}/frame_alert.svg | 0 {src/assets => assets}/system_update_c3.svg | 0 .../zadig_create_new_device.png | Bin {src/assets => assets}/zadig_form.png | Bin index.html | 265 ++- package-lock.json | 1854 +++++++++++++++++ package.json | 34 +- src/app/App.test.jsx | 10 - src/app/Flash.jsx | 254 --- src/app/favicon.ico | Bin 867 -> 0 bytes src/app/icon.png | Bin 1389 -> 0 bytes src/app/icon.svg | 1 - src/app/index.jsx | 162 -- src/{config.js => config.ts} | 0 src/main.jsx | 14 - src/main.ts | 221 ++ src/utils/android-fastboot.d.ts | 69 + src/utils/blob.js | 23 - src/utils/blob.ts | 24 + src/utils/fastboot.js | 368 ---- src/utils/fastboot.ts | 387 ++++ src/utils/image.js | 17 - src/utils/manifest.js | 95 - src/utils/manifest.ts | 112 + src/utils/progress.js | 42 - src/utils/progress.ts | 50 + src/workers/image.worker.js | 185 -- src/workers/image.worker.ts | 177 ++ tailwind.config.js | 21 +- tsconfig.json | 26 + vite.config.js | 18 - vite.config.ts | 10 + 44 files changed, 3210 insertions(+), 1247 deletions(-) rename {src/assets => assets}/bolt.svg (100%) rename {src/assets => assets}/cable.svg (100%) rename {src/assets => assets}/cloud.svg (100%) rename {src/assets => assets}/cloud_download.svg (100%) rename {src/assets => assets}/cloud_error.svg (100%) rename {src/assets => assets}/comma.svg (100%) rename {src/assets => assets}/device_exclamation_c3.svg (100%) rename {src/assets => assets}/device_question_c3.svg (100%) rename {src/assets => assets}/done.svg (100%) rename {src/assets => assets}/exclamation.svg (100%) rename {src/assets => assets}/fastboot-ports.svg (100%) rename {src/assets => assets}/frame_alert.svg (100%) rename {src/assets => assets}/system_update_c3.svg (100%) rename {src/assets => assets}/zadig_create_new_device.png (100%) rename {src/assets => assets}/zadig_form.png (100%) create mode 100644 package-lock.json delete mode 100644 src/app/App.test.jsx delete mode 100644 src/app/Flash.jsx delete mode 100644 src/app/favicon.ico delete mode 100644 src/app/icon.png delete mode 100644 src/app/icon.svg delete mode 100644 src/app/index.jsx rename src/{config.js => config.ts} (100%) delete mode 100644 src/main.jsx create mode 100644 src/main.ts create mode 100644 src/utils/android-fastboot.d.ts delete mode 100644 src/utils/blob.js create mode 100644 src/utils/blob.ts delete mode 100644 src/utils/fastboot.js create mode 100644 src/utils/fastboot.ts delete mode 100644 src/utils/image.js delete mode 100644 src/utils/manifest.js create mode 100644 src/utils/manifest.ts delete mode 100644 src/utils/progress.js create mode 100644 src/utils/progress.ts delete mode 100644 src/workers/image.worker.js create mode 100644 src/workers/image.worker.ts create mode 100644 tsconfig.json delete mode 100644 vite.config.js create mode 100644 vite.config.ts diff --git a/.gitignore b/.gitignore index c5eab7ea..0ff36cc2 100644 --- a/.gitignore +++ b/.gitignore @@ -5,9 +5,11 @@ .idea *.iml .vscode +!.vscode/extensions.json + # dependencies -/node_modules +node_modules /.pnp .pnp.js /.pnpm-store @@ -20,16 +22,28 @@ /out/ # production -/build +build +dist +dist-ssr # misc .DS_Store *.pem +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? + # debug npm-debug.log* yarn-debug.log* yarn-error.log* +pnpm-debug.log* +lerna-debug.log* +logs +*.log # local env files .env*.local diff --git a/src/assets/bolt.svg b/assets/bolt.svg similarity index 100% rename from src/assets/bolt.svg rename to assets/bolt.svg diff --git a/src/assets/cable.svg b/assets/cable.svg similarity index 100% rename from src/assets/cable.svg rename to assets/cable.svg diff --git a/src/assets/cloud.svg b/assets/cloud.svg similarity index 100% rename from src/assets/cloud.svg rename to assets/cloud.svg diff --git a/src/assets/cloud_download.svg b/assets/cloud_download.svg similarity index 100% rename from src/assets/cloud_download.svg rename to assets/cloud_download.svg diff --git a/src/assets/cloud_error.svg b/assets/cloud_error.svg similarity index 100% rename from src/assets/cloud_error.svg rename to assets/cloud_error.svg diff --git a/src/assets/comma.svg b/assets/comma.svg similarity index 100% rename from src/assets/comma.svg rename to assets/comma.svg diff --git a/src/assets/device_exclamation_c3.svg b/assets/device_exclamation_c3.svg similarity index 100% rename from src/assets/device_exclamation_c3.svg rename to assets/device_exclamation_c3.svg diff --git a/src/assets/device_question_c3.svg b/assets/device_question_c3.svg similarity index 100% rename from src/assets/device_question_c3.svg rename to assets/device_question_c3.svg diff --git a/src/assets/done.svg b/assets/done.svg similarity index 100% rename from src/assets/done.svg rename to assets/done.svg diff --git a/src/assets/exclamation.svg b/assets/exclamation.svg similarity index 100% rename from src/assets/exclamation.svg rename to assets/exclamation.svg diff --git a/src/assets/fastboot-ports.svg b/assets/fastboot-ports.svg similarity index 100% rename from src/assets/fastboot-ports.svg rename to assets/fastboot-ports.svg diff --git a/src/assets/frame_alert.svg b/assets/frame_alert.svg similarity index 100% rename from src/assets/frame_alert.svg rename to assets/frame_alert.svg diff --git a/src/assets/system_update_c3.svg b/assets/system_update_c3.svg similarity index 100% rename from src/assets/system_update_c3.svg rename to assets/system_update_c3.svg diff --git a/src/assets/zadig_create_new_device.png b/assets/zadig_create_new_device.png similarity index 100% rename from src/assets/zadig_create_new_device.png rename to assets/zadig_create_new_device.png diff --git a/src/assets/zadig_form.png b/assets/zadig_form.png similarity index 100% rename from src/assets/zadig_form.png rename to assets/zadig_form.png diff --git a/index.html b/index.html index 3060b4a2..036cf674 100644 --- a/index.html +++ b/index.html @@ -1,22 +1,249 @@ - - - - - - flash.comma.ai - - -
- - - + + + + + flash.comma.ai + + + +
+
+
+ comma +

flash.comma.ai

+

+ This tool allows you to flash AGNOS onto your comma + device. +

+

+ AGNOS is the Ubuntu-based operating system for your + + comma 3/3X + + . +

+
+
+
+

Requirements

+
    +
  • + A web browser which supports WebUSB (such as Google + Chrome, Microsoft Edge, Opera), running on Windows, + macOS, Linux, or Android. +
  • +
  • + A USB-C cable to power your device outside the car. +
  • +
  • + Another USB-C cable to connect the device to your + computer. +
  • +
+

USB Driver

+

+ You need additional driver software for Windows before + you connect your device. +

+
    +
  1. + Download and install + Zadig. +
  2. +
  3. + Under Device in the menu bar, select + Create New Device. + Zadig Create New Device +
  4. +
  5. + Fill in three fields. The first field is just a + description and you can fill in anything. The next + two fields are very important. Fill them in with + 18D1 and D00D + respectively. Press "Install Driver" and + give it a few minutes to install. + Zadig Form +
  6. +
+

+ No additional software is required for macOS or Linux. +

+
+
+ +
+

Fastboot

+

+ Follow these steps to put your device into fastboot + mode: +

+
    +
  1. + Power off the device and wait for the LEDs to switch + off. +
  2. +
  3. + Connect power to the OBD-C port + (port 1). +
  4. +
  5. + Then, + + quickly + + connect the device to your computer using the USB-C + port + (port 2). +
  6. +
  7. + After a few seconds, the device should indicate + it's in fastboot mode and show its serial + number. +
  8. +
+ image showing comma three and two ports. the upper port is labeled 1. the lower port is labeled 2. +

+ If your device shows the comma spinner with a loading + bar, then it's not in fastboot mode. Unplug all + cables, wait for the device to switch off, and try + again. +

+
+
+ +
+

Flashing

+

+ After your device is in fastboot mode, you can click the + button to start flashing. A prompt may appear to select + a device; choose the device labeled "Android". +

+

+ The process can take 15+ minutes depending on your + internet connection and system performance. Do not + unplug the device until all steps are complete. +

+
+
+ +
+

Troubleshooting

+

+ Cannot enter fastboot or device says "Press any key + to continue" +

+

+ Try using a different USB cable or USB port. Sometimes + USB 2.0 ports work better than USB 3.0 (blue) ports. If + you're using a USB hub, try connecting the device + directly to your computer, or alternatively use a USB + hub between your computer and the device. +

+

My device's screen is blank

+

+ The device can still be in fastboot mode and reflashed + normally if the screen isn't displaying anything. A + blank screen is usually caused by installing older + software that doesn't support newer displays. If a + reflash doesn't fix the blank screen, then the + device's display may be damaged. +

+

+ After flashing, device says unable to mount data + partition +

+

+ This is expected after the filesystem is erased. Press + confirm to finish resetting your device. +

+

General Tips

+
    +
  • Try another computer or OS
  • +
  • Try different USB ports on your computer
  • +
  • + Try different USB-C cables, including the OBD-C + cable that came with the device +
  • +
+

Other questions

+

+ If you need help, join our + + Discord server + + and go to the #hw-three-3x channel. +

+
+ + +
+
+
+
+ cable +
+
+
+
+
+
+ + +
+
+
+ + diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..64e69705 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1854 @@ +{ + "name": "@commaai/flash", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@commaai/flash", + "version": "0.1.0", + "dependencies": { + "@fontsource-variable/inter": "^5.0.19", + "@fontsource-variable/jetbrains-mono": "^5.0.21", + "android-fastboot": "github:commaai/fastboot.js#c3ec6fe3c96a48dab46e23d0c8c861af15b2144a", + "comlink": "^4.4.1", + "jssha": "^3.3.1", + "xz-decompress": "^0.2.2" + }, + "devDependencies": { + "@tailwindcss/typography": "^0.5.13", + "@types/node": "^20.14.11", + "autoprefixer": "^10.4.19", + "postcss": "^8.4.39", + "tailwindcss": "^3.4.6", + "typescript": "^5.2.2", + "vite": "^5.3.4" + }, + "engines": { + "node": ">=20.11.0" + } + }, + "node_modules/@alloc/quick-lru": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@fontsource-variable/inter": { + "version": "5.0.19", + "resolved": "https://registry.npmjs.org/@fontsource-variable/inter/-/inter-5.0.19.tgz", + "integrity": "sha512-V5KPpF5o0sI1uNWAdFArC87NDOb/ZJDPXLomEiKmDCYMlDUCTn2flkuAZkyME2rtGOKO7vzCuDJAND0m/5PhDA==" + }, + "node_modules/@fontsource-variable/jetbrains-mono": { + "version": "5.0.21", + "resolved": "https://registry.npmjs.org/@fontsource-variable/jetbrains-mono/-/jetbrains-mono-5.0.21.tgz", + "integrity": "sha512-LL/57KBbM3r0UMuN6tSeYExiBObt0QuGq49m1FyoDFIv1GAcuKU0EQ/GAKJ/yt3R8onOCD3f5X9Dln//G6uzRQ==" + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.1.tgz", + "integrity": "sha512-vk+ma8iC1ebje/ahpxpnrfVQJibTMyHdWpOGZ3JpQ7Mgn/3QNHmPq7YwjZbIE7km73dH5M1e6MRRsnEBW7v5CQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@tailwindcss/typography": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.13.tgz", + "integrity": "sha512-ADGcJ8dX21dVVHIwTRgzrcunY6YY9uSlAHHGVKvkA+vLc5qLwEszvKts40lx7z0qc4clpjclwLeK5rVCV2P/uw==", + "dev": true, + "dependencies": { + "lodash.castarray": "^4.4.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "postcss-selector-parser": "6.0.10" + }, + "peerDependencies": { + "tailwindcss": ">=3.0.0 || insiders" + } + }, + "node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": { + "version": "6.0.10", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz", + "integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@types/estree": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.14.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.11.tgz", + "integrity": "sha512-kprQpL8MMeszbz6ojB5/tU8PLN4kesnN8Gjzw349rDlNgsSzg90lAVj3llK99Dh7JON+t9AuscPPFW6mPbTnSA==", + "dev": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@zip.js/zip.js": { + "version": "2.7.47", + "resolved": "https://registry.npmjs.org/@zip.js/zip.js/-/zip.js-2.7.47.tgz", + "integrity": "sha512-jmtJMA3/Jl4rMzo/DZ79s6g0CJ1AZcNAO6emTy/vHfIKAB/iiFY7PLs6KmbRTJ+F8GnK2eCLnjQfCCneRxXgzg==", + "engines": { + "bun": ">=0.7.0", + "deno": ">=1.0.0", + "node": ">=16.5.0" + } + }, + "node_modules/android-fastboot": { + "version": "1.1.5-commaai", + "resolved": "git+ssh://git@github.com/commaai/fastboot.js.git#c3ec6fe3c96a48dab46e23d0c8c861af15b2144a", + "integrity": "sha512-fbmXKrcRlUpXz+hq05Xi2ySy2VFvXLy5KSWqv+mYCL/KopFxWd069qAAin7tOBh59VU7qfNrjMqED5Sq3uhwEw==", + "dependencies": { + "@zip.js/zip.js": "^2.7.6", + "pako": "^2.1.0" + } + }, + "node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/autoprefixer": { + "version": "10.4.19", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", + "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-lite": "^1.0.30001599", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.23.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.2.tgz", + "integrity": "sha512-qkqSyistMYdxAcw+CzbZwlBy8AGmS/eEWs+sEV5TnLRGDOL+C5M2EnH6tlZyg0YoAxGJAFKh61En9BR941GnHA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001640", + "electron-to-chromium": "^1.4.820", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.1.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001642", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001642.tgz", + "integrity": "sha512-3XQ0DoRgLijXJErLSl+bLnJ+Et4KqV1PY6JJBGAFlsNsz31zeAIncyeZfLCabHK/jtSh+671RM9YMldxjUPZtA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/comlink": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/comlink/-/comlink-4.4.1.tgz", + "integrity": "sha512-+1dlx0aY5Jo1vHy/tSsIGpSkN4tS9rZSW8FIhG0JH/crs9wwweswIo/POr451r7bZww3hFbPAKnTpimzL/mm4Q==" + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.4.829", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.829.tgz", + "integrity": "sha512-5qp1N2POAfW0u1qGAxXEtz6P7bO1m6gpZr5hdf5ve6lxpLM7MpiM4jIPz7xcrNlClQMafbyUDDWjlIQZ1Mw0Rw==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fastq": { + "version": "1.17.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", + "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/foreground-child": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", + "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.0.tgz", + "integrity": "sha512-Dd+Lb2/zvk9SKy1TGCt1wFJFo/MWBPMX5x7KcvLajWTGuomczdQX61PvY5yK6SVACwpoexWo81IfFyoKY2QnTA==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jiti": { + "version": "1.21.6", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.6.tgz", + "integrity": "sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/jssha": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.3.1.tgz", + "integrity": "sha512-VCMZj12FCFMQYcFLPRm/0lOBbLi8uM2BhXPTqw3U4YAfs4AZfiApOoBLoN8cQE60Z50m1MYMTQVCfgF/KaCVhQ==", + "engines": { + "node": "*" + } + }, + "node_modules/lilconfig": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/lodash.castarray": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz", + "integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==", + "dev": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", + "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "dev": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/node-releases": { + "version": "2.0.17", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.17.tgz", + "integrity": "sha512-Ww6ZlOiEQfPfXM45v17oabk77Z7mg5bOt7AjDyzy7RjK9OrLrLC8dyZQoAPEOtFX9SaNf1Tdvr5gRJWdTJj7GA==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true + }, + "node_modules/pako": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pako/-/pako-2.1.0.tgz", + "integrity": "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==" + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picocolors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/postcss": { + "version": "8.4.39", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", + "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.0.1", + "source-map-js": "^1.2.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", + "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", + "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "lilconfig": "^3.0.0", + "yaml": "^2.3.4" + }, + "engines": { + "node": ">= 14" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-load-config/node_modules/lilconfig": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.2.tgz", + "integrity": "sha512-eop+wDAvpItUys0FWkHIKeC9ybYrTGbU41U5K7+bttZZeohvnY7M9dZ5kB21GNWiFT2q1OoPTvncPCgSOVO5ow==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/postcss-nested": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", + "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.11" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.1.tgz", + "integrity": "sha512-b4dlw/9V8A71rLIDsSwVmak9z2DuBUB7CA1/wSdelNEzqsjoSPeADTWNO09lpH49Diy3/JIZ2bSPB1dI3LJCHg==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.1.tgz", + "integrity": "sha512-Elx2UT8lzxxOXMpy5HWQGZqkrQOtrVDDa/bm9l10+U4rQnVzbL/LgZ4NOM1MPIDyHk69W4InuYDF5dzRh4Kw1A==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.5" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.18.1", + "@rollup/rollup-android-arm64": "4.18.1", + "@rollup/rollup-darwin-arm64": "4.18.1", + "@rollup/rollup-darwin-x64": "4.18.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.18.1", + "@rollup/rollup-linux-arm-musleabihf": "4.18.1", + "@rollup/rollup-linux-arm64-gnu": "4.18.1", + "@rollup/rollup-linux-arm64-musl": "4.18.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.18.1", + "@rollup/rollup-linux-riscv64-gnu": "4.18.1", + "@rollup/rollup-linux-s390x-gnu": "4.18.1", + "@rollup/rollup-linux-x64-gnu": "4.18.1", + "@rollup/rollup-linux-x64-musl": "4.18.1", + "@rollup/rollup-win32-arm64-msvc": "4.18.1", + "@rollup/rollup-win32-ia32-msvc": "4.18.1", + "@rollup/rollup-win32-x64-msvc": "4.18.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", + "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/string-width-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/sucrase": { + "version": "3.35.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", + "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "glob": "^10.3.10", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tailwindcss": { + "version": "3.4.6", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.6.tgz", + "integrity": "sha512-1uRHzPB+Vzu57ocybfZ4jh5Q3SdlH7XW23J5sQoM9LhE9eIOlzxer/3XPSsycvih3rboRsvt0QCmzSrqyOYUIA==", + "dev": true, + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.21.0", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, + "node_modules/typescript": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true + }, + "node_modules/update-browserslist-db": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/vite": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.4.tgz", + "integrity": "sha512-Cw+7zL3ZG9/NZBB8C+8QbQZmR54GwqIz+WMI4b3JgdYJvX+ny9AjJXqkGQlDXSXRP9rP0B4tbciRMOVEKulVOA==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.39", + "rollup": "^4.13.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/xz-decompress": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/xz-decompress/-/xz-decompress-0.2.2.tgz", + "integrity": "sha512-DSOnX+ZLVTrsW+CtjZPwjrMWvuRkzCcEpwLsY2faZyVgLH/ZHpTg3h3+KyN16mGuduMgO+/pc9rSEG735oGN0g==", + "engines": { + "node": ">=16" + } + }, + "node_modules/yaml": { + "version": "2.4.5", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz", + "integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + } + } +} diff --git a/package.json b/package.json index 9c54987e..729e4bd5 100644 --- a/package.json +++ b/package.json @@ -5,41 +5,27 @@ "type": "module", "scripts": { "dev": "vite", - "build": "vite build", - "start": "vite preview", - "lint": "eslint . --ext js,jsx --report-unused-disable-directives", - "test": "vitest" + "build": "tsc && vite build", + "preview": "vite preview" }, "engines": { "node": ">=20.11.0" }, "dependencies": { - "@fontsource-variable/inter": "^5.0.18", + "@fontsource-variable/inter": "^5.0.19", "@fontsource-variable/jetbrains-mono": "^5.0.21", "android-fastboot": "github:commaai/fastboot.js#c3ec6fe3c96a48dab46e23d0c8c861af15b2144a", "comlink": "^4.4.1", "jssha": "^3.3.1", - "react": "^18.3.1", - "react-dom": "^18.3.1", - "xz-decompress": "^0.2.1" + "xz-decompress": "^0.2.2" }, "devDependencies": { "@tailwindcss/typography": "^0.5.13", - "@testing-library/jest-dom": "^6.4.5", - "@testing-library/react": "^15.0.7", - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", - "@vitejs/plugin-react": "^4.3.0", - "autoprefixer": "10.4.14", - "eslint": "^8.57.0", - "eslint-plugin-react": "^7.34.2", - "eslint-plugin-react-hooks": "^4.6.2", - "eslint-plugin-react-refresh": "^0.4.7", - "jsdom": "^22.1.0", - "postcss": "^8.4.38", - "tailwindcss": "^3.4.3", - "vite": "^5.2.12", - "vite-svg-loader": "^5.1.0", - "vitest": "^1.6.0" + "@types/node": "^20.14.11", + "autoprefixer": "^10.4.19", + "postcss": "^8.4.39", + "tailwindcss": "^3.4.6", + "typescript": "^5.2.2", + "vite": "^5.3.4" } } diff --git a/src/app/App.test.jsx b/src/app/App.test.jsx deleted file mode 100644 index 206de720..00000000 --- a/src/app/App.test.jsx +++ /dev/null @@ -1,10 +0,0 @@ -import { Suspense } from 'react' -import { expect, test } from 'vitest' -import { render, screen } from '@testing-library/react' - -import App from '.' - -test('renders without crashing', () => { - render() - expect(screen.getByText('flash.comma.ai')).toBeInTheDocument() -}) diff --git a/src/app/Flash.jsx b/src/app/Flash.jsx deleted file mode 100644 index d7c544dc..00000000 --- a/src/app/Flash.jsx +++ /dev/null @@ -1,254 +0,0 @@ -import { useCallback } from 'react' - -import { Step, Error, useFastboot } from '@/utils/fastboot' - -import bolt from '@/assets/bolt.svg' -import cable from '@/assets/cable.svg' -import cloud from '@/assets/cloud.svg' -import cloudDownload from '@/assets/cloud_download.svg' -import cloudError from '@/assets/cloud_error.svg' -import deviceExclamation from '@/assets/device_exclamation_c3.svg' -import deviceQuestion from '@/assets/device_question_c3.svg' -import done from '@/assets/done.svg' -import exclamation from '@/assets/exclamation.svg' -import frameAlert from '@/assets/frame_alert.svg' -import systemUpdate from '@/assets/system_update_c3.svg' - - -const steps = { - [Step.INITIALIZING]: { - status: 'Initializing...', - bgColor: 'bg-gray-400 dark:bg-gray-700', - icon: cloud, - }, - [Step.READY]: { - status: 'Ready', - description: 'Tap the button above to begin', - bgColor: 'bg-[#51ff00]', - icon: bolt, - iconStyle: '', - }, - [Step.CONNECTING]: { - status: 'Waiting for connection', - description: 'Follow the instructions to connect your device to your computer', - bgColor: 'bg-yellow-500', - icon: cable, - }, - [Step.DOWNLOADING]: { - status: 'Downloading...', - bgColor: 'bg-blue-500', - icon: cloudDownload, - }, - [Step.UNPACKING]: { - status: 'Unpacking...', - bgColor: 'bg-blue-500', - icon: cloudDownload, - }, - [Step.FLASHING]: { - status: 'Flashing device...', - description: 'Do not unplug your device until the process is complete.', - bgColor: 'bg-lime-400', - icon: systemUpdate, - }, - [Step.ERASING]: { - status: 'Erasing device...', - bgColor: 'bg-lime-400', - icon: systemUpdate, - }, - [Step.DONE]: { - status: 'Done', - description: 'Your device has been updated successfully. You can now unplug the USB cable from your computer. To ' + - 'complete the system reset, follow the instructions on your device.', - bgColor: 'bg-green-500', - icon: done, - }, -} - -const errors = { - [Error.UNKNOWN]: { - status: 'Unknown error', - description: 'An unknown error has occurred. Restart your browser and try again.', - bgColor: 'bg-red-500', - icon: exclamation, - }, - [Error.UNRECOGNIZED_DEVICE]: { - status: 'Unrecognized device', - description: 'The device connected to your computer is not supported.', - bgColor: 'bg-yellow-500', - icon: deviceQuestion, - }, - [Error.LOST_CONNECTION]: { - status: 'Lost connection', - description: 'The connection to your device was lost. Check that your cables are connected properly and try again.', - icon: cable, - }, - [Error.DOWNLOAD_FAILED]: { - status: 'Download failed', - description: 'The system image could not be downloaded. Check your internet connection and try again.', - icon: cloudError, - }, - [Error.CHECKSUM_MISMATCH]: { - status: 'Download mismatch', - description: 'The system image downloaded does not match the expected checksum. Try again.', - icon: frameAlert, - }, - [Error.FLASH_FAILED]: { - status: 'Flash failed', - description: 'The system image could not be flashed to your device. Try using a different cable, USB port, or ' + - 'computer. If the problem persists, join the #hw-three-3x channel on Discord for help.', - icon: deviceExclamation, - }, - [Error.ERASE_FAILED]: { - status: 'Erase failed', - description: 'The device could not be erased. Try using a different cable, USB port, or computer. If the problem ' + - 'persists, join the #hw-three-3x channel on Discord for help.', - icon: deviceExclamation, - }, - [Error.REQUIREMENTS_NOT_MET]: { - status: 'Requirements not met', - description: 'Your system does not meet the requirements to flash your device. Make sure to use a browser which ' + - 'supports WebUSB and is up to date.', - }, -} - - -function LinearProgress({ value, barColor }) { - if (value === -1 || value > 100) value = 100 - return ( -
-
-
- ) -} - - -function USBIndicator() { - return
- - - - Device connected -
-} - - -function SerialIndicator({ serial }) { - return
- - Serial: - {serial || 'unknown'} - -
-} - - -function DeviceState({ serial }) { - return ( -
- - | - -
- ) -} - - -function beforeUnloadListener(event) { - // NOTE: not all browsers will show this message - event.preventDefault() - return (event.returnValue = "Flash in progress. Are you sure you want to leave?") -} - - -export default function Flash() { - const { - step, - message, - progress, - error, - - onContinue, - onRetry, - - connected, - serial, - } = useFastboot() - - const handleContinue = useCallback(() => { - onContinue?.() - }, [onContinue]) - - const handleRetry = useCallback(() => { - onRetry?.() - }, [onRetry]) - - const uiState = steps[step] - if (error) { - Object.assign(uiState, errors[Error.UNKNOWN], errors[error]) - } - const { status, description, bgColor, icon, iconStyle = 'invert' } = uiState - - let title - if (message && !error) { - title = message + '...' - if (progress >= 0) { - title += ` (${(progress * 100).toFixed(0)}%)` - } - } else { - title = status - } - - // warn the user if they try to leave the page while flashing - if (Step.DOWNLOADING <= step && step <= Step.ERASING) { - window.addEventListener("beforeunload", beforeUnloadListener, { capture: true }) - } else { - window.removeEventListener("beforeunload", beforeUnloadListener, { capture: true }) - } - - return ( -
-
- cable -
-
- -
- {title} - {description} - {error && ( - - ) || false} - {connected && } -
- ) -} diff --git a/src/app/favicon.ico b/src/app/favicon.ico deleted file mode 100644 index fad53e899245ec68a53112f8f7b4a5149f24372b..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 867 zcmV-p1DyN-0096205C8R0096508Ik`02TlM0EtjeM-2)Z3IG5A4M|8uQUCw|FaQ7m zFbD;6hRdK?QuD|gOgy0L?ensNFizDih!gM>?H^m z7D5U^EG-1BwY2mP5bXl7HN~|GrZ`NlFytD)F&~5+7+gZuV%_Z5?96-fUi1gc-p;*! z^SP)JK=Yi|3^7;>85Qf#LwtK}wnb}N#=B@_bp^Hu zA=d6!|35M+pChY8Rsogp>EE^g-@c?VOA1vF^q+><>e^`}6pnfubKG=(YAMC}?J1ZbHa|wJZjtS%> zc3En;;a*`A#1+RcX^T+nM#BM(PEv6S2!K=pQ9}m+J3Ti|a}v0^ES>2Hy8D6On{JxU zBJD_#h@1z+aP0>aDoz$h5mPe_aVM}1&9>)UvxzH{S*&K%( z=5fV}lZDj-B?1ae9+37p6)4G8mBcJ1OeIv>;}~F+Nu>%EOQ>?i0X)CZBriGpq%+s! zD;VQ#$ceB>ogtCW%!nkS}{sBol4P#20KOq1B002ovPDHLkV1m-$gi8Pb diff --git a/src/app/icon.png b/src/app/icon.png deleted file mode 100644 index d5a6bf263a6ddfd3577fcaa1338e21f914785a96..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1389 zcmV-z1(N!SP)pF8FWQhbW?9;ba!ELWdL_~cP?peYja~^aAhuUa%Y?FJQ@H11o}xt zK~!jgy_#KY6jc<*e`j`UN&$lbjI9l#flz%=A&50bBEI+tWl0gCq%px7BxsBn9~43~ z8UxWpV?skhq7V!Xt<*$cj3D6wh!r#{77Eml5c#lt^oum@?(t!ExBE3Scjiv~C4Jbr z=bn51y?4%>J9k7?v85TgCk@XiJPtGnJfJWWn3gpe78p>t2=pqPsDdLM=w;inC8~gx z$V_0ZLW{uSh~xhtuvcM!7djBdL1+Ofq+a25U;|JSWze!9U?c;(t6*mbdMQ+2Qvq#A z)m7Li@Gg@}BW^|ocHD;dzd^@LIv5F9jx0#P0pJna2o3oOSlxx53o>+q44RNs0($uW zf(LjM=t&_>L599F(1f%K>;{rH(cI{PYu`=&p3?%=bw*v&z@{#AmzRO3fE3cE;MviX z@mp~CVHo{u^4hcgP&3!uhrs4e^pi?$6_-#Gl2Z6IRHPpKys0>NNy3!+a6S}1YC=|2 zYAX}49GNGu2S|h)rksBc!QcCeuT80UMT8_Fuy+;GP})!=pc%O%0SAFQLl05H*i|^) zR(?Iq(kWqD8uqs#RfQLe1Wdqd#F1?jPQBoYD8)hm0t>IfD}~qc0-BL~1>TD8evq%_Z|!!^S6mZKz5}(2U!6MEw)Z?o`&9c{B5E<1AJHy zAS;zxg-d`$^^_TdBagx0FaCBlbKR$KOlTTv9bg$2!2|sEW^nKj&+;iilJK;X-3CTx z;22!^*vvL&0c4;t36wpaO&PxhJ?q@;W5VbkaPEU(yXrc(VhuuIK@yl}Hhejyg<25* ze((PIk*J2Jj+*%snC}2H%tM%@QlVyzz@j~H*Ag>xg&7VJ_P1Vznk^4*gZee0x=at; zQkLOUsM%1Lf`+Z(`#HdHm=1o0T2y8%g2kT^C7BO7zztLDph7JQ)wR&r4T%V4q`*xF zxL{&zQmBO`Q4NiUqL&v6eGbrTVr){Vg(76esf5DWq{0b-%|XU9m%@|ZdbeLc4!?)E z2^+SC&N!7gp3K1E1SkQw(5$8Yo`%ktk6M>G=Fful}0dIk5*n-wGH&Fm~PPAMn5 z(JKzFuf4Y3hWmnTY%)$MxtMbsn*9PJHV%gaX23S%6e2L9?#pIZKo>fYao>SPLMPz% zpZ+7-W}HGYuq$^Uk}oByU}w}>80m-1xOaPm8pn^JD%hF(kr&W`UQ!6y)mFyu298?e zcp+3%uNkwtE=_o>Dd;D+E3VrpFeR8G`~XUjO+VM#rqH zkojpiAh0mD@ccQIhSl9@V1;G0Cq9Vw-Gn6yuL3b{Y04NC*q($ZOoE4KpBmIEY!P@d zj`IY_*Ea1**b(*v5UYZGG v@_t}81)tjh*MNS7UI$Jn9O*)>U(WmoAwiys6^&pO00000NkvXXu0mjf$=PlK diff --git a/src/app/icon.svg b/src/app/icon.svg deleted file mode 100644 index c2688769..00000000 --- a/src/app/icon.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/app/index.jsx b/src/app/index.jsx deleted file mode 100644 index 54828b10..00000000 --- a/src/app/index.jsx +++ /dev/null @@ -1,162 +0,0 @@ -import { Suspense, lazy } from 'react' - -import comma from '../assets/comma.svg' -import fastbootPorts from '../assets/fastboot-ports.svg' -import zadigCreateNewDevice from '../assets/zadig_create_new_device.png' -import zadigForm from '../assets/zadig_form.png' - -const Flash = lazy(() => import('./Flash')) - -export default function App() { - const version = import.meta.env.VITE_PUBLIC_GIT_SHA || 'dev' - console.info(`flash.comma.ai version: ${version}`); - return ( -
-
-
- comma -

flash.comma.ai

- -

This tool allows you to flash AGNOS onto your comma device.

-

- AGNOS is the Ubuntu-based operating system for your{" "} - comma 3/3X. -

-
-
- -
-

Requirements

-
    -
  • - A web browser which supports WebUSB (such as Google Chrome, Microsoft Edge, Opera), running on Windows, macOS, Linux, or Android. -
  • -
  • - A USB-C cable to power your device outside the car. -
  • -
  • - Another USB-C cable to connect the device to your computer. -
  • -
-

USB Driver

-

- You need additional driver software for Windows before you connect - your device. -

-
    -
  1. - Download and install Zadig. -
  2. -
  3. - Under Device in the menu bar, select Create New Device. - Zadig Create New Device -
  4. -
  5. - Fill in three fields. The first field is just a description and - you can fill in anything. The next two fields are very important. - Fill them in with 18D1 and D00D respectively. - Press "Install Driver" and give it a few minutes to install. - Zadig Form -
  6. -
-

- No additional software is required for macOS or Linux. -

-
-
- -
-

Fastboot

-

Follow these steps to put your device into fastboot mode:

-
    -
  1. Power off the device and wait for the LEDs to switch off.
  2. -
  3. Connect power to the OBD-C port (port 1).
  4. -
  5. Then, quickly connect - the device to your computer using the USB-C port (port 2).
  6. -
  7. After a few seconds, the device should indicate it's in fastboot mode and show its serial number.
  8. -
- image showing comma three and two ports. the upper port is labeled 1. the lower port is labeled 2. -

- If your device shows the comma spinner with a loading bar, then it's not in fastboot mode. - Unplug all cables, wait for the device to switch off, and try again. -

-
-
- -
-

Flashing

-

- After your device is in fastboot mode, you can click the button to start flashing. A prompt may appear to - select a device; choose the device labeled "Android". -

-

- The process can take 15+ minutes depending on your internet connection and system performance. Do not - unplug the device until all steps are complete. -

-
-
- -
-

Troubleshooting

-

Cannot enter fastboot or device says "Press any key to continue"

-

- Try using a different USB cable or USB port. Sometimes USB 2.0 ports work better than USB 3.0 (blue) ports. - If you're using a USB hub, try connecting the device directly to your computer, or alternatively use a - USB hub between your computer and the device. -

-

My device's screen is blank

-

- The device can still be in fastboot mode and reflashed normally if the screen isn't displaying - anything. A blank screen is usually caused by installing older software that doesn't support newer - displays. If a reflash doesn't fix the blank screen, then the device's display may be damaged. -

-

After flashing, device says unable to mount data partition

-

- This is expected after the filesystem is erased. Press confirm to finish resetting your device. -

-

General Tips

-
    -
  • Try another computer or OS
  • -
  • Try different USB ports on your computer
  • -
  • Try different USB-C cables, including the OBD-C cable that came with the device
  • -
-

Other questions

-

- If you need help, join our Discord server and go to - the #hw-three-3x channel. -

-
- -
-
- flash.comma.ai version: {version} -
-
- -
- Loading...

}> - -
-
- -
- flash.comma.ai version: {version.substring(0, 7)} -
-
- ) -} diff --git a/src/config.js b/src/config.ts similarity index 100% rename from src/config.js rename to src/config.ts diff --git a/src/main.jsx b/src/main.jsx deleted file mode 100644 index 40fea3ef..00000000 --- a/src/main.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react' -import ReactDOM from 'react-dom/client' - -import '@fontsource-variable/inter' -import '@fontsource-variable/jetbrains-mono' - -import './index.css' -import App from './app' - -ReactDOM.createRoot(document.getElementById('root')).render( - - - , -) diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 00000000..4a59cd85 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,221 @@ +import { + FastbootError, + FastbootManager, + FastbootManagerStateType, + FastbootStep, +} from "./utils/fastboot"; +import "@fontsource-variable/inter"; +import "@fontsource-variable/jetbrains-mono"; +import "./index.css"; + +const fb = new FastbootManager(); +fb.init(); + +const linearProgressCtnEl = document.getElementById("linear-progress-ctn")!; +const linearProgressEl = document.getElementById("linear-progress")!; +const titleEl = document.getElementById("title")!; +const iconCtnEl = document.getElementById("icon-ctn")!; + +fb.addEventListener("step", ((event: CustomEvent) => { + const state = event.detail; + updateIcon(iconCtnEl, state); + updateTitle(titleEl, state); +}) as EventListener); + +fb.addEventListener("progress", (( + event: CustomEvent, +) => { + const state = event.detail; + console.log("progress", state); + updateLinearProgress(linearProgressEl, state); +}) as EventListener); + +fb.addEventListener("message", (( + event: CustomEvent, +) => { + const state = event.detail; + updateTitle(titleEl, state); +}) as EventListener); + +fb.addEventListener("error", (( + event: CustomEvent, +) => { + const state = event.detail; + updateIcon(iconCtnEl, state); +}) as EventListener); + +function updateLinearProgress( + element: HTMLElement, + state: FastbootManagerStateType, +) { + const { progress, step } = state; + element.style.transform = `translateX(${progress - 100}%)`; + element.className = `absolute top-0 bottom-0 left-0 w-full transition-all ${fbSteps[step].bgColor}`; + linearProgressCtnEl.style.opacity = progress === -1 ? "0" : "1"; +} + +function updateTitle(el: HTMLElement, state: FastbootManagerStateType) { + const { message, error, progress, step } = state; + let title; + if (message && !error) { + title = message + "..."; + if (progress >= 0) { + title += ` (${(progress * 100).toFixed(0)}%)`; + } + } else { + title = fbSteps[step].status; + } + el.innerHTML = title; + const subtitleEl = document.getElementById("subtitle")!; + subtitleEl.innerHTML = fbSteps[step].description ?? ""; +} + +function updateIcon(el: HTMLElement, state: FastbootManagerStateType) { + const { step, error, onContinue } = state; + el.className = `p-8 rounded-full ${fbSteps[step].bgColor}`; + if (onContinue) { + el.style.cursor = "pointer"; + el.addEventListener("click", onContinue); + } + const img = el.getElementsByTagName("img")[0]; + img.src = fbSteps[step].icon; + img.className = `${!error && step !== FastbootStep.DONE ? "animate-pulse" : ""}`; +} + +function updateRetryButton(state: FastbootManagerStateType) { + const { error } = state; + if (error !== FastbootError.NONE) { + const el = document.getElementById("subtitle")!; + el.insertAdjacentHTML( + "afterend", + ` + + `, + ); + const retryButton = document.getElementById("retry-btn")!; + retryButton.addEventListener("click", retryFlashing); + } else { + const retryButton = document.getElementById("retry-btn"); + if (retryButton) { + retryButton.removeEventListener("click", retryFlashing); + retryButton.remove(); + } + } +} + +function updateDeviceState(state: FastbootManagerStateType) { + const { serial, connected } = state; + if (!connected) { + const deviceStateEl = document.getElementById("device-state"); + if (!deviceStateEl) return; + deviceStateEl.remove(); + return; + } + + let el: Element; + const retryButton = document.getElementById("retry-btn"); + if (retryButton) el = retryButton; + else el = document.getElementById("subtitle")!; + el.insertAdjacentHTML( + "afterend", + ` +
+
+ + + + Device connected +
+ | +
+ + Serial: + ${serial || "unknown"} + +
+
+ `, + ); +} + +function retryFlashing() { + console.debug("[fastboot] on retry"); + window.location.reload(); +} + +const fbSteps: Record< + FastbootStep, + { status: string; bgColor: string; icon: string; description?: string } +> = { + [FastbootStep.INITIALIZING]: { + status: "Initializing...", + bgColor: "bg-gray-400 dark:bg-gray-700", + icon: "assets/cloud.svg", + }, + [FastbootStep.READY]: { + status: "Ready", + description: "Tap the button above to begin", + bgColor: "bg-[#51ff00]", + icon: "assets/bolt.svg", + }, + [FastbootStep.CONNECTING]: { + status: "Waiting for connection", + description: + "Follow the instructions to connect your device to your computer", + bgColor: "bg-yellow-500", + icon: "assets/cable.svg", + }, + [FastbootStep.DOWNLOADING]: { + status: "Downloading...", + bgColor: "bg-blue-500", + icon: "assets/cloud_download.svg", + }, + [FastbootStep.UNPACKING]: { + status: "Unpacking...", + bgColor: "bg-blue-500", + icon: "assets/cloud_download.svg", + }, + [FastbootStep.FLASHING]: { + status: "Flashing device...", + description: "Do not unplug your device until the process is complete.", + bgColor: "bg-lime-400", + icon: "assets/system_update_c3.svg", + }, + [FastbootStep.ERASING]: { + status: "Erasing device...", + bgColor: "bg-lime-400", + icon: "assets/system_update_c3.svg", + }, + [FastbootStep.DONE]: { + status: "Done", + description: + "Your device has been updated successfully. You can now unplug the USB cable from your computer. To " + + "complete the system reset, follow the instructions on your device.", + bgColor: "bg-green-500", + icon: "assets/done.svg", + }, +}; + +updateIcon(iconCtnEl, fb.state); +updateTitle(titleEl, fb.state); +updateLinearProgress(linearProgressEl, fb.state); +updateRetryButton(fb.state); +updateDeviceState(fb.state); diff --git a/src/utils/android-fastboot.d.ts b/src/utils/android-fastboot.d.ts new file mode 100644 index 00000000..d85bd58b --- /dev/null +++ b/src/utils/android-fastboot.d.ts @@ -0,0 +1,69 @@ +declare module "android-fastboot" { + export class FastbootError extends Error {} + export class FastbootDevice { + /** + * Request the user to select a USB device and connect to it using the + * fastboot protocol. + * + * @throws {UsbError} + */ + connect(): Promise; + get isConnected(): boolean; + /** + * Wait for the current USB device to disconnect, if it's still connected. + * Returns immediately if no device is connected. + */ + waitForDisconnect(): Promise; + /** + * Wait for the USB device to connect. Returns at the next connection, + * regardless of whether the connected USB device matches the previous one. + */ + waitForConnect(onReconnect?: () => void): Promise; + + /** + * Read the value of a bootloader variable. Returns undefined if the variable + * does not exist. + * @throws {FastbootError} + */ + getVariable(varName: string): Promise; + /** + * Send a textual command to the bootloader and read the response. + * This is in raw fastboot format, not AOSP fastboot syntax. + * + * @param {string} command - The command to send. + * @returns {Promise} Object containing response text and data size, if any. + * @throws {FastbootError} + */ + runCommand( + cmd: string, + ): Promise<{ textSize: string; dataSize?: string | number }>; + + /** + * Flash the given Blob to the given partition on the device. Any image + * format supported by the bootloader is allowed, e.g. sparse or raw images. + * Large raw images will be converted to sparse images automatically, and + * large sparse images will be split and flashed in multiple passes + * depending on the bootloader's payload size limit. + * + * @param {string} partition - The name of the partition to flash. + * @param {Blob} blob - The Blob to retrieve data from. + * @param {"a" | "b" | "current" | "other"} targetSlot - Which slot to flash to, if partition + * is A/B. Defaults to current slot. + * @param {FlashProgressCallback} onProgress - Callback for flashing progress updates. + * @throws {FastbootError} + */ + flashBlob( + partition: string, + blob: Blob, + onProgress: (p: number) => void, + targetSlot: "a" | "b" | "current" | "other", + ): Promise; + } + /** + * Change the debug level for the fastboot client: + * - 0 = silent + * - 1 = debug, recommended for general use + * - 2 = verbose, for debugging only + */ + export function setDebugLevel(level: number): void; +} diff --git a/src/utils/blob.js b/src/utils/blob.js deleted file mode 100644 index 27e0422f..00000000 --- a/src/utils/blob.js +++ /dev/null @@ -1,23 +0,0 @@ -export async function download(url) { - const response = await fetch(url, { mode: 'cors' }) - const reader = response.body.getReader() - const contentLength = +response.headers.get('Content-Length') - console.debug('[blob] Downloading', url, contentLength) - - const chunks = [] - while (true) { - const { done, value } = await reader.read() - if (done) break - chunks.push(value) - } - - const blob = new Blob(chunks) - console.debug('[blob] Downloaded', url, blob.size) - if (blob.size !== contentLength) console.warn('[blob] Download size mismatch', { - url, - expected: contentLength, - actual: blob.size, - }) - - return blob -} diff --git a/src/utils/blob.ts b/src/utils/blob.ts new file mode 100644 index 00000000..5d57823d --- /dev/null +++ b/src/utils/blob.ts @@ -0,0 +1,24 @@ +export async function download(url: string) { + const response = await fetch(url, { mode: "cors" }); + const reader = response.body!.getReader(); + const contentLength = +response.headers.get("Content-Length")!; + console.debug("[blob] Downloading", url, contentLength); + + const chunks = []; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + chunks.push(value); + } + + const blob = new Blob(chunks); + console.debug("[blob] Downloaded", url, blob.size); + if (blob.size !== contentLength) + console.warn("[blob] Download size mismatch", { + url, + expected: contentLength, + actual: blob.size, + }); + + return blob; +} diff --git a/src/utils/fastboot.js b/src/utils/fastboot.js deleted file mode 100644 index a9af8655..00000000 --- a/src/utils/fastboot.js +++ /dev/null @@ -1,368 +0,0 @@ -import { useEffect, useRef, useState } from 'react' - -import { FastbootDevice, setDebugLevel } from 'android-fastboot' -import * as Comlink from 'comlink' - -import config from '@/config' -import { download } from '@/utils/blob' -import { useImageWorker } from '@/utils/image' -import { createManifest } from '@/utils/manifest' -import { withProgress } from '@/utils/progress' - -/** - * @typedef {import('./manifest.js').Image} Image - */ - -// Verbose logging for fastboot -setDebugLevel(2) - -export const Step = { - INITIALIZING: 0, - READY: 1, - CONNECTING: 2, - DOWNLOADING: 3, - UNPACKING: 4, - FLASHING: 6, - ERASING: 7, - DONE: 8, -} - -export const Error = { - UNKNOWN: -1, - NONE: 0, - UNRECOGNIZED_DEVICE: 1, - LOST_CONNECTION: 2, - DOWNLOAD_FAILED: 3, - UNPACK_FAILED: 4, - CHECKSUM_MISMATCH: 5, - FLASH_FAILED: 6, - ERASE_FAILED: 7, - REQUIREMENTS_NOT_MET: 8, -} - -function isRecognizedDevice(deviceInfo) { - // check some variables are as expected for a comma three - const { - kernel, - "max-download-size": maxDownloadSize, - "slot-count": slotCount, - } = deviceInfo - if (kernel !== "uefi" || maxDownloadSize !== "104857600" || slotCount !== "2") { - console.error('[fastboot] Unrecognised device (kernel, maxDownloadSize or slotCount)', deviceInfo) - return false - } - - const partitions = [] - for (const key of Object.keys(deviceInfo)) { - if (!key.startsWith("partition-type:")) continue - let partition = key.substring("partition-type:".length) - if (partition.endsWith("_a") || partition.endsWith("_b")) { - partition = partition.substring(0, partition.length - 2) - } - if (partitions.includes(partition)) continue - partitions.push(partition) - } - - // check we have the expected partitions to make sure it's a comma three - const expectedPartitions = [ - "ALIGN_TO_128K_1", "ALIGN_TO_128K_2", "ImageFv", "abl", "aop", "apdp", "bluetooth", "boot", "cache", - "cdt", "cmnlib", "cmnlib64", "ddr", "devcfg", "devinfo", "dip", "dsp", "fdemeta", "frp", "fsc", "fsg", - "hyp", "keymaster", "keystore", "limits", "logdump", "logfs", "mdtp", "mdtpsecapp", "misc", "modem", - "modemst1", "modemst2", "msadp", "persist", "qupfw", "rawdump", "sec", "splash", "spunvm", "ssd", - "sti", "storsec", "system", "systemrw", "toolsfv", "tz", "userdata", "vm-linux", "vm-system", "xbl", - "xbl_config" - ] - if (!partitions.every(partition => expectedPartitions.includes(partition))) { - console.error('[fastboot] Unrecognised device (partitions)', partitions) - return false - } - - // sanity check, also useful for logging - if (!deviceInfo['serialno']) { - console.error('[fastboot] Unrecognised device (missing serialno)', deviceInfo) - return false - } - - return true -} - -export function useFastboot() { - const [step, _setStep] = useState(Step.INITIALIZING) - const [message, _setMessage] = useState('') - const [progress, setProgress] = useState(0) - const [error, _setError] = useState(Error.NONE) - - const [connected, setConnected] = useState(false) - const [serial, setSerial] = useState(null) - - const [onContinue, setOnContinue] = useState(null) - const [onRetry, setOnRetry] = useState(null) - - const imageWorker = useImageWorker() - const fastboot = useRef(new FastbootDevice()) - - /** @type {React.RefObject} */ - const manifest = useRef(null) - - function setStep(step) { - _setStep(step) - } - - function setMessage(message = '') { - if (message) console.info('[fastboot]', message) - _setMessage(message) - } - - function setError(error) { - _setError(error) - } - - useEffect(() => { - setProgress(-1) - setMessage() - - if (error) return - if (!imageWorker.current) { - console.debug('[fastboot] Waiting for image worker') - return - } - - switch (step) { - case Step.INITIALIZING: { - // Check that the browser supports WebUSB - if (typeof navigator.usb === 'undefined') { - console.error('[fastboot] WebUSB not supported') - setError(Error.REQUIREMENTS_NOT_MET) - break - } - - // Check that the browser supports Web Workers - if (typeof Worker === 'undefined') { - console.error('[fastboot] Web Workers not supported') - setError(Error.REQUIREMENTS_NOT_MET) - break - } - - // Check that the browser supports Storage API - if (typeof Storage === 'undefined') { - console.error('[fastboot] Storage API not supported') - setError(Error.REQUIREMENTS_NOT_MET) - break - } - - // TODO: change manifest once alt image is in release - imageWorker.current?.init() - .then(() => download(config.manifests['master'])) - .then(blob => blob.text()) - .then(text => { - manifest.current = createManifest(text) - - // sanity check - if (manifest.current.length === 0) { - throw 'Manifest is empty' - } - - console.debug('[fastboot] Loaded manifest', manifest.current) - setStep(Step.READY) - }) - .catch((err) => { - console.error('[fastboot] Initialization error', err) - setError(Error.UNKNOWN) - }) - break - } - - case Step.READY: { - // wait for user interaction (we can't use WebUSB without user event) - setOnContinue(() => () => { - setOnContinue(null) - setStep(Step.CONNECTING) - }) - break - } - - case Step.CONNECTING: { - fastboot.current.waitForConnect() - .then(() => { - console.info('[fastboot] Connected', { fastboot: fastboot.current }) - return fastboot.current.getVariable('all') - .then((all) => { - const deviceInfo = all.split('\n').reduce((obj, line) => { - const parts = line.split(':') - const key = parts.slice(0, -1).join(':').trim() - obj[key] = parts.slice(-1)[0].trim() - return obj - }, {}) - - const recognized = isRecognizedDevice(deviceInfo) - console.debug('[fastboot] Device info', { recognized, deviceInfo }) - - if (!recognized) { - setError(Error.UNRECOGNIZED_DEVICE) - return - } - - setSerial(deviceInfo['serialno'] || 'unknown') - setConnected(true) - setStep(Step.DOWNLOADING) - }) - .catch((err) => { - console.error('[fastboot] Error getting device information', err) - setError(Error.UNKNOWN) - }) - }) - .catch((err) => { - console.error('[fastboot] Connection lost', err) - setError(Error.LOST_CONNECTION) - setConnected(false) - }) - - fastboot.current.connect() - .catch((err) => { - console.error('[fastboot] Connection error', err) - setStep(Step.READY) - }) - break - } - - case Step.DOWNLOADING: { - setProgress(0) - - async function downloadImages() { - for await (const [image, onProgress] of withProgress(manifest.current, setProgress)) { - setMessage(`Downloading ${image.name}`) - await imageWorker.current.downloadImage(image, Comlink.proxy(onProgress)) - } - } - - downloadImages() - .then(() => { - console.debug('[fastboot] Downloaded all images') - setStep(Step.UNPACKING) - }) - .catch((err) => { - console.error('[fastboot] Download error', err) - setError(Error.DOWNLOAD_FAILED) - }) - break - } - - case Step.UNPACKING: { - setProgress(0) - - async function unpackImages() { - for await (const [image, onProgress] of withProgress(manifest.current, setProgress)) { - setMessage(`Unpacking ${image.name}`) - await imageWorker.current.unpackImage(image, Comlink.proxy(onProgress)) - } - } - - unpackImages() - .then(() => { - console.debug('[fastboot] Unpacked all images') - setStep(Step.FLASHING) - }) - .catch((err) => { - console.error('[fastboot] Unpack error', err) - if (err.startsWith('Checksum mismatch')) { - setError(Error.CHECKSUM_MISMATCH) - } else { - setError(Error.UNPACK_FAILED) - } - }) - break - } - - case Step.FLASHING: { - setProgress(0) - - async function flashDevice() { - const currentSlot = await fastboot.current.getVariable('current-slot') - if (!['a', 'b'].includes(currentSlot)) { - throw `Unknown current slot ${currentSlot}` - } - - for await (const [image, onProgress] of withProgress(manifest.current, setProgress)) { - const fileHandle = await imageWorker.current.getImage(image) - const blob = await fileHandle.getFile() - - if (image.sparse) { - setMessage(`Erasing ${image.name}`) - await fastboot.current.runCommand(`erase:${image.name}`) - } - setMessage(`Flashing ${image.name}`) - await fastboot.current.flashBlob(image.name, blob, onProgress, 'other') - } - console.debug('[fastboot] Flashed all partitions') - - const otherSlot = currentSlot === 'a' ? 'b' : 'a' - setMessage(`Changing slot to ${otherSlot}`) - await fastboot.current.runCommand(`set_active:${otherSlot}`) - } - - flashDevice() - .then(() => { - console.debug('[fastboot] Flash complete') - setStep(Step.ERASING) - }) - .catch((err) => { - console.error('[fastboot] Flashing error', err) - setError(Error.FLASH_FAILED) - }) - break - } - - case Step.ERASING: { - setProgress(0) - - async function eraseDevice() { - setMessage('Erasing userdata') - await fastboot.current.runCommand('erase:userdata') - setProgress(0.9) - - setMessage('Rebooting') - await fastboot.current.runCommand('continue') - setProgress(1) - setConnected(false) - } - - eraseDevice() - .then(() => { - console.debug('[fastboot] Erase complete') - setStep(Step.DONE) - }) - .catch((err) => { - console.error('[fastboot] Erase error', err) - setError(Error.ERASE_FAILED) - }) - break - } - } - }, [error, imageWorker, step]) - - useEffect(() => { - if (error !== Error.NONE) { - console.debug('[fastboot] error', error) - setProgress(-1) - setOnContinue(null) - - setOnRetry(() => () => { - console.debug('[fastboot] on retry') - window.location.reload() - }) - } - }, [error]) - - return { - step, - message, - progress, - error, - - connected, - serial, - - onContinue, - onRetry, - } -} diff --git a/src/utils/fastboot.ts b/src/utils/fastboot.ts new file mode 100644 index 00000000..96ccd7aa --- /dev/null +++ b/src/utils/fastboot.ts @@ -0,0 +1,387 @@ +import * as Comlink from "comlink"; +import { FastbootDevice, setDebugLevel } from "android-fastboot"; +import type { ImageWorkerType } from "../workers/image.worker"; +import { download } from "./blob"; +import config from "../config"; +import { createManifest, Image as ManifestImage } from "./manifest"; +import { withProgress } from "./progress"; + +setDebugLevel(2); + +export class FastbootManager extends EventTarget { + state: FastbootManagerStateType; + imageWorker: Comlink.Remote; + device: FastbootDevice; + manifest: ManifestImage[] | null; + + constructor() { + super(); + this.state = { + step: FastbootStep.INITIALIZING, + message: "", + progress: -1, + error: FastbootError.NONE, + connected: false, + serial: null, + }; + this.imageWorker = Comlink.wrap( + new Worker(new URL("../workers/image.worker.ts", import.meta.url), { + type: "module", + }), + ); + this.device = new FastbootDevice(); + this.manifest = null; + } + + private setStep(step: FastbootStep) { + this.state.step = step; + this.dispatchEvent(new CustomEvent("step", { detail: this.state })); + } + + private setError(error: FastbootError) { + this.state.error = error; + this.dispatchEvent(new CustomEvent("error", { detail: this.state })); + } + + private setSerial(serial: string) { + this.state.serial = serial; + this.dispatchEvent(new CustomEvent("serial", { detail: this.state })); + } + + private setConnected(isConnected: boolean) { + this.state.connected = isConnected; + this.dispatchEvent(new CustomEvent("connected", { detail: this.state })); + } + + private setProgress(progress: number) { + this.state.progress = progress; + this.dispatchEvent(new CustomEvent("progress", { detail: this.state })); + } + + private setMessage(message: string) { + this.state.message = message; + this.dispatchEvent(new CustomEvent("message", { detail: this.state })); + } + + init() { + // @ts-ignore Check that the browser supports WebUSB + if (typeof navigator.usb === "undefined") { + console.error("[fastboot] WebUSB not supported"); + this.setError(FastbootError.REQUIREMENTS_NOT_MET); + return; + } + + // Check that the browser supports Web Workers + if (typeof Worker === "undefined") { + console.error("[fastboot] Web Workers not supported"); + this.setError(FastbootError.REQUIREMENTS_NOT_MET); + return; + } + + // Check that the browser supports Storage API + if (typeof Storage === "undefined") { + console.error("[fastboot] Storage API not supported"); + this.setError(FastbootError.REQUIREMENTS_NOT_MET); + return; + } + + this.imageWorker + .init() + .then(() => download(config.manifests["master"])) + .then((blob) => blob.text()) + .then((text) => { + this.manifest = createManifest(text); + // sanity check + if (this.manifest.length === 0) { + throw "Manifest is empty"; + } + console.debug("[fastboot] Loaded manifest", this.manifest); + this.state.onContinue = () => this.connectDevice(); + this.setStep(FastbootStep.READY); + }) + .catch((err) => { + console.error("[fastboot] Initialization error", err); + this.setError(FastbootError.UNKNOWN); + }); + } + + async connectDevice() { + this.device.connect().catch((err) => { + console.error("[fastboot] Connection error", err); + this.setStep(FastbootStep.READY); + }); + this.setStep(FastbootStep.CONNECTING); + try { + await this.device.waitForConnect(); + console.info("[fastboot] Connected", { + fastboot: this.device, + }); + try { + const all = await this.device.getVariable("all"); + const deviceInfo = all.split("\n").reduce( + (obj, line) => { + const parts = line.split(":"); + const key = parts.slice(0, -1).join(":").trim(); + obj[key] = parts.slice(-1)[0].trim(); + return obj; + }, + {} as Record, + ); + + const recognized = isRecognizedDevice(deviceInfo); + console.debug("[fastboot] Device info", { + recognized, + deviceInfo, + }); + + if (!recognized) { + this.setError(FastbootError.UNRECOGNIZED_DEVICE); + return; + } + + this.setSerial(deviceInfo["serialno"] || "unknown"); + this.setConnected(true); + this.downloadImages(); + } catch (err) { + console.error("[fastboot] Error getting device information", err); + this.setError(FastbootError.UNKNOWN); + } + } catch (err) { + console.error("[fastboot] Connection lost", err); + this.setError(FastbootError.LOST_CONNECTION); + this.setConnected(false); + } + } + + async downloadImages() { + this.setStep(FastbootStep.DOWNLOADING); + this.setProgress(0); + try { + for await (const [image, onProgress] of withProgress( + this.manifest!, + this.setProgress, + )) { + this.setMessage(`Downloading ${image.name}`); + await this.imageWorker.downloadImage(image, Comlink.proxy(onProgress)); + } + this.unpackImages(); + } catch (err) { + console.error("[fastboot] Download error", err); + this.setError(FastbootError.DOWNLOAD_FAILED); + } + } + + async unpackImages() { + this.setStep(FastbootStep.UNPACKING); + this.setProgress(0); + try { + for await (const [image, onProgress] of withProgress( + this.manifest!, + this.setProgress, + )) { + this.setMessage(`Unpacking ${image.name}`); + await this.imageWorker.unpackImage(image, Comlink.proxy(onProgress)); + } + this.flashDevice(); + } catch (err) { + console.error("[fastboot] Unpack error", err); + if ((err as string).startsWith("Checksum mismatch")) { + this.setError(FastbootError.CHECKSUM_MISMATCH); + } else { + this.setError(FastbootError.UNPACK_FAILED); + } + } + } + + async flashDevice() { + this.setStep(FastbootStep.FLASHING); + this.setProgress(0); + try { + const currentSlot = await this.device.getVariable("current-slot"); + if (!["a", "b"].includes(currentSlot)) { + throw `Unknown current slot ${currentSlot}`; + } + + for await (const [image, onProgress] of withProgress( + this.manifest!, + this.setProgress, + )) { + const fileHandle = await this.imageWorker.getImage(image); + const blob = await fileHandle.getFile(); + + if (image.sparse) { + this.setMessage(`Erasing ${image.name}`); + await this.device.runCommand(`erase:${image.name}`); + } + this.setMessage(`Flashing ${image.name}`); + await this.device.flashBlob(image.name, blob, onProgress, "other"); + } + console.debug("[fastboot] Flashed all partitions"); + + const otherSlot = currentSlot === "a" ? "b" : "a"; + this.setMessage(`Changing slot to ${otherSlot}`); + await this.device.runCommand(`set_active:${otherSlot}`); + console.debug("[fastboot] Flash complete"); + this.eraseDevice(); + } catch (err) { + console.error("[fastboot] Flashing error", err); + this.setError(FastbootError.FLASH_FAILED); + } + } + + async eraseDevice() { + this.setStep(FastbootStep.ERASING); + this.setProgress(0); + try { + this.setMessage("Erasing userdata"); + await this.device.runCommand("erase:userdata"); + this.setProgress(0.9); + + this.setMessage("Rebooting"); + await this.device.runCommand("continue"); + this.setProgress(1); + this.setConnected(false); + } catch (err) { + console.error("[fastboot] Erase error", err); + this.setError(FastbootError.ERASE_FAILED); + } + } +} + +export type FastbootManagerStateType = { + step: FastbootStep; + message: string; + progress: number; + error: FastbootError; + connected: boolean; + serial: string | null; + onContinue?: () => any; +}; + +export enum FastbootStep { + INITIALIZING = 0, + READY, + CONNECTING, + DOWNLOADING, + UNPACKING, + FLASHING, + ERASING, + DONE, +} + +export enum FastbootError { + UNKNOWN = -1, + NONE, + UNRECOGNIZED_DEVICE, + LOST_CONNECTION, + DOWNLOAD_FAILED, + UNPACK_FAILED, + CHECKSUM_MISMATCH, + FLASH_FAILED, + ERASE_FAILED, + REQUIREMENTS_NOT_MET, +} + +function isRecognizedDevice(deviceInfo: Record) { + // check some variables are as expected for a comma three + const { + kernel, + "max-download-size": maxDownloadSize, + "slot-count": slotCount, + } = deviceInfo; + if ( + kernel !== "uefi" || + maxDownloadSize !== "104857600" || + slotCount !== "2" + ) { + console.error( + "[fastboot] Unrecognised device (kernel, maxDownloadSize or slotCount)", + deviceInfo, + ); + return false; + } + + const partitions: string[] = []; + for (const key of Object.keys(deviceInfo)) { + if (!key.startsWith("partition-type:")) continue; + let partition = key.substring("partition-type:".length); + if (partition.endsWith("_a") || partition.endsWith("_b")) { + partition = partition.substring(0, partition.length - 2); + } + if (partitions.includes(partition)) continue; + partitions.push(partition); + } + + // check we have the expected partitions to make sure it's a comma three + const expectedPartitions = [ + "ALIGN_TO_128K_1", + "ALIGN_TO_128K_2", + "ImageFv", + "abl", + "aop", + "apdp", + "bluetooth", + "boot", + "cache", + "cdt", + "cmnlib", + "cmnlib64", + "ddr", + "devcfg", + "devinfo", + "dip", + "dsp", + "fdemeta", + "frp", + "fsc", + "fsg", + "hyp", + "keymaster", + "keystore", + "limits", + "logdump", + "logfs", + "mdtp", + "mdtpsecapp", + "misc", + "modem", + "modemst1", + "modemst2", + "msadp", + "persist", + "qupfw", + "rawdump", + "sec", + "splash", + "spunvm", + "ssd", + "sti", + "storsec", + "system", + "systemrw", + "toolsfv", + "tz", + "userdata", + "vm-linux", + "vm-system", + "xbl", + "xbl_config", + ]; + if ( + !partitions.every((partition) => expectedPartitions.includes(partition)) + ) { + console.error("[fastboot] Unrecognised device (partitions)", partitions); + return false; + } + + // sanity check, also useful for logging + if (!deviceInfo["serialno"]) { + console.error( + "[fastboot] Unrecognised device (missing serialno)", + deviceInfo, + ); + return false; + } + + return true; +} diff --git a/src/utils/image.js b/src/utils/image.js deleted file mode 100644 index ed27f09f..00000000 --- a/src/utils/image.js +++ /dev/null @@ -1,17 +0,0 @@ -import { useEffect, useRef } from 'react' - -import * as Comlink from 'comlink' - -export function useImageWorker() { - const apiRef = useRef() - - useEffect(() => { - const worker = new Worker(new URL('../workers/image.worker', import.meta.url), { - type: 'module', - }) - apiRef.current = Comlink.wrap(worker) - return () => worker.terminate() - }, []) - - return apiRef -} diff --git a/src/utils/manifest.js b/src/utils/manifest.js deleted file mode 100644 index 9afdd487..00000000 --- a/src/utils/manifest.js +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Represents a partition image defined in the AGNOS manifest. - * - * Image archives can be retrieved from {@link archiveUrl}. - */ -export class Image { - /** - * Partition name - * @type {string} - */ - name - - /** - * SHA-256 checksum of the image, encoded as a hex string - * @type {string} - */ - checksum - /** - * Size of the unpacked image in bytes - * @type {number} - */ - size - /** - * Whether the image is sparse - * @type {boolean} - */ - sparse - - /** - * Name of the image file - * @type {string} - */ - fileName - - /** - * Name of the image archive file - * @type {string} - */ - archiveFileName - /** - * URL of the image archive - * @type {string} - */ - archiveUrl - - constructor(json) { - this.name = json.name - this.sparse = json.sparse - - if (this.name === 'system') { - this.checksum = json.alt.hash - this.fileName = `${this.name}-skip-chunks-${json.hash_raw}.img` - this.archiveUrl = json.alt.url - this.size = json.alt.size - } else { - this.checksum = json.hash - this.fileName = `${this.name}-${json.hash_raw}.img` - this.archiveUrl = json.url - this.size = json.size - } - - this.archiveFileName = this.archiveUrl.split('/').pop() - } -} - -/** - * @param {string} text - * @returns {Image[]} - */ -export function createManifest(text) { - const expectedPartitions = ['aop', 'devcfg', 'xbl', 'xbl_config', 'abl', 'boot', 'system'] - const partitions = JSON.parse(text).map((image) => new Image(image)) - - // Sort into consistent order - partitions.sort((a, b) => expectedPartitions.indexOf(a.name) - expectedPartitions.indexOf(b.name)) - - // Check that all partitions are present - // TODO: should we prevent flashing if there are extra partitions? - const missingPartitions = expectedPartitions.filter((name) => !partitions.some((image) => image.name === name)) - if (missingPartitions.length > 0) { - throw new Error(`Manifest is missing partitions: ${missingPartitions.join(', ')}`) - } - - return partitions -} - -/** - * @param {string} url - * @returns {Promise} - */ -export function getManifest(url) { - return fetch(url) - .then((response) => response.text()) - .then(createManifest) -} diff --git a/src/utils/manifest.ts b/src/utils/manifest.ts new file mode 100644 index 00000000..192f9815 --- /dev/null +++ b/src/utils/manifest.ts @@ -0,0 +1,112 @@ +/** + * Represents a partition image defined in the AGNOS manifest. + * + * Image archives can be retrieved from {@link archiveUrl}. + */ +export class Image { + /** + * Partition name + * @type {string} + */ + name: string; + + /** + * SHA-256 checksum of the image, encoded as a hex string + * @type {string} + */ + checksum: string; + /** + * Size of the unpacked image in bytes + * @type {number} + */ + size: number; + /** + * Whether the image is sparse + * @type {boolean} + */ + sparse: boolean; + + /** + * Name of the image file + * @type {string} + */ + fileName: string; + + /** + * Name of the image archive file + * @type {string} + */ + archiveFileName: string; + /** + * URL of the image archive + * @type {string} + */ + archiveUrl: string; + + constructor(json: Record) { + this.name = json.name; + this.sparse = json.sparse; + + if (this.name === "system") { + this.checksum = json.alt.hash; + this.fileName = `${this.name}-skip-chunks-${json.hash_raw}.img`; + this.archiveUrl = json.alt.url; + this.size = json.alt.size; + } else { + this.checksum = json.hash; + this.fileName = `${this.name}-${json.hash_raw}.img`; + this.archiveUrl = json.url; + this.size = json.size; + } + + this.archiveFileName = this.archiveUrl.split("/").pop()!; + } +} + +/** + * @param {string} text + * @returns {Image[]} + */ +export function createManifest(text: string) { + const expectedPartitions = [ + "aop", + "devcfg", + "xbl", + "xbl_config", + "abl", + "boot", + "system", + ]; + const partitions: Image[] = JSON.parse(text).map( + (image: any) => new Image(image), + ); + + // Sort into consistent order + partitions.sort( + (a, b) => + expectedPartitions.indexOf(a.name) - expectedPartitions.indexOf(b.name), + ); + + // Check that all partitions are present + // TODO: should we prevent flashing if there are extra partitions? + const missingPartitions = expectedPartitions.filter( + (name) => !partitions.some((image) => image.name === name), + ); + if (missingPartitions.length > 0) { + throw new Error( + `Manifest is missing partitions: ${missingPartitions.join(", ")}`, + ); + } + + return partitions; +} + +/** + * @param {string} url + * @returns {Promise} + */ +export function getManifest(url: string | URL) { + return fetch(url) + .then((response) => response.text()) + .then(createManifest); +} diff --git a/src/utils/progress.js b/src/utils/progress.js deleted file mode 100644 index ef209eed..00000000 --- a/src/utils/progress.js +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Create a set of callbacks that can be used to track progress of a multistep process. - * - * @param {(number[]|number)} steps - * @param {progressCallback} onProgress - * @returns {(progressCallback)[]} - */ -export function createSteps(steps, onProgress) { - const stepWeights = typeof steps === 'number' ? Array(steps).fill(1) : steps - - const progressParts = Array(stepWeights.length).fill(0) - const totalSize = stepWeights.reduce((total, weight) => total + weight, 0) - - function updateProgress() { - const weightedAverage = stepWeights.reduce((acc, weight, idx) => { - return acc + progressParts[idx] * weight - }, 0) - onProgress(weightedAverage / totalSize) - } - - return stepWeights.map((weight, idx) => (progress) => { - if (progressParts[idx] !== progress) { - progressParts[idx] = progress - updateProgress() - } - }) -} - -/** - * Iterate over a list of steps while reporting progress. - * @template T - * @param {(number[]|T[])} steps - * @param {progressCallback} onProgress - * @returns {([T, progressCallback])[]} - */ -export function withProgress(steps, onProgress) { - const callbacks = createSteps( - steps.map(step => typeof step === 'number' ? step : step.size || step.length || 1), - onProgress, - ) - return steps.map((step, idx) => [step, callbacks[idx]]) -} diff --git a/src/utils/progress.ts b/src/utils/progress.ts new file mode 100644 index 00000000..7133b158 --- /dev/null +++ b/src/utils/progress.ts @@ -0,0 +1,50 @@ +/** + * Create a set of callbacks that can be used to track progress of a multistep process. + * + * @param {(number[]|number)} steps + * @param {progressCallback} onProgress + * @returns {(progressCallback)[]} + */ +export function createSteps( + steps: number | number[], + onProgress: (val: number) => void, +) { + const stepWeights = typeof steps === "number" ? Array(steps).fill(1) : steps; + + const progressParts = Array(stepWeights.length).fill(0); + const totalSize = stepWeights.reduce((total, weight) => total + weight, 0); + + function updateProgress() { + const weightedAverage = stepWeights.reduce((acc, weight, idx) => { + return acc + progressParts[idx] * weight; + }, 0); + onProgress(weightedAverage / totalSize); + } + + return stepWeights.map((_weight, idx) => (progress: number) => { + if (progressParts[idx] !== progress) { + progressParts[idx] = progress; + updateProgress(); + } + }); +} + +/** + * Iterate over a list of steps while reporting progress. + * @template T + * @param {(number[]|T[])} steps + * @param {progressCallback} onProgress + * @returns {([T, progressCallback])[]} + */ +export function withProgress( + steps: number[] | any[], + onProgress: (val: number) => void, +) { + const callbacks = createSteps( + steps.map((step) => + typeof step === "number" ? step : step.size || step.length || 1, + ), + onProgress, + ); + return steps.map((step, idx) => [step, callbacks[idx]]); +} diff --git a/src/workers/image.worker.js b/src/workers/image.worker.js deleted file mode 100644 index 5df064cf..00000000 --- a/src/workers/image.worker.js +++ /dev/null @@ -1,185 +0,0 @@ -import * as Comlink from 'comlink' - -import jsSHA from 'jssha' -import { XzReadableStream } from 'xz-decompress' - -/** - * @typedef {import('@/utils/manifest').Image} Image - */ - -/** - * Chunk callback - * - * @callback chunkCallback - * @param {Uint8Array} chunk - * @returns {Promise} - */ - -/** - * Progress callback - * - * @callback progressCallback - * @param {number} progress - * @returns {void} - */ - -/** - * Read chunks from a readable stream reader while reporting progress - * - * @param {ReadableStreamDefaultReader} reader - * @param {number} total - * @param {chunkCallback} onChunk - * @param {progressCallback} [onProgress] - * @returns {Promise} - */ -async function readChunks(reader, total, { onChunk, onProgress = undefined }) { - let processed = 0 - while (true) { - const { done, value } = await reader.read() - if (done) break - await onChunk(value) - processed += value.length - onProgress?.(processed / total) - } -} - -let root - -const imageWorker = { - async init() { - if (root) { - console.warn('[ImageWorker] Already initialized') - return - } - - // TODO: check storage quota and report error if insufficient - root = await navigator.storage.getDirectory() - console.info('[ImageWorker] Initialized') - }, - - /** - * Download an image to persistent storage. - * - * @param {Image} image - * @param {progressCallback} [onProgress] - * @returns {Promise} - */ - async downloadImage(image, onProgress = undefined) { - const { archiveFileName, archiveUrl } = image - - let writable - try { - const fileHandle = await root.getFileHandle(archiveFileName, { create: true }) - writable = await fileHandle.createWritable() - } catch (e) { - throw `Error opening file handle: ${e}` - } - - console.debug('[ImageWorker] Downloading', archiveUrl) - const response = await fetch(archiveUrl, { mode: 'cors' }) - if (!response.ok) { - throw `Fetch failed: ${response.status} ${response.statusText}` - } - - try { - const contentLength = +response.headers.get('Content-Length') - const reader = response.body.getReader() - await readChunks(reader, contentLength, { - onChunk: async (chunk) => await writable.write(chunk), - onProgress, - }) - onProgress?.(1) - } catch (e) { - throw `Could not read response body: ${e}` - } - - try { - await writable.close() - } catch (e) { - throw `Error closing file handle: ${e}` - } - }, - - /** - * Unpack and verify a downloaded image archive. - * - * Throws an error if the checksum does not match. - * - * @param {Image} image - * @param {progressCallback} [onProgress] - * @returns {Promise} - */ - async unpackImage(image, onProgress = undefined) { - const { archiveFileName, checksum: expectedChecksum, fileName, size: imageSize } = image - - let archiveFile - try { - const archiveFileHandle = await root.getFileHandle(archiveFileName, { create: false }) - archiveFile = await archiveFileHandle.getFile() - } catch (e) { - throw `Error opening archive file handle: ${e}` - } - - let writable - try { - const fileHandle = await root.getFileHandle(fileName, { create: true }) - writable = await fileHandle.createWritable() - } catch (e) { - throw `Error opening output file handle: ${e}` - } - - const shaObj = new jsSHA('SHA-256', 'UINT8ARRAY') - let complete - try { - const reader = (new XzReadableStream(archiveFile.stream())).getReader() - - await readChunks(reader, imageSize, { - onChunk: async (chunk) => { - await writable.write(chunk) - shaObj.update(chunk) - }, - onProgress, - }) - - complete = true - onProgress?.(1) - } catch (e) { - throw `Error unpacking archive: ${e}` - } - - if (!complete) { - throw 'Decompression error: unexpected end of stream' - } - - try { - await writable.close() - } catch (e) { - throw `Error closing file handle: ${e}` - } - - const checksum = shaObj.getHash('HEX') - if (checksum !== expectedChecksum) { - throw `Checksum mismatch: got ${checksum}, expected ${expectedChecksum}` - } - }, - - /** - * Get a file handle for an image. - * @param {Image} image - * @returns {Promise} - */ - async getImage(image) { - const { fileName } = image - - let fileHandle - try { - fileHandle = await root.getFileHandle(fileName, { create: false }) - } catch (e) { - throw `Error getting file handle: ${e}` - } - - return fileHandle - }, -} - -Comlink.expose(imageWorker) diff --git a/src/workers/image.worker.ts b/src/workers/image.worker.ts new file mode 100644 index 00000000..d4a8a82b --- /dev/null +++ b/src/workers/image.worker.ts @@ -0,0 +1,177 @@ +import * as Comlink from "comlink"; + +import jsSHA from "jssha"; +import { XzReadableStream } from "xz-decompress"; +import { Image as ManifestImage } from "../utils/manifest"; + +type ChunkCallbackFnType = (chunk: Uint8Array) => Promise; + +type ProgressCallbackFnType = (progress: number) => Promise; + +type CallbackType = { + onChunk: ChunkCallbackFnType; + onProgress?: ProgressCallbackFnType; +}; + +/** + * Read chunks from a readable stream reader while reporting progress + */ +async function readChunks( + reader: ReadableStreamDefaultReader, + total: number, + { onChunk, onProgress = undefined }: CallbackType, +) { + let processed = 0; + while (true) { + const { done, value } = await reader.read(); + if (done) break; + await onChunk(value); + processed += value.length; + onProgress?.(processed / total); + } +} + +let root: FileSystemDirectoryHandle; + +const imageWorker = { + async init() { + if (root) { + console.warn("[ImageWorker] Already initialized"); + return; + } + + // TODO: check storage quota and report error if insufficient + root = await navigator.storage.getDirectory(); + console.info("[ImageWorker] Initialized"); + }, + + /** + * Download an image to persistent storage. + */ + async downloadImage( + image: ManifestImage, + onProgress?: ProgressCallbackFnType, + ) { + const { archiveFileName, archiveUrl } = image; + + let writable; + try { + const fileHandle = await root.getFileHandle(archiveFileName, { + create: true, + }); + writable = await fileHandle.createWritable(); + } catch (e) { + throw `Error opening file handle: ${e}`; + } + + console.debug("[ImageWorker] Downloading", archiveUrl); + const response = await fetch(archiveUrl, { mode: "cors" }); + if (!response.ok) { + throw `Fetch failed: ${response.status} ${response.statusText}`; + } + + try { + const contentLength = +response.headers.get("Content-Length")!; + const reader = response.body!.getReader(); + await readChunks(reader, contentLength, { + onChunk: async (chunk) => await writable.write(chunk), + onProgress, + }); + onProgress?.(1); + } catch (e) { + throw `Could not read response body: ${e}`; + } + + try { + await writable.close(); + } catch (e) { + throw `Error closing file handle: ${e}`; + } + }, + + /** + * Unpack and verify a downloaded image archive. + * + * Throws an error if the checksum does not match. + */ + async unpackImage(image: ManifestImage, onProgress?: ProgressCallbackFnType) { + const { + archiveFileName, + checksum: expectedChecksum, + fileName, + size: imageSize, + } = image; + + let archiveFile; + try { + const archiveFileHandle = await root.getFileHandle(archiveFileName, { + create: false, + }); + archiveFile = await archiveFileHandle.getFile(); + } catch (e) { + throw `Error opening archive file handle: ${e}`; + } + + let writable; + try { + const fileHandle = await root.getFileHandle(fileName, { create: true }); + writable = await fileHandle.createWritable(); + } catch (e) { + throw `Error opening output file handle: ${e}`; + } + + const shaObj = new jsSHA("SHA-256", "UINT8ARRAY"); + let complete; + try { + const reader = new XzReadableStream(archiveFile.stream()).getReader(); + + await readChunks(reader, imageSize, { + onChunk: async (chunk) => { + await writable.write(chunk); + shaObj.update(chunk); + }, + onProgress, + }); + + complete = true; + onProgress?.(1); + } catch (e) { + throw `Error unpacking archive: ${e}`; + } + + if (!complete) { + throw "Decompression error: unexpected end of stream"; + } + + try { + await writable.close(); + } catch (e) { + throw `Error closing file handle: ${e}`; + } + + const checksum = shaObj.getHash("HEX"); + if (checksum !== expectedChecksum) { + throw `Checksum mismatch: got ${checksum}, expected ${expectedChecksum}`; + } + }, + + /** + * Get a file handle for an image. + */ + async getImage(image: ManifestImage) { + const { fileName } = image; + + let fileHandle; + try { + fileHandle = await root.getFileHandle(fileName, { create: false }); + } catch (e) { + throw `Error getting file handle: ${e}`; + } + + return fileHandle; + }, +}; + +export type ImageWorkerType = typeof imageWorker; + +Comlink.expose(imageWorker); diff --git a/tailwind.config.js b/tailwind.config.js index f1f20e20..64a450c1 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,23 +1,18 @@ /** @type {import('tailwindcss').Config} */ export default { - content: [ - './index.html', - './src/**/*.{js,jsx}', - ], + content: ["./index.html", "./src/**/*.{ts,js}"], theme: { extend: { backgroundImage: { - 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', - 'gradient-conic': - 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', + "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", + "gradient-conic": + "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", }, fontFamily: { - sans: ['Inter Variable', 'sans-serif'], - monospace: ['JetBrains Mono Variable', 'monospace'], + sans: ["Inter Variable", "sans-serif"], + monospace: ["JetBrains Mono Variable", "monospace"], }, }, }, - plugins: [ - require('@tailwindcss/typography'), - ], -} + plugins: [require("@tailwindcss/typography")], +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..1b8364c2 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "module": "ESNext", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "skipLibCheck": true, + "allowJs": true, + "esModuleInterop": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src"] +} diff --git a/vite.config.js b/vite.config.js deleted file mode 100644 index ba64cd9d..00000000 --- a/vite.config.js +++ /dev/null @@ -1,18 +0,0 @@ -import { fileURLToPath, URL } from 'node:url'; -import { defineConfig } from 'vite' -import react from '@vitejs/plugin-react' - -// https://vitejs.dev/config/ -export default defineConfig({ - plugins: [react()], - resolve: { - alias: [ - { find: '@', replacement: fileURLToPath(new URL('./src', import.meta.url)) }, - ], - }, - test: { - globals: true, - environment: 'jsdom', - setupFiles: './src/test/setup.js', - }, -}) diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 00000000..026a016a --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vite"; + +export default defineConfig({ + build: { + target: "esnext", + }, + worker: { + format: "es", + }, +}); From 8fd7ade3c304e8ddb58375b1fce0a115cc273dfb Mon Sep 17 00:00:00 2001 From: Miraj Shah Date: Sat, 20 Jul 2024 18:12:01 +0530 Subject: [PATCH 2/4] refactor the how the render functions called --- src/main.ts | 101 +++++++++++++++++++++--------------------- src/utils/fastboot.ts | 6 +++ 2 files changed, 56 insertions(+), 51 deletions(-) diff --git a/src/main.ts b/src/main.ts index 4a59cd85..e094530b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -11,50 +11,49 @@ import "./index.css"; const fb = new FastbootManager(); fb.init(); -const linearProgressCtnEl = document.getElementById("linear-progress-ctn")!; -const linearProgressEl = document.getElementById("linear-progress")!; -const titleEl = document.getElementById("title")!; -const iconCtnEl = document.getElementById("icon-ctn")!; - -fb.addEventListener("step", ((event: CustomEvent) => { - const state = event.detail; - updateIcon(iconCtnEl, state); - updateTitle(titleEl, state); -}) as EventListener); - -fb.addEventListener("progress", (( - event: CustomEvent, -) => { - const state = event.detail; - console.log("progress", state); - updateLinearProgress(linearProgressEl, state); -}) as EventListener); - -fb.addEventListener("message", (( - event: CustomEvent, -) => { - const state = event.detail; - updateTitle(titleEl, state); -}) as EventListener); - -fb.addEventListener("error", (( - event: CustomEvent, -) => { - const state = event.detail; - updateIcon(iconCtnEl, state); -}) as EventListener); - -function updateLinearProgress( - element: HTMLElement, - state: FastbootManagerStateType, -) { +function setupProgressIndicatorView(initialState: FastbootManagerStateType) { + renderProgressIndicator(initialState); + fb.on("progress", renderProgressIndicator); +} + +function setupStatusView(initialState: FastbootManagerStateType) { + renderStatusView(initialState); + fb.on("message", renderStatusView); + fb.on("step", renderStatusView); + fb.on("error", renderStatusView); +} + +function setupIconView(initialState: FastbootManagerStateType) { + renderIconView(initialState); + fb.on("step", renderIconView); + fb.on("error", renderIconView); +} + +function setupRetryButtonView(initialState: FastbootManagerStateType) { + renderRetryButtonView(initialState); + fb.on("error", renderRetryButtonView); +} + +function setupDeviceStatusView(initialState: FastbootManagerStateType) { + renderDeviceStateView(initialState); + fb.on("connected", renderDeviceStateView); + fb.on("serial", renderDeviceStateView); +} + +function renderProgressIndicator(state: FastbootManagerStateType) { + const el = document.getElementById("linear-progress")!; + const ctnEl = document.getElementById("linear-progress-ctn")!; + const { progress, step } = state; - element.style.transform = `translateX(${progress - 100}%)`; - element.className = `absolute top-0 bottom-0 left-0 w-full transition-all ${fbSteps[step].bgColor}`; - linearProgressCtnEl.style.opacity = progress === -1 ? "0" : "1"; + el.style.transform = `translateX(${progress - 100}%)`; + el.className = `absolute top-0 bottom-0 left-0 w-full transition-all ${fbSteps[step].bgColor}`; + ctnEl.style.opacity = progress === -1 ? "0" : "1"; } -function updateTitle(el: HTMLElement, state: FastbootManagerStateType) { +function renderStatusView(state: FastbootManagerStateType) { + const el = document.getElementById("title")!; + const subtitleEl = document.getElementById("subtitle")!; + const { message, error, progress, step } = state; let title; if (message && !error) { @@ -66,23 +65,23 @@ function updateTitle(el: HTMLElement, state: FastbootManagerStateType) { title = fbSteps[step].status; } el.innerHTML = title; - const subtitleEl = document.getElementById("subtitle")!; subtitleEl.innerHTML = fbSteps[step].description ?? ""; } -function updateIcon(el: HTMLElement, state: FastbootManagerStateType) { +function renderIconView(state: FastbootManagerStateType) { + const el = document.getElementById("icon-ctn")!; + const img = el.getElementsByTagName("img")[0]; const { step, error, onContinue } = state; el.className = `p-8 rounded-full ${fbSteps[step].bgColor}`; if (onContinue) { el.style.cursor = "pointer"; el.addEventListener("click", onContinue); } - const img = el.getElementsByTagName("img")[0]; img.src = fbSteps[step].icon; img.className = `${!error && step !== FastbootStep.DONE ? "animate-pulse" : ""}`; } -function updateRetryButton(state: FastbootManagerStateType) { +function renderRetryButtonView(state: FastbootManagerStateType) { const { error } = state; if (error !== FastbootError.NONE) { const el = document.getElementById("subtitle")!; @@ -108,7 +107,7 @@ function updateRetryButton(state: FastbootManagerStateType) { } } -function updateDeviceState(state: FastbootManagerStateType) { +function renderDeviceStateView(state: FastbootManagerStateType) { const { serial, connected } = state; if (!connected) { const deviceStateEl = document.getElementById("device-state"); @@ -214,8 +213,8 @@ const fbSteps: Record< }, }; -updateIcon(iconCtnEl, fb.state); -updateTitle(titleEl, fb.state); -updateLinearProgress(linearProgressEl, fb.state); -updateRetryButton(fb.state); -updateDeviceState(fb.state); +setupProgressIndicatorView(fb.state); +setupIconView(fb.state); +setupStatusView(fb.state); +setupDeviceStatusView(fb.state); +setupRetryButtonView(fb.state); diff --git a/src/utils/fastboot.ts b/src/utils/fastboot.ts index 96ccd7aa..30eba7f4 100644 --- a/src/utils/fastboot.ts +++ b/src/utils/fastboot.ts @@ -33,6 +33,12 @@ export class FastbootManager extends EventTarget { this.manifest = null; } + on(type: string, listener: (data: FastbootManagerStateType) => void) { + this.addEventListener(type, (( + event: CustomEvent, + ) => listener(event.detail)) as EventListener); + } + private setStep(step: FastbootStep) { this.state.step = step; this.dispatchEvent(new CustomEvent("step", { detail: this.state })); From 79be48af41d7c3508d3827550593abbc87747bf2 Mon Sep 17 00:00:00 2001 From: Miraj Shah Date: Sat, 20 Jul 2024 20:42:45 +0530 Subject: [PATCH 3/4] linting and tests --- .eslintrc.json | 33 ++------------ bun.lockb | Bin 233456 -> 127160 bytes package.json | 8 +++- src/utils/fastboot.ts | 5 +- src/utils/manifest.test.js | 75 ------------------------------ src/utils/manifest.test.ts | 91 +++++++++++++++++++++++++++++++++++++ src/utils/manifest.ts | 2 + src/utils/progress.ts | 1 + 8 files changed, 109 insertions(+), 106 deletions(-) delete mode 100644 src/utils/manifest.test.js create mode 100644 src/utils/manifest.test.ts diff --git a/.eslintrc.json b/.eslintrc.json index dc9027bf..57819752 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -1,33 +1,18 @@ { "root": true, + "parser": "@typescript-eslint/parser", "env": { "browser": true, "es2020": true, "node": true }, - "extends": [ - "eslint:recommended", - "plugin:react/recommended", - "plugin:react/jsx-runtime", - "plugin:react-hooks/recommended" - ], - "ignorePatterns": [ - "node_modules", - "dist", - ".eslintrc.cjs" - ], + "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended"], + "ignorePatterns": ["node_modules", "dist", ".eslintrc.cjs"], + "plugins": ["@typescript-eslint"], "parserOptions": { "ecmaVersion": "latest", "sourceType": "module" }, - "settings": { - "react": { - "version": "detect" - } - }, - "plugins": [ - "react-refresh" - ], "rules": { "no-constant-condition": [ "error", @@ -35,14 +20,6 @@ "checkLoops": false } ], - "no-inner-declarations": "off", - "react/jsx-no-target-blank": "off", - "react/prop-types": "off", - "react-refresh/only-export-components": [ - "warn", - { - "allowConstantExport": true - } - ] + "no-inner-declarations": "off" } } diff --git a/bun.lockb b/bun.lockb index c6d81dfa577f76429b19ad261f3a0dbb04f49b79..f7d966a235ae85109324277fe48ed54e1c4c32d9 100755 GIT binary patch delta 34416 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 L!U>a93CjNq7CL+j literal 233456 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 z zEa$?b{i&jM=3rM(geqd#&Yj$%v-hEf=NcBreZBBXf7#>BA9&gXY|4SCccl?pZ8oC= zi&VbiV!Y=fNKjA2LYK%HH{O(m9e~fgI1VP|Dw^ zP#t7}RXssK;B?5hQ+aw(uzHS#JNnDjGuQ*C=NMJto(OV1kORwjX9#X0xneZud^dv+ z#}JFI+&HZqpd9{32AYNpM$MU+^!T$vcI8+kX9(34MigD9_8NSIBRS6MYi)%V86hn* z0z&X=MSouHu%tSb@GOtO#)rs!SD+WQ6z!#sqAo?7#KHx8ON^N6k0_ds#1LLWz7KGSrgROCe=WvfS2j;Ef+q7=C~GebEHAE z|32Q7oGC&z1h@}TEV-Tpkr=p{9q3i$k9gWilPu1CC@R??-7NqyLf*F2?#IBpMEI$z z6g=?6yJAiwnivMDw$voN^2=!y?}&*<+*g(>e1=kQmm_~~$(RJAar$$=6sv>pGldL! zBzii3OOi<=!H_f(l_2l$!n}N}ISKbNRPvs_4#SrTA(X?7nnJhcsW&#?%oqTR>f3xFoGlj~-w))?%k;LG!P$HnAG z@D;bSUkd3SHU=ZP0N_KhEIET+XdrI#3Qywh0NkZbx5l{=aofC^_B;kJY(v#*lZ<$( zQrS?znsPIwAo@D3F@x!XVJevyr{mRwg34<>Co%Y(34GLZ&MSSC!E+t-_Z&aaU{_TJ ztm=un{vw{%_IE>9y$Dj&6LrMyLw9ECHn7yeeTq279v34vOOkzO~$wbJV)m41>sbOuh$1^bJ1`h56zjdu<*MF)YnsR$nhMD zVVly&W5O{EuXOT#Gm2M$pFd>v-|FIQX}BK$ipbz^ypKK{SL0vNA^0nukN4d|iVrGk zClYf5cn0TF%7wnmtz4r@jSgun=N96Pz<9}`1JAKzRP5sZNAWgCzVpz9DW%cHSL#}m zQXTl8_t~UkPBR|nRz3$2A{JcG$A~d1Uao76vxub~xicDHz`%=_Qn7@9D4&F3i^ZLp zBFkaMavEPui%kH^XjbNtW_4Pj1~=sKR+b5y!DT2D9d$}fYI1@pg^@JX4EY%0nQho* zD5W#AB)nnGl?lg7z?d>6Mr@8tPqec;#TZQ7>XXe$W~uVX1aU(-8>4dj!>HVTu{gM8 z6pGPKQa{eh((!U(d#Vd>!mx9e=EKb&h#!^rK-a-h<+`qq9DSDuS!FBXg9-I%Le=?% zMG+_EOP0i7F;tzeC~q_sAEfb70wph1DDE4|p^V$3vG*45XvIrfjoKz5L_L8fO_hNoZoJu2F-EjGV@znIcm=mJ z5sMFr)<)@eN1T!46!Bn$*t8)Ur_GWk-o0$G`kH2CzZBVLPZC2fe~TK(jhlJ-Bf^)z z&Q(e5 z!ZiNpp;_{TfGp?mO6g8`bvGWZz^bbcRXo=~jOSRyVSnC$RaeX&XcuvSai0GL&>S5S z*mI3;*6=h zi6M`~7+2)X2aE8^a*qDQ2oMjfVCmh5EX`Grr#X8dN6=&kZq1pP3;U}ii!YZCuLe*+(hVizv&( z0pIt(bc=zM!xRrbAp+Qrrl$dHe-2&J>GnNvS^fz6m$cTna_9 zC#g}uhY9#DSMZ8gx;{u>%Au2Z09rW^dH5?fhEy1Ly4X_PhoaN)*TC@o@l738mPwK#Db8eh!e{9GKt!1AUfA7(J|Md!ZO z9o3732sel5NBmWj#fh;jT~|54YMXqFQhB~-nA6wmK;_&_JEInoIOjKNQ(-*+FQE10 z2pOJZQ3%m8{$F!vy4*&N1K@ojd7Vs>ncAw|%9G5LY|FM%w&b7Bl{Nd*z+hB57V*Z!^WPD zL6OW=535$HJ8t8n^p`eEK^+%SOLIN`NgtDuUrKVZeX@j9?e&0_bw*0B6O^414XAq5 zb`$6nTvg}o51C8&%gV%gsLotEK6P?vwIr6vru2EsO^k5(pm;=|0)7iXP;BDUD*_C? zWESNZ-*RQZy8MypB^DPNy-cZ~0(lt~`GI|yE+`F@ACa5;;6X1Lfl*q8X4T}Fw0hb? z?0T|JW<-T+a_r|-ZWpDXvMgFM7nOY=ar@s8hT&+?%)dsS`XXY~l1eS$;E1c4Hriy z1tQdAqTZ&6jlSa~TIR>y(+in-pqc)n4AA3I*42&37b-5;pturMHLpi_Og6rCidlL4 zXSH@_?LNMkuLXSp`kHI}f1R(sibC-4P*A2e3hZHj|3yjoVlAP6zvb^t1_p8p0~w1^qEgZR1LPFeGIje0vbtk&xKlLz)9&yvw5ROkrl3wLMY`DN<>^}q z-kaZ@Gt#qpDb25u^SoQo6bZZp$-JFm(`rZC#W_ce=M9?_S0uMR9A9KZB|n-!+Jp62 z1AH$Te6a$%Y6ls_Ae*JBIH6og_gGHc;p5tnj`~3h^s-YxFBxjxsAhn#KM{ek{{ftG z=?iV!0B7B5_Q=pIO{vqi@##g*I0s0JTu@iUv{9l)t|njLt2h|0O))blpDyZ!>MReA zCPlGCofDX}ei()n6L!enAI|Dh?T?^sZr)KW+Kiy-rjRHW4i2qvciTUCrdN+&MM@BV z_pJ6O9qoVeETAJR;u&j`c)aH^1XU`#DB6+KniBwd8yk?gATS18QTy>se zZxf~o1tUh&)p8Uk9)HT(5tZpt&)1m~4*vecnSiyLcVV$iB!G`CvM@fZa6E55@5ihRg3QARbmaW zRa`WEAqJ}JB9hhJBg(aCDgd@4iy5BIhqWzH$$N&aN?N6Ug|4-)u@TGuh zD9RK)yNDK?EbW3IM^~I%bfqqm*c!-bE$yZ& z#;DI0kZEmclUCu;Zru)}>FGYP~bNdq$5vHg7v=9;med$8Ygb zx*K5ul+3YY3QoG|U<`;R%rx4-=g0r|kAb$S7)4m7-57tffzxwF9osGlPGgsO_$c8@e2Efq2Ct$SAXuu;cxpggyYJ z^gn1tbiwmI?c4qdAL%-@2c^t2Q^Gt^>!A~*$QP&g18$w&=Mf2LO&_k@0gG%R2(r5-NYpYB; z(JGLO%CMBHu#pr4EX5ey1?7R>U{X0NXq@xjP|VYni=3>FXL?BQwXz}YeaVe8Ziu=w z8NNDASsT%pEOS7Rg22VL8tpWgj+DW(^eK;pLCxmC=@7e%Iek>zQ8#LF%K~71WZ?@~ zeO|wSVj5LX={xkMPvHG*8#NJcwp>@iw(WlZ*U&#lnN4sPQ1))RnZw+?9)IG5j+24Y z`FCl@U#_pOKYjW31@AliH{4utEogTyXV}am$W+;vSmd~=$q5ZjidywXjZhQ~h9bu9 z)!!Z-FuSY-;AIr1dS$Yag zRB@eB!k3eJtO8RnnM!e))85Y>R7M}&5nmBcZ>X_hIo%hGWmLP5Hk&)d7Pp1TH@1Oc zxp6pg*uWbTDu0Wdh>O$v6}j}0BY#6({cZp8hu}9nj9oSmkLD)AELP}1qsFP=s7Mu; zVdVBeBbJD2@q~{Kyp|g=;3;Ba=IHt)ub%?cb(mJQI{CbNeqnChHL0b36((S>#T4uQ9dVa29VDR)>e`2$QA1jfku#GvK_xF3O9*!HeR&;{~ z&Z!54%rjG^dE%-CHzp0-UZ)=6!T{_#qIJD4#?-scoQ%IpHqJ&n*QSNNqZBRdDPR-x z)Uj~nL|U7H!vaP~2wV04sUv4frX=uv3pYNAl)+4)bAV78H-kf}E#08IIgm^+ z6sPW2wbv_4zHgsj=(OVanP~&@MSOov5UfWmsn2QRn|M&uA@|TR8Bj|z4zAf+o$3sJ z0eYG+>(41RO%py&eSSa*F1}1Q_A;DYkkp0rdULZoZ?8TNDlS&ZU#YVGGMZuw7Ah#4 zDquXMt7IxF=zeoTmEA4%mkjGP!GS0R7kdM*SnUxg#CS5@L8x0H?f&$fx&HKcM7+b3 zEDqg8$54ONtKZ^AO71T5w%-%}3xi#wsj92Hb!dWOWzxEW;YOOJ&Rv z@Tj+_Vw{gDt5h(B3ycE{^&Jhd8II)2`Vs~u)(Kpn@tZ`0QswAcY-NRB59+B*v>0A; zvqg3hQD0g1z$4qmXbCm%OFsE%Yd#^Gq{--0@@X*OgJE#XXW*T3c7j%_LwzuPZWB&3{nx4fOJNyVyYdy8-8kqBC2Je5$Hq$=5(D(cEL-pcVw zW?fSQYr#mFR%7mn9GULUM=3*`%_2ZLBFV%ZUpe5aQD-UHT!Rcc)8wv^vZpXpj|`l# zsQQknB1b3SZJK{wa3 zOf}k~iQD<-fYVTG_V1Js;Gk~-dW-^-JOpgp+3)$CJ5vL+AkMjFIH4A7O{)4c<(RQ zz)WA6x)Q6cHl{e{l{9co;ms#`=rmkg3k(yqLaJovioR^wG7zg~dBK+f#UuvrC(#D+ zyWGSOAHlqT@yf#jR-a6nY!B6H+m~s7QgQ|+Pu88!FQ}D=JI+{p z(^c+XxgYlWb9ZBb{piV6Jv@Y-lln2@Z$Z$Y78BTE`*M0b%GU4X!OIY!h!tf$2cR0b z3^=*z(cg8T`d+mh+fw8dE$2&#lplrWK@zBjNFqQ}VSjaUYB66bx0{q5ci zCh2#3&RPp6r?!! zI6Np#c3@Hto&t1ctK!Vj@scjS-JUi)t3c6Hn6hJ+M)T1K3K^z8HnsRrA7S&WfF)=t zIkXqRbunikfLI`_bg`t7!x7t>8apg02Ub;$)ne-TAUK5}tN8my%u+*mn(to2`w=0sB zX{(c3ctqrWFW%UdTWUvjBqJ`%a)c29?TX2rs`CDyc~$ZFo+GsF{$R$*|M|C4F3XX5 w_a1slNDmG+Z|6e5!WZ*l7%`?t7&v>$+Pxy#tF3s(HSq#GZfWt4zv;jK1M+kU*#H0l diff --git a/package.json b/package.json index 729e4bd5..c2bfc3b6 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,9 @@ "scripts": { "dev": "vite", "build": "tsc && vite build", - "preview": "vite preview" + "preview": "vite preview", + "lint": "eslint . --ext ts,js --report-unused-disable-directives", + "test": "vitest" }, "engines": { "node": ">=20.11.0" @@ -22,10 +24,14 @@ "devDependencies": { "@tailwindcss/typography": "^0.5.13", "@types/node": "^20.14.11", + "@typescript-eslint/eslint-plugin": "^7.16.1", + "@typescript-eslint/parser": "^7.16.1", "autoprefixer": "^10.4.19", + "eslint": "^8.57.0", "postcss": "^8.4.39", "tailwindcss": "^3.4.6", "typescript": "^5.2.2", + "vitest": "^1.6.0", "vite": "^5.3.4" } } diff --git a/src/utils/fastboot.ts b/src/utils/fastboot.ts index 30eba7f4..b3b9a4f4 100644 --- a/src/utils/fastboot.ts +++ b/src/utils/fastboot.ts @@ -70,6 +70,7 @@ export class FastbootManager extends EventTarget { } init() { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore Check that the browser supports WebUSB if (typeof navigator.usb === "undefined") { console.error("[fastboot] WebUSB not supported"); @@ -261,7 +262,7 @@ export type FastbootManagerStateType = { error: FastbootError; connected: boolean; serial: string | null; - onContinue?: () => any; + onContinue?: () => void; }; export enum FastbootStep { @@ -288,7 +289,7 @@ export enum FastbootError { REQUIREMENTS_NOT_MET, } -function isRecognizedDevice(deviceInfo: Record) { +function isRecognizedDevice(deviceInfo: Record) { // check some variables are as expected for a comma three const { kernel, diff --git a/src/utils/manifest.test.js b/src/utils/manifest.test.js deleted file mode 100644 index cabcac0e..00000000 --- a/src/utils/manifest.test.js +++ /dev/null @@ -1,75 +0,0 @@ -import { describe, expect, test, vi } from 'vitest' - -import * as Comlink from 'comlink' - -import config from '../config' -import { getManifest } from './manifest' - -async function getImageWorker() { - let imageWorker - - vi.mock('comlink') - vi.mocked(Comlink.expose).mockImplementation(worker => { - imageWorker = worker - imageWorker.init() - }) - - await import('./../workers/image.worker') - - return imageWorker -} - -for (const [branch, manifestUrl] of Object.entries(config.manifests)) { - describe(`${branch} manifest`, async () => { - const imageWorkerFileHandler = { - getFile: vi.fn(), - createWritable: vi.fn().mockImplementation(() => ({ - write: vi.fn(), - close: vi.fn(), - })), - } - - globalThis.navigator = { - storage: { - getDirectory: () => ({ - getFileHandle: () => imageWorkerFileHandler, - }) - } - } - - const imageWorker = await getImageWorker() - - const images = await getManifest(manifestUrl) - - // Check all images are present - expect(images.length).toBe(7) - - for (const image of images) { - describe(`${image.name} image`, async () => { - test('xz archive', () => { - expect(image.archiveFileName, 'archive to be in xz format').toContain('.xz') - expect(image.archiveUrl, 'archive url to be in xz format').toContain('.xz') - }) - - if (image.name === 'system') { - test('alt image', () => { - expect(image.sparse, 'system image to be sparse').toBe(true) - expect(image.fileName, 'system image to be skip chunks').toContain('-skip-chunks-') - expect(image.archiveUrl, 'system image to point to skip chunks').toContain('-skip-chunks-') - }) - } - - test('image and checksum', async () => { - imageWorkerFileHandler.getFile.mockImplementation(async () => { - const response = await fetch(image.archiveUrl) - expect(response.ok, 'to be uploaded').toBe(true) - - return response.blob() - }) - - await imageWorker.unpackImage(image) - }, 8 * 60 * 1000) - }) - } - }) -} diff --git a/src/utils/manifest.test.ts b/src/utils/manifest.test.ts new file mode 100644 index 00000000..a4ef80ae --- /dev/null +++ b/src/utils/manifest.test.ts @@ -0,0 +1,91 @@ +import { describe, expect, test, vi } from "vitest"; + +import * as Comlink from "comlink"; + +import config from "../config"; +import { getManifest } from "./manifest"; +import { ImageWorkerType } from "./../workers/image.worker"; + +async function getImageWorker() { + let imageWorker: ImageWorkerType | undefined; + + // // vi.mock("comlink"); + vi.spyOn(Comlink, "expose").mockImplementation((worker) => { + imageWorker = worker; + imageWorker!.init(); + }); + + await import("./../workers/image.worker"); + + return imageWorker; +} + +for (const [branch, manifestUrl] of Object.entries(config.manifests)) { + describe(`${branch} manifest`, async () => { + const imageWorkerFileHandler = { + getFile: vi.fn(), + createWritable: vi.fn().mockImplementation(() => ({ + write: vi.fn(), + close: vi.fn(), + })), + }; + + globalThis.navigator = { + storage: { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + getDirectory: async () => ({ + getFileHandle: () => imageWorkerFileHandler, + }), + }, + }; + + const imageWorker = await getImageWorker(); + + const images = await getManifest(manifestUrl); + + // Check all images are present + expect(images.length).toBe(7); + + for (const image of images) { + describe(`${image.name} image`, () => { + test("xz archive", () => { + expect(image.archiveFileName, "archive to be in xz format").toContain( + ".xz", + ); + expect(image.archiveUrl, "archive url to be in xz format").toContain( + ".xz", + ); + }); + + if (image.name === "system") { + test("alt image", () => { + expect(image.sparse, "system image to be sparse").toBe(true); + expect(image.fileName, "system image to be skip chunks").toContain( + "-skip-chunks-", + ); + expect( + image.archiveUrl, + "system image to point to skip chunks", + ).toContain("-skip-chunks-"); + }); + } + + test( + "image and checksum", + async () => { + imageWorkerFileHandler.getFile.mockImplementation(async () => { + const response = await fetch(image.archiveUrl); + expect(response.ok, "to be uploaded").toBe(true); + + return response.blob(); + }); + + await imageWorker!.unpackImage(image); + }, + 8 * 60 * 1000, + ); + }); + } + }); +} diff --git a/src/utils/manifest.ts b/src/utils/manifest.ts index 192f9815..0bda302e 100644 --- a/src/utils/manifest.ts +++ b/src/utils/manifest.ts @@ -43,6 +43,7 @@ export class Image { */ archiveUrl: string; + // eslint-disable-next-line @typescript-eslint/no-explicit-any constructor(json: Record) { this.name = json.name; this.sparse = json.sparse; @@ -78,6 +79,7 @@ export function createManifest(text: string) { "system", ]; const partitions: Image[] = JSON.parse(text).map( + // eslint-disable-next-line @typescript-eslint/no-explicit-any (image: any) => new Image(image), ); diff --git a/src/utils/progress.ts b/src/utils/progress.ts index 7133b158..361da3c1 100644 --- a/src/utils/progress.ts +++ b/src/utils/progress.ts @@ -37,6 +37,7 @@ export function createSteps( * @returns {([T, progressCallback])[]} */ export function withProgress( + // eslint-disable-next-line @typescript-eslint/no-explicit-any steps: number[] | any[], onProgress: (val: number) => void, ) { From 66aa8ae1978bbffd03aaf8e9f3532a55d4817567 Mon Sep 17 00:00:00 2001 From: Miraj Shah Date: Sat, 20 Jul 2024 20:57:08 +0530 Subject: [PATCH 4/4] trying older test mock --- src/utils/manifest.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/manifest.test.ts b/src/utils/manifest.test.ts index a4ef80ae..e0e5d1d8 100644 --- a/src/utils/manifest.test.ts +++ b/src/utils/manifest.test.ts @@ -9,8 +9,8 @@ import { ImageWorkerType } from "./../workers/image.worker"; async function getImageWorker() { let imageWorker: ImageWorkerType | undefined; - // // vi.mock("comlink"); - vi.spyOn(Comlink, "expose").mockImplementation((worker) => { + vi.mock("comlink"); + vi.mocked(Comlink.expose).mockImplementation((worker) => { imageWorker = worker; imageWorker!.init(); });