diff --git a/.gitignore b/.gitignore index c5eab7e..9fb2df8 100644 --- a/.gitignore +++ b/.gitignore @@ -20,7 +20,7 @@ /out/ # production -/build +/dist # misc .DS_Store diff --git a/bun.lockb b/bun.lockb index c6d81df..3e0773e 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/index.html b/index.html deleted file mode 100644 index 3060b4a..0000000 --- a/index.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - flash.comma.ai - - -
- - - - diff --git a/package.json b/package.json index 9c54987..ae67038 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "scripts": { "dev": "vite", "build": "vite build", - "start": "vite preview", + "start": "vite build && vite preview", "lint": "eslint . --ext js,jsx --report-unused-disable-directives", "test": "vitest" }, @@ -38,6 +38,8 @@ "jsdom": "^22.1.0", "postcss": "^8.4.38", "tailwindcss": "^3.4.3", + "vike": "^0.4.179", + "vike-react": "^0.4.17", "vite": "^5.2.12", "vite-svg-loader": "^5.1.0", "vitest": "^1.6.0" diff --git a/src/app/favicon.ico b/public/favicon.ico similarity index 100% rename from src/app/favicon.ico rename to public/favicon.ico diff --git a/src/app/icon.png b/public/icon.png similarity index 100% rename from src/app/icon.png rename to public/icon.png diff --git a/src/app/icon.svg b/public/icon.svg similarity index 100% rename from src/app/icon.svg rename to public/icon.svg diff --git a/src/assets/cloud.svg b/src/assets/cloud.svg deleted file mode 100644 index a94044a..0000000 --- a/src/assets/cloud.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/app/Flash.jsx b/src/components/Flash.jsx similarity index 92% rename from src/app/Flash.jsx rename to src/components/Flash.jsx index d7c544d..f8babd3 100644 --- a/src/app/Flash.jsx +++ b/src/components/Flash.jsx @@ -1,10 +1,11 @@ -import { useCallback } from 'react' +'use client'; + +import { useCallback, useEffect } 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' @@ -16,11 +17,6 @@ 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', @@ -189,10 +185,6 @@ export default function Flash() { serial, } = useFastboot() - const handleContinue = useCallback(() => { - onContinue?.() - }, [onContinue]) - const handleRetry = useCallback(() => { onRetry?.() }, [onRetry]) @@ -214,18 +206,24 @@ export default function Flash() { } // 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 }) - } + useEffect(() => { + if (Step.DOWNLOADING <= step && step <= Step.ERASING) { + window.addEventListener("beforeunload", beforeUnloadListener, { capture: true }) + } else { + window.removeEventListener("beforeunload", beforeUnloadListener, { capture: true }) + } + + return () => { + window.removeEventListener("beforeunload", beforeUnloadListener, { capture: true }) + } + }, [step]) return (
+ + + + + + ); +} diff --git a/src/layouts/Layout.jsx b/src/layouts/Layout.jsx new file mode 100644 index 0000000..7febf1b --- /dev/null +++ b/src/layouts/Layout.jsx @@ -0,0 +1,9 @@ +import '../index.css' + +import React from 'react' + +export default function Layout({ children }) { + return ( + {children} + ) +} diff --git a/src/main.jsx b/src/main.jsx deleted file mode 100644 index 40fea3e..0000000 --- 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/pages/+config.js b/src/pages/+config.js new file mode 100644 index 0000000..5a49bba --- /dev/null +++ b/src/pages/+config.js @@ -0,0 +1,11 @@ +import vikeReact from 'vike-react/config' +import Head from '../layouts/Head' +import Layout from '../layouts/Layout' + +export default { + Head, + Layout, + title: "flash.comma.ai", + extends: vikeReact, +} + diff --git a/src/app/index.jsx b/src/pages/index/+Page.jsx similarity index 91% rename from src/app/index.jsx rename to src/pages/index/+Page.jsx index 54828b1..2e7fd24 100644 --- a/src/app/index.jsx +++ b/src/pages/index/+Page.jsx @@ -1,15 +1,13 @@ -import { Suspense, lazy } from 'react' +import Flash from '../../components/Flash' -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')) +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' export default function App() { - const version = import.meta.env.VITE_PUBLIC_GIT_SHA || 'dev' - console.info(`flash.comma.ai version: ${version}`); + const version = import.meta?.env?.VITE_PUBLIC_GIT_SHA ?? 'dev' + console.info(`flash.comma.ai version: ${version}`) return (
@@ -149,9 +147,7 @@ export default function App() {
- Loading...

}> - -
+
diff --git a/src/app/App.test.jsx b/src/pages/index/Page.test.jsx similarity index 73% rename from src/app/App.test.jsx rename to src/pages/index/Page.test.jsx index 206de72..13095c3 100644 --- a/src/app/App.test.jsx +++ b/src/pages/index/Page.test.jsx @@ -1,10 +1,9 @@ import { Suspense } from 'react' import { expect, test } from 'vitest' import { render, screen } from '@testing-library/react' - -import App from '.' +import Page from './+Page' test('renders without crashing', () => { - render() + render() expect(screen.getByText('flash.comma.ai')).toBeInTheDocument() }) diff --git a/src/test/mockWorker.js b/src/test/mockWorker.js new file mode 100644 index 0000000..26bd9ea --- /dev/null +++ b/src/test/mockWorker.js @@ -0,0 +1,16 @@ +class MockWorker { + constructor(stringUrl) { + this.url = stringUrl; + this.onmessage = () => {}; + } + + postMessage(msg) { + this.onmessage({ data: msg }); + } + + addEventListener() {} + removeEventListener() {} + terminate() {} +} + +export default MockWorker; diff --git a/src/test/setup.js b/src/test/setup.js index c44951a..82b22f0 100644 --- a/src/test/setup.js +++ b/src/test/setup.js @@ -1 +1,4 @@ import '@testing-library/jest-dom' +import MockWorker from './mockWorker'; + +global.Worker = MockWorker diff --git a/src/utils/fastboot.js b/src/utils/fastboot.js index a9af865..2bda782 100644 --- a/src/utils/fastboot.js +++ b/src/utils/fastboot.js @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { FastbootDevice, setDebugLevel } from 'android-fastboot' import * as Comlink from 'comlink' @@ -17,14 +17,13 @@ import { withProgress } from '@/utils/progress' setDebugLevel(2) export const Step = { - INITIALIZING: 0, - READY: 1, - CONNECTING: 2, - DOWNLOADING: 3, - UNPACKING: 4, - FLASHING: 6, - ERASING: 7, - DONE: 8, + READY: 0, + CONNECTING: 1, + DOWNLOADING: 2, + UNPACKING: 3, + FLASHING: 4, + ERASING: 5, + DONE: 6, } export const Error = { @@ -87,15 +86,15 @@ function isRecognizedDevice(deviceInfo) { } export function useFastboot() { - const [step, _setStep] = useState(Step.INITIALIZING) + const [step, setStep] = useState(Step.READY) const [message, _setMessage] = useState('') - const [progress, setProgress] = useState(0) - const [error, _setError] = useState(Error.NONE) + const [progress, setProgress] = useState(-1) + const [error, setError] = useState(Error.NONE) + const [isInitialized, setIsInitialized] = useState(false) const [connected, setConnected] = useState(false) const [serial, setSerial] = useState(null) - const [onContinue, setOnContinue] = useState(null) const [onRetry, setOnRetry] = useState(null) const imageWorker = useImageWorker() @@ -104,83 +103,87 @@ export function useFastboot() { /** @type {React.RefObject} */ const manifest = useRef(null) - function setStep(step) { - _setStep(step) - } + const initializePromise = useRef(null) function setMessage(message = '') { if (message) console.info('[fastboot]', message) _setMessage(message) } - function setError(error) { - _setError(error) - } + const initialize = useCallback(async () => { + if (isInitialized) return true - useEffect(() => { - setProgress(-1) - setMessage() + if (initializePromise.current) return initializePromise.current - if (error) return - if (!imageWorker.current) { - console.debug('[fastboot] Waiting for image worker') - return - } + initializePromise.current = (async () => { + // Check browser support + if (typeof navigator.usb === 'undefined') { + console.error('[fastboot] WebUSB not supported') + setError(Error.REQUIREMENTS_NOT_MET) + return false + } - 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 - } + if (typeof Worker === 'undefined') { + console.error('[fastboot] Web Workers not supported') + setError(Error.REQUIREMENTS_NOT_MET) + return false + } - // Check that the browser supports Web Workers - if (typeof Worker === 'undefined') { - console.error('[fastboot] Web Workers not supported') - setError(Error.REQUIREMENTS_NOT_MET) - break - } + if (typeof Storage === 'undefined') { + console.error('[fastboot] Storage API not supported') + setError(Error.REQUIREMENTS_NOT_MET) + return false + } - // Check that the browser supports Storage API - if (typeof Storage === 'undefined') { - console.error('[fastboot] Storage API not supported') - setError(Error.REQUIREMENTS_NOT_MET) - break - } + if (!imageWorker.current) { + console.debug('[fastboot] Waiting for image worker') + return false + } - // 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) + try { + await imageWorker.current?.init() + const blob = await download(config.manifests['master']) + const text = await blob.text() + manifest.current = createManifest(text) - // sanity check - if (manifest.current.length === 0) { - throw 'Manifest is empty' - } + if (manifest.current.length === 0) { + throw new Error('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 + console.debug('[fastboot] Loaded manifest', manifest.current) + setIsInitialized(true) + return true + } catch (err) { + console.error('[fastboot] Initialization error', err) + setError(Error.UNKNOWN) + return false + } finally { + initializePromise.current = null } + })() - case Step.READY: { - // wait for user interaction (we can't use WebUSB without user event) - setOnContinue(() => () => { - setOnContinue(null) - setStep(Step.CONNECTING) - }) - break - } + return initializePromise.current + }, [imageWorker, isInitialized]) + + useEffect(() => { + initialize() + }, [initialize]) + + // wait for user interaction (we can't use WebUSB without user event) + const handleContinue = useCallback(async () => { + const shouldContinue = await initialize() + if (!shouldContinue || error !== Error.NONE) return + setStep(Step.CONNECTING) + }, [initialize]) + + useEffect(() => { + setProgress(-1) + setMessage() + + if (error) return + + switch (step) { case Step.CONNECTING: { fastboot.current.waitForConnect() .then(() => { @@ -344,7 +347,6 @@ export function useFastboot() { if (error !== Error.NONE) { console.debug('[fastboot] error', error) setProgress(-1) - setOnContinue(null) setOnRetry(() => () => { console.debug('[fastboot] on retry') @@ -362,7 +364,7 @@ export function useFastboot() { connected, serial, - onContinue, + onContinue: handleContinue, onRetry, } } diff --git a/vite.config.js b/vite.config.js index ba64cd9..8d0a0c1 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,10 +1,20 @@ -import { fileURLToPath, URL } from 'node:url'; +import { fileURLToPath, URL } from 'node:url' import { defineConfig } from 'vite' import react from '@vitejs/plugin-react' +import vike from 'vike/plugin' // https://vitejs.dev/config/ export default defineConfig({ - plugins: [react()], + plugins: [ + react(), + vike({ + prerender: true, + }), + ], + publicDir: 'public', + preview: { + port: 5173 + }, resolve: { alias: [ { find: '@', replacement: fileURLToPath(new URL('./src', import.meta.url)) },