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)) },