From f381147f1679bc9ce1e2695ce55fffbeb8bc0a13 Mon Sep 17 00:00:00 2001
From: Drew Bednar <drew@runcible.io>
Date: Wed, 14 Aug 2024 19:39:35 +0000
Subject: [PATCH] Adds build step using vite and breaks code into JS6 modules
 (#14)

This is a large update. I've selected Vite as the build system for this project. I tried other tools like golang minify but this had the most useful features and serves as a dev server too. I think a lot can be done to shore up the management of global state in the application. For now this works.

closes #13

#### Changes
- adds vite for dev/build
- Breaks out JS into JS6 modules
- Updates docker file to have two stage build

Co-authored-by: Drew Bednar <drew@androiddrew.com>
Reviewed-on: https://git.runcible.io/androiddrew/webserial/pulls/14
---
 .dockerignore              |   4 +-
 .gitignore                 |   3 +-
 Dockerfile                 |  11 +-
 Makefile                   |   4 +-
 package-lock.json          | 426 ++++++++++++++++++++++++++++++++++++-
 package.json               |   7 +-
 postcss.config.js          |   7 +
 src/app.js                 |  10 +
 src/eventListeners.js      |  67 ++++++
 src/globals.js             |  71 +++++++
 src/index.html             |  36 +---
 src/script.js              | 230 --------------------
 src/serialCommunication.js |  73 +++++++
 src/uiHelpers.js           |  63 ++++++
 vite.config.js             |  29 +++
 15 files changed, 764 insertions(+), 277 deletions(-)
 create mode 100644 postcss.config.js
 create mode 100644 src/app.js
 create mode 100644 src/eventListeners.js
 create mode 100644 src/globals.js
 delete mode 100644 src/script.js
 create mode 100644 src/serialCommunication.js
 create mode 100644 src/uiHelpers.js
 create mode 100644 vite.config.js

diff --git a/.dockerignore b/.dockerignore
index ffa7578..d2cc3c6 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,3 +1,3 @@
 Dockerfile
-Makefile
-README.md
\ No newline at end of file
+dist/
+node_modules/
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 66cfb9f..2d72753 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
 node_modules/
-dist/*.{html,js,css,ico}
+dist/
+examples/
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index 021334c..06d4871 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,2 +1,9 @@
-FROM nginx
-COPY ./src/index.html ./src/script.js ./src/main.css /usr/share/nginx/html
+FROM node:20.16.0 AS build-stage
+WORKDIR /build
+COPY package.json package-lock.json ./
+RUN npm install
+COPY . .
+RUN make build
+
+FROM nginx AS runtime-stage
+COPY --from=build-stage /build/dist/* /usr/share/nginx/html/
diff --git a/Makefile b/Makefile
index 06aa75b..55d978d 100644
--- a/Makefile
+++ b/Makefile
@@ -1,5 +1,5 @@
 serve:
-	python3 -m http.server 8080
+	npx vite
 .PHONY: serve
 
 watch:
@@ -7,7 +7,7 @@ watch:
 .PHONY: watch
 
 build:
-	echo "Not implemented yet..."
+	npx vite build --emptyOutDir
 .PHONY: build
 
 image:
diff --git a/package-lock.json b/package-lock.json
index 73526c1..a4255f5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -8,6 +8,167 @@
       "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==",
       "dev": true
     },
+    "@esbuild/aix-ppc64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+      "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/android-arm": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+      "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/android-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+      "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/android-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+      "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+      "dev": true,
+      "optional": true
+    },
+    "@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==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/darwin-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+      "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/freebsd-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+      "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/freebsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+      "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-arm": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+      "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+      "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-ia32": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+      "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-loong64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+      "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-mips64el": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+      "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-ppc64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+      "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-riscv64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+      "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-s390x": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+      "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/linux-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+      "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/netbsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+      "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/openbsd-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+      "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/sunos-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+      "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/win32-arm64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+      "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/win32-ia32": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+      "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+      "dev": true,
+      "optional": true
+    },
+    "@esbuild/win32-x64": {
+      "version": "0.21.5",
+      "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+      "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+      "dev": true,
+      "optional": true
+    },
     "@isaacs/cliui": {
       "version": "8.0.2",
       "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@@ -94,6 +255,124 @@
       "dev": true,
       "optional": true
     },
+    "@rollup/rollup-android-arm-eabi": {
+      "version": "4.20.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.20.0.tgz",
+      "integrity": "sha512-TSpWzflCc4VGAUJZlPpgAJE1+V60MePDQnBd7PPkpuEmOy8i87aL6tinFGKBFKuEDikYpig72QzdT3QPYIi+oA==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-android-arm64": {
+      "version": "4.20.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.20.0.tgz",
+      "integrity": "sha512-u00Ro/nok7oGzVuh/FMYfNoGqxU5CPWz1mxV85S2w9LxHR8OoMQBuSk+3BKVIDYgkpeOET5yXkx90OYFc+ytpQ==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-darwin-arm64": {
+      "version": "4.20.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.20.0.tgz",
+      "integrity": "sha512-uFVfvzvsdGtlSLuL0ZlvPJvl6ZmrH4CBwLGEFPe7hUmf7htGAN+aXo43R/V6LATyxlKVC/m6UsLb7jbG+LG39Q==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-darwin-x64": {
+      "version": "4.20.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.20.0.tgz",
+      "integrity": "sha512-xbrMDdlev53vNXexEa6l0LffojxhqDTBeL+VUxuuIXys4x6xyvbKq5XqTXBCEUA8ty8iEJblHvFaWRJTk/icAQ==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-linux-arm-gnueabihf": {
+      "version": "4.20.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.20.0.tgz",
+      "integrity": "sha512-jMYvxZwGmoHFBTbr12Xc6wOdc2xA5tF5F2q6t7Rcfab68TT0n+r7dgawD4qhPEvasDsVpQi+MgDzj2faOLsZjA==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-linux-arm-musleabihf": {
+      "version": "4.20.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.20.0.tgz",
+      "integrity": "sha512-1asSTl4HKuIHIB1GcdFHNNZhxAYEdqML/MW4QmPS4G0ivbEcBr1JKlFLKsIRqjSwOBkdItn3/ZDlyvZ/N6KPlw==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-linux-arm64-gnu": {
+      "version": "4.20.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.20.0.tgz",
+      "integrity": "sha512-COBb8Bkx56KldOYJfMf6wKeYJrtJ9vEgBRAOkfw6Ens0tnmzPqvlpjZiLgkhg6cA3DGzCmLmmd319pmHvKWWlQ==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-linux-arm64-musl": {
+      "version": "4.20.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.20.0.tgz",
+      "integrity": "sha512-+it+mBSyMslVQa8wSPvBx53fYuZK/oLTu5RJoXogjk6x7Q7sz1GNRsXWjn6SwyJm8E/oMjNVwPhmNdIjwP135Q==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-linux-powerpc64le-gnu": {
+      "version": "4.20.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.20.0.tgz",
+      "integrity": "sha512-yAMvqhPfGKsAxHN8I4+jE0CpLWD8cv4z7CK7BMmhjDuz606Q2tFKkWRY8bHR9JQXYcoLfopo5TTqzxgPUjUMfw==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-linux-riscv64-gnu": {
+      "version": "4.20.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.20.0.tgz",
+      "integrity": "sha512-qmuxFpfmi/2SUkAw95TtNq/w/I7Gpjurx609OOOV7U4vhvUhBcftcmXwl3rqAek+ADBwSjIC4IVNLiszoj3dPA==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-linux-s390x-gnu": {
+      "version": "4.20.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.20.0.tgz",
+      "integrity": "sha512-I0BtGXddHSHjV1mqTNkgUZLnS3WtsqebAXv11D5BZE/gfw5KoyXSAXVqyJximQXNvNzUo4GKlCK/dIwXlz+jlg==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-linux-x64-gnu": {
+      "version": "4.20.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.20.0.tgz",
+      "integrity": "sha512-y+eoL2I3iphUg9tN9GB6ku1FA8kOfmF4oUEWhztDJ4KXJy1agk/9+pejOuZkNFhRwHAOxMsBPLbXPd6mJiCwew==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-linux-x64-musl": {
+      "version": "4.20.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.20.0.tgz",
+      "integrity": "sha512-hM3nhW40kBNYUkZb/r9k2FKK+/MnKglX7UYd4ZUy5DJs8/sMsIbqWK2piZtVGE3kcXVNj3B2IrUYROJMMCikNg==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-win32-arm64-msvc": {
+      "version": "4.20.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.20.0.tgz",
+      "integrity": "sha512-psegMvP+Ik/Bg7QRJbv8w8PAytPA7Uo8fpFjXyCRHWm6Nt42L+JtoqH8eDQ5hRP7/XW2UiIriy1Z46jf0Oa1kA==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-win32-ia32-msvc": {
+      "version": "4.20.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.20.0.tgz",
+      "integrity": "sha512-GabekH3w4lgAJpVxkk7hUzUf2hICSQO0a/BLFA11/RMxQT92MabKAqyubzDZmMOC/hcJNlc+rrypzNzYl4Dx7A==",
+      "dev": true,
+      "optional": true
+    },
+    "@rollup/rollup-win32-x64-msvc": {
+      "version": "4.20.0",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.20.0.tgz",
+      "integrity": "sha512-aJ1EJSuTdGnM6qbVC4B5DSmozPTqIag9fSzXRNNo+humQLG89XpPgdt16Ia56ORD7s+H8Pmyx44uczDQ0yDzpg==",
+      "dev": true,
+      "optional": true
+    },
+    "@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
+    },
     "ansi-regex": {
       "version": "6.0.1",
       "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
@@ -128,6 +407,20 @@
       "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==",
       "dev": true
     },
+    "autoprefixer": {
+      "version": "10.4.20",
+      "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz",
+      "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==",
+      "dev": true,
+      "requires": {
+        "browserslist": "^4.23.3",
+        "caniuse-lite": "^1.0.30001646",
+        "fraction.js": "^4.3.7",
+        "normalize-range": "^0.1.2",
+        "picocolors": "^1.0.1",
+        "postcss-value-parser": "^4.2.0"
+      }
+    },
     "balanced-match": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
@@ -158,12 +451,30 @@
         "fill-range": "^7.1.1"
       }
     },
+    "browserslist": {
+      "version": "4.23.3",
+      "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz",
+      "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==",
+      "dev": true,
+      "requires": {
+        "caniuse-lite": "^1.0.30001646",
+        "electron-to-chromium": "^1.5.4",
+        "node-releases": "^2.0.18",
+        "update-browserslist-db": "^1.1.0"
+      }
+    },
     "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
     },
+    "caniuse-lite": {
+      "version": "1.0.30001651",
+      "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz",
+      "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==",
+      "dev": true
+    },
     "chokidar": {
       "version": "3.6.0",
       "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz",
@@ -247,12 +558,55 @@
       "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
       "dev": true
     },
+    "electron-to-chromium": {
+      "version": "1.5.6",
+      "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.6.tgz",
+      "integrity": "sha512-jwXWsM5RPf6j9dPYzaorcBSUg6AiqocPEyMpkchkvntaH9HGfOOMZwxMJjDY/XEs3T5dM7uyH1VhRMkqUU9qVw==",
+      "dev": true
+    },
     "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
     },
+    "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,
+      "requires": {
+        "@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"
+      }
+    },
+    "escalade": {
+      "version": "3.1.2",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
+      "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
+      "dev": true
+    },
     "fast-glob": {
       "version": "3.3.2",
       "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz",
@@ -305,6 +659,12 @@
         "signal-exit": "^4.0.1"
       }
     },
+    "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
+    },
     "fsevents": {
       "version": "2.3.3",
       "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -483,12 +843,24 @@
       "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
       "dev": true
     },
+    "node-releases": {
+      "version": "2.0.18",
+      "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz",
+      "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==",
+      "dev": true
+    },
     "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
     },
+    "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
+    },
     "object-assign": {
       "version": "4.1.1",
       "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -668,6 +1040,32 @@
       "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==",
       "dev": true
     },
+    "rollup": {
+      "version": "4.20.0",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.20.0.tgz",
+      "integrity": "sha512-6rbWBChcnSGzIlXeIdNIZTopKYad8ZG8ajhl78lGRLsI2rX8IkaotQhVas2Ma+GPxJav19wrSzvRvuiv0YKzWw==",
+      "dev": true,
+      "requires": {
+        "@rollup/rollup-android-arm-eabi": "4.20.0",
+        "@rollup/rollup-android-arm64": "4.20.0",
+        "@rollup/rollup-darwin-arm64": "4.20.0",
+        "@rollup/rollup-darwin-x64": "4.20.0",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.20.0",
+        "@rollup/rollup-linux-arm-musleabihf": "4.20.0",
+        "@rollup/rollup-linux-arm64-gnu": "4.20.0",
+        "@rollup/rollup-linux-arm64-musl": "4.20.0",
+        "@rollup/rollup-linux-powerpc64le-gnu": "4.20.0",
+        "@rollup/rollup-linux-riscv64-gnu": "4.20.0",
+        "@rollup/rollup-linux-s390x-gnu": "4.20.0",
+        "@rollup/rollup-linux-x64-gnu": "4.20.0",
+        "@rollup/rollup-linux-x64-musl": "4.20.0",
+        "@rollup/rollup-win32-arm64-msvc": "4.20.0",
+        "@rollup/rollup-win32-ia32-msvc": "4.20.0",
+        "@rollup/rollup-win32-x64-msvc": "4.20.0",
+        "@types/estree": "1.0.5",
+        "fsevents": "~2.3.2"
+      }
+    },
     "run-parallel": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz",
@@ -797,9 +1195,9 @@
       "dev": true
     },
     "tailwindcss": {
-      "version": "3.4.9",
-      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.9.tgz",
-      "integrity": "sha512-1SEOvRr6sSdV5IDf9iC+NU4dhwdqzF4zKKq3sAbasUWHEM6lsMhX+eNN5gkPx1BvLFEnZQEUFbXnGj8Qlp83Pg==",
+      "version": "3.4.10",
+      "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz",
+      "integrity": "sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==",
       "dev": true,
       "requires": {
         "@alloc/quick-lru": "^5.2.0",
@@ -859,12 +1257,34 @@
       "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
       "dev": true
     },
+    "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,
+      "requires": {
+        "escalade": "^3.1.2",
+        "picocolors": "^1.0.1"
+      }
+    },
     "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
     },
+    "vite": {
+      "version": "5.4.0",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.0.tgz",
+      "integrity": "sha512-5xokfMX0PIiwCMCMb9ZJcMyh5wbBun0zUzKib+L65vAZ8GY9ePZMXxFrHbr/Kyll2+LSCY7xtERPpxkBDKngwg==",
+      "dev": true,
+      "requires": {
+        "esbuild": "^0.21.3",
+        "fsevents": "~2.3.3",
+        "postcss": "^8.4.40",
+        "rollup": "^4.13.0"
+      }
+    },
     "which": {
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
diff --git a/package.json b/package.json
index b93902c..f8b40c4 100644
--- a/package.json
+++ b/package.json
@@ -1,5 +1,8 @@
 {
   "devDependencies": {
-    "tailwindcss": "^3.4.9"
+    "autoprefixer": "^10.4.20",
+    "postcss": "^8.4.41",
+    "tailwindcss": "^3.4.10",
+    "vite": "^5.4.0"
   }
-}
+}
\ No newline at end of file
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 0000000..a42e12c
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,7 @@
+// postcss.config.js
+module.exports = {
+    plugins: {
+        tailwindcss: {},
+        autoprefixer: {},
+    },
+};
diff --git a/src/app.js b/src/app.js
new file mode 100644
index 0000000..0f6f05b
--- /dev/null
+++ b/src/app.js
@@ -0,0 +1,10 @@
+import * as globals from './globals.js';
+import { setupEventListeners } from './eventListeners.js';
+import { updateSerialSelect } from './uiHelpers.js';
+
+document.addEventListener('DOMContentLoaded', async () => {
+    const ports = await navigator.serial.getPorts();
+    globals.setSerialPorts(ports);
+    updateSerialSelect(ports);
+    setupEventListeners();
+});
\ No newline at end of file
diff --git a/src/eventListeners.js b/src/eventListeners.js
new file mode 100644
index 0000000..a7ca9a5
--- /dev/null
+++ b/src/eventListeners.js
@@ -0,0 +1,67 @@
+import * as globals from './globals.js';
+import { connectToSerialPort, transmitContents } from './serialCommunication.js';
+import { scrollToBottom, updateSerialSelect, onPortDisconnect } from './uiHelpers.js';
+
+const uPyKeyboardInterrupt = new Uint8Array([13, 3, 3]);
+
+export function setupEventListeners() {
+    globals.addPort.addEventListener('click', async () => {
+        const port = await navigator.serial.requestPort();
+        globals.serialPorts.push(port);
+        updateSerialSelect(globals.serialPorts);
+    });
+
+    globals.autoscrollCheckbox.addEventListener('change', (e) => {
+        globals.setAutoscroll(e.target.checked);
+        if (globals.autoscroll) {
+            scrollToBottom();
+        }
+    });
+
+    globals.clearButton.addEventListener('click', () => {
+        while (globals.scrollableElement.firstChild) {
+            globals.scrollableElement.removeChild(globals.scrollableElement.firstChild);
+        }
+    });
+
+    globals.connectButton.addEventListener('click', async () => {
+        const selectedPort = globals.serialPorts[globals.select.selectedIndex];
+        const baudRate = Math.round(globals.baud.value);
+        if (!globals.isPortConnected) {
+            console.log("selected port: ", selectedPort)
+            console.log("Baud Rate: ", baudRate)
+            await connectToSerialPort(selectedPort, baudRate);
+        } else {
+            console.log("Disconnecting from port...")
+            globals.setPortConnected(false);
+            onPortDisconnect();
+        }
+    });
+
+    globals.refreshPorts.addEventListener('click', async () => {
+        const ports = await navigator.serial.getPorts();
+        console.log(ports)
+        globals.setSerialPorts(ports)
+        updateSerialSelect(globals.serialPorts)
+    });
+
+    globals.transmitButton.addEventListener('click', async (event) => {
+        transmitContents(globals.transmitInput.innerText)
+        globals.transmitInput.innerHTML = ''
+    });
+
+    globals.transmitInput.addEventListener('keydown', async (event) => {
+        if (event.key === 'c' && event.ctrlKey) {
+            event.preventDefault();
+            console.log("Sending interrupt ", uPyKeyboardInterrupt)
+            await globals.writer.write(uPyKeyboardInterrupt);
+        }
+    });
+
+    globals.transmitInput.addEventListener('keyup', (event) => {
+        if (event.key === 'Enter' && !event.shiftKey) {
+            transmitContents(globals.transmitInput.innerText);
+            globals.transmitInput.innerHTML = ''
+        }
+    });
+}
\ No newline at end of file
diff --git a/src/globals.js b/src/globals.js
new file mode 100644
index 0000000..f2c686d
--- /dev/null
+++ b/src/globals.js
@@ -0,0 +1,71 @@
+export const addDeviceMessage = `Add a device...&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`;
+export const addPort = document.getElementById('add-port');
+export const autoscrollCheckbox = document.getElementById('autoscroll-checkbox');
+export const baud = document.getElementById('baud');
+export const clearButton = document.getElementById('clear-button');
+export const connectButton = document.getElementById('connect-button');
+export const refreshPorts = document.getElementById('refresh-ports');
+export const scrollableElement = document.getElementById('scrollable-element');
+export const select = document.getElementById('serial-select');
+export const transmitInput = document.querySelector('div[contenteditable="true"]');
+export const transmitButton = document.getElementById('transmit-button');
+export const uPyKeyboardInterrupt = new Uint8Array([13, 3, 3]);
+
+export let controller;
+export let isPortConnected = false;
+export let autoscroll = true;
+export let serialPorts = [];
+export let encoder;
+export let reader;
+export let writer;
+
+export function setController(newController) {
+    controller = newController
+}
+
+export function setPortConnected(isConnected) {
+    isPortConnected = isConnected;
+}
+
+export function setAutoscroll(isSet) {
+    autoscroll = isSet;
+    console.log('Set autoscroll: ', autoscroll)
+}
+
+export function setSerialPorts(ports) {
+    serialPorts = ports
+    console.log('Set ports: ', serialPorts)
+}
+
+export function getEncoder() {
+    return encoder;
+}
+
+export function setEncoder(newEncoder) {
+    encoder = newEncoder;
+}
+
+export function getReader() {
+    return reader;
+}
+
+export function setReader(newReader) {
+    reader = newReader;
+}
+
+export function getWriter() {
+    return writer;
+}
+
+export function setWriter(newWriter) {
+    writer = newWriter;
+}
+
+// expose some globals for debuggin in webconsole
+if (typeof window !== 'undefined') {
+    window.wsc = {
+        isPortConnected,
+        autoscroll,
+        serialPorts,
+    };
+}
\ No newline at end of file
diff --git a/src/index.html b/src/index.html
index 32b0cea..71df1ae 100644
--- a/src/index.html
+++ b/src/index.html
@@ -55,17 +55,6 @@
         </div>
       </div>
       <div class="font-mono rounded-md bg-white shadow-xl h-[calc(75vh-70px)] overflow-y-auto border border-gray-300 p-2.5 bg-gray-200 mx-5 mb-6  " id="scrollable-element"></div>
-    <!-- Transmit OLD-->
-      <!-- <div class="flex items-center space-x-2 px-5 mb-5">
-        <input type="text" id="transmit-input" class="font-mono flex-grow font-medium bg-white rounded-md py-1.5 px-3 border-0 border-gray-300 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-emerald-500 sm:text-sm sm:leading-6" placeholder="Enter text to send...">
-        <button id="transmit-button" class="relative inline-flex items-center gap-x-1.5 px-4 py-1.5 bg-blue-500 text-white rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50">
-          <svg class="-ml-0.5 h-5 w-5 text-white" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
-            <path fill-rule="evenodd" d="M2 3.75A.75.75 0 012.75 3h11.5a.75.75 0 010 1.5H2.75A.75.75 0 012 3.75zM2 7.5a.75.75 0 01.75-.75h6.365a.75.75 0 010 1.5H2.75A.75.75 0 012 7.5zM14 7a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02l-1.95-2.1v6.59a.75.75 0 01-1.5 0V9.66l-1.95 2.1a.75.75 0 11-1.1-1.02l3.25-3.5A.75.75 0 0114 7zM2 11.25a.75.75 0 01.75-.75H7A.75.75 0 017 12H2.75a.75.75 0 01-.75-.75z" clip-rule="evenodd" />
-          </svg>
-          Send</button>
-      </div> -->
-    <!-- End Transmit OLD-->
-      <!-- Experiment -->
       <div class="flex items-center space-x-2 px-5 mb-5">
         <div contenteditable="true" class="shadow-xl flex-grow rounded-md font-mono py-1.5 px-3 ring-1 ring-inset ring-gray-300 text-gray-900 outline-none focus:ring-2 focus:ring-emerald-500">
           <p data-placeholder="Enter text to send..." class="empty:before:content-[attr(data-placeholder)] empty:before:text-gray-500"></p>
@@ -77,29 +66,6 @@
           Send
         </button>
       </div> 
-
-      <!-- <div class="flex items-center space-x-2 px-5 mb-5">
-        <div aria-label="Enter text to send..." class="flex-grow mt-1 max-h-96 w-full overflow-y-auto break-words">
-          <div contenteditable="true" translate="no" enterkeyhint="enter" tabindex="0" 
-               class="font-mono flex-grow font-medium bg-white rounded-md py-1.5 px-3 text-gray-900 ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-emerald-500 sm:text-sm sm:leading-6 break-words max-w-[60ch]">
-            <p data-placeholder="Enter text to send..." 
-               class="m-0 text-gray-900 focus:outline-none empty:before:content-[attr(data-placeholder)] empty:before:text-gray-500">
-            </p>
-          </div>
-        </div>
-        <button id="transmit-button" class="relative inline-flex items-center gap-x-1.5 px-4 py-1.5 bg-blue-500 text-white rounded-md hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50">
-          <svg class="-ml-0.5 h-5 w-5 text-white" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
-            <path fill-rule="evenodd" d="M2 3.75A.75.75 0 012.75 3h11.5a.75.75 0 010 1.5H2.75A.75.75 0 012 3.75zM2 7.5a.75.75 0 01.75-.75h6.365a.75.75 0 010 1.5H2.75A.75.75 0 012 7.5zM14 7a.75.75 0 01.55.24l3.25 3.5a.75.75 0 11-1.1 1.02l-1.95-2.1v6.59a.75.75 0 01-1.5 0V9.66l-1.95 2.1a.75.75 0 11-1.1-1.02l3.25-3.5A.75.75 0 0114 7zM2 11.25a.75.75 0 01.75-.75H7A.75.75 0 017 12H2.75a.75.75 0 01-.75-.75z" clip-rule="evenodd" />
-          </svg>
-          Send
-        </button>
-      </div> -->
-       <!-- End experiment -->
-      <!-- <div aria-label="Enter text to send..." class="mt-1 max-h-96 w-full overflow-y-auto break-words">
-        <div contenteditable="true" translate="no" enterkeyhint="enter" tabindex="0" class="break-words max-w-[60ch]">
-           <p data-placeholder="Enter text to send..." class="text-gray-900 ring-1 ring-gray-300 font-mono ring-1 ring-inset ring-gray-300 focus:ring-2 focus:ring-emerald-500 is-empty is-editor-empty before:!text-text-500 before:whitespace-nowrap"><br class="ProseMirror-trailingBreak"></p>
-        </div>
-     </div> -->
     </div>
     <footer>
       <div class="mx-auto max-w-7xl overflow-hidden py-12 px-4 sm:px-6 lg:px-8">
@@ -110,6 +76,6 @@
         </p>
       </div>
     </footer>
-    <script src="script.js"></script>
+    <script type="module" src="./app.js"></script>
   </body>
 </html>
diff --git a/src/script.js b/src/script.js
deleted file mode 100644
index a79f767..0000000
--- a/src/script.js
+++ /dev/null
@@ -1,230 +0,0 @@
-// Globals
-const addDeviceMessage = `Add a device...&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;`
-const addPort = document.getElementById('add-port')
-const autoscrollCheckbox = document.getElementById('autoscroll-checkbox');
-const baud = document.getElementById('baud');
-const clearButton = document.getElementById('clear-button');
-const connectButton = document.getElementById('connect-button')
-const refreshPorts = document.getElementById('refresh-ports');
-const scrollableElement = document.getElementById('scrollable-element');
-const select = document.getElementById('serial-select');
-const transmitInput = document.querySelector('div[contenteditable="true"]');
-const transmitButton = document.getElementById('transmit-button');
-const uPyKeyboardInterrupt = new Uint8Array([13, 3, 3])
-
-let isPortConnected = false;
-let controller;
-let autoscroll = true;
-let serialPorts = [];
-
-let encoder;
-let reader;
-let writer;
-
-// Event Listeners
-addPort.addEventListener('click', async () => {
-    const port = await navigator.serial.requestPort();
-    serialPorts.push(port);
-    updateSerialSelect(serialPorts);
-});
-
-autoscrollCheckbox.addEventListener('change', (e) => {
-    autoscroll = e.target.checked;
-    if (autoscroll) {
-        scrollToBottom();
-    }
-});
-
-clearButton.addEventListener('click', () => {
-    while (scrollableElement.firstChild) {
-        scrollableElement.removeChild(scrollableElement.firstChild);
-    }
-});
-
-connectButton.addEventListener('click', async () => {
-    const selectedPort = serialPorts[select.selectedIndex]
-    const baudRate = Math.round(baud.value)
-    if (!isPortConnected) {
-        await connectToSerialPort(selectedPort, baudRate);
-    } else {
-        isPortConnected = false;
-        onPortDisconnect();
-    }
-});
-
-refreshPorts.addEventListener('click', async () => {
-    ports = await navigator.serial.getPorts();
-    serialPorts = ports
-    updateSerialSelect(ports)
-});
-
-transmitButton.addEventListener('click', async (event) => {
-    transmitContents(transmitInput.innerText)
-    transmitInput.innerHTML = ''
-});
-
-transmitInput.addEventListener('keydown', (event) => {
-    if (event.key === 'c' && event.ctrlKey) {
-        event.preventDefault();
-        console.log("Sending interrupt ", uPyKeyboardInterrupt)
-        writer.write(uPyKeyboardInterrupt);
-    }
-});
-
-transmitInput.addEventListener('keyup', (event) => {
-    if (event.key === 'Enter' && !event.shiftKey) {
-        transmitContents(transmitInput.innerText);
-        transmitInput.innerHTML = ''
-    }
-});
-
-
-// Functions
-function transmitContents(input) {
-    encoded_string = encoder.encode(input + '\r')
-    console.log("Binary Contents: ", encoded_string)
-    writer.write(encoded_string)
-}
-
-
-function onPortConnect() {
-    connectButton.classList.remove('bg-emerald-500', 'hover:bg-emerald-600', 'focus:ring-emerald-500');
-    connectButton.classList.add('bg-red-500', 'hover:bg-red-600', 'focus:ring-red-500');
-    connectButton.textContent = 'Disconnect';
-}
-
-function onPortDisconnect() {
-    connectButton.classList.remove('bg-red-500', 'hover:bg-red-600', 'focus:ring-red-500');
-    connectButton.classList.add('bg-emerald-500', 'hover:bg-emerald-600', 'focus:ring-emerald-500');
-    connectButton.textContent = 'Connect';
-}
-
-
-function buildPortOption(port) {
-    const option = document.createElement('option');
-    option.value = port;
-
-    try {
-        const info = port.getInfo();
-        if (info && 'usbVendorId' in info && 'usbProductId' in info) {
-            const { usbVendorId, usbProductId } = info;
-            option.text = `Device ${usbVendorId}:${usbProductId}`;
-        } else {
-            console.error('getInfo() did not return expected properties:', info);
-            option.text = 'Unknown Device';
-        }
-    } catch (error) {
-        console.error('Error retrieving port information:', error);
-        option.text = 'Unknown Device';
-    }
-
-    return option;
-}
-
-function addText(text) {
-    const newText = document.createElement('p');
-    newText.textContent = `${new Date().toLocaleTimeString()} ${text}`;
-    scrollableElement.appendChild(newText);
-
-    if (autoscroll) {
-        scrollToBottom();
-    }
-}
-
-function scrollToBottom() {
-    scrollableElement.scrollTop = scrollableElement.scrollHeight;
-}
-
-// Async Functions
-async function connectToSerialPort(port, baud) {
-    await port.open({ baudRate: baud });
-    onPortConnect()
-
-    let buffer = ''
-    // The TextDecoderStream interface of the Encoding API converts a stream of text in a binary encoding,
-    // such as UTF-8 etc., to a stream of strings
-    // const textEncoder = new TextEncoderStream();
-    // const writableStreamClosed = textEncoder.readable.pipeTo(port.writable);
-    // writer = textEncoder.writable.getWriter();
-
-    encoder = new TextEncoder()
-    writer = port.writable.getWriter();
-
-    const textDecoder = new TextDecoderStream();
-    const readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
-    reader = textDecoder.readable.getReader();
-
-    controller = new AbortController();
-    const signal = controller.signal;
-    try {
-        isPortConnected = true;
-        while (isPortConnected) {
-            const { value, done } = await reader.read({ signal });
-            if (done || !port.readable) {
-                break;
-            }
-
-            buffer += value;
-
-            while (buffer.includes('\n')) {
-                const newlineIndex = buffer.indexOf('\n');
-                const line = buffer.slice(0, newlineIndex);
-                buffer = buffer.slice(newlineIndex + 1);
-
-                addText(line);
-            }
-        }
-    } catch (error) {
-        console.error('Error reading data from serial port:', error);
-    } finally {
-        // Port cleanup
-        controller.abort();
-        writer.releaseLock();
-        reader.releaseLock();
-
-        // try {
-        //     writer.close();
-        //     await writableStreamClosed;
-        // } catch (error) {
-        //     console.error("Error encountered in writeableSteamClosed:", error)
-        // }
-
-        try {
-            reader.cancel();
-        } catch (error) {
-            console.error("Error encountered in readableStreamClosed:", error);
-        }
-
-        try {
-            await readableStreamClosed;
-        } catch (error) {
-        }
-
-        try {
-            await readableStreamClosed;
-        } catch (error) {
-        }
-
-        try {
-            await port.close();
-        } catch (error) {
-            console.error("Error encountered closing port:", error);
-        }
-        onPortDisconnect()
-    }
-}
-
-async function updateSerialSelect(ports) {
-    if (ports.length < 1) {
-        const option = document.createElement('option');
-        option.text = addDeviceMessage;
-        select.innerHTML = ''
-        select.appendChild(option)
-        return;
-    }
-    select.innerHTML = ''
-    ports.forEach(port => {
-        const option = buildPortOption(port)
-        select.appendChild(option);
-    });
-}
\ No newline at end of file
diff --git a/src/serialCommunication.js b/src/serialCommunication.js
new file mode 100644
index 0000000..f436792
--- /dev/null
+++ b/src/serialCommunication.js
@@ -0,0 +1,73 @@
+import * as globals from './globals.js';
+import { onPortConnect, onPortDisconnect, addText } from './uiHelpers.js';
+
+export async function connectToSerialPort(port, baud) {
+    console.log("Connecting to port: ", port);
+    await port.open({ baudRate: baud });
+    onPortConnect()
+
+    let buffer = ''
+    globals.setEncoder(new TextEncoder());
+    globals.setWriter(port.writable.getWriter());
+
+    const textDecoder = new TextDecoderStream();
+    const readableStreamClosed = port.readable.pipeTo(textDecoder.writable);
+    globals.setReader(textDecoder.readable.getReader());
+
+    globals.setController(new AbortController());
+    const signal = globals.controller.signal;
+    try {
+        globals.setPortConnected(true);
+        while (globals.isPortConnected) {
+            const { value, done } = await globals.reader.read({ signal });
+            if (done || !port.readable) {
+                break;
+            }
+
+            buffer += value;
+
+            while (buffer.includes('\n')) {
+                const newlineIndex = buffer.indexOf('\n');
+                const line = buffer.slice(0, newlineIndex);
+                buffer = buffer.slice(newlineIndex + 1);
+
+                addText(line);
+            }
+        }
+        console.log("Exiting serial print loop")
+    } catch (error) {
+        console.error('Error reading data from serial port:', error);
+    } finally {
+        // Port cleanup
+        console.log("Cleaning up port.")
+        globals.controller.abort();
+        console.log("Releasing writer lock")
+        globals.writer.releaseLock();
+
+        // Since we are using a readableSteam we should call cancel instead of globals.reader.releaseLock();
+        globals.reader.cancel().then(() => {
+            console.log('Stream canceled');
+        }).catch(error => {
+            console.error('Error canceling the stream:', error);
+        });
+
+        try {
+            await readableStreamClosed;
+        } catch (error) {
+        }
+
+        try {
+            await port.close();
+        } catch (error) {
+            console.error("Error encountered closing port:", error);
+        }
+        console.log("Port closed.")
+        onPortDisconnect()
+    }
+}
+
+export function transmitContents(input) {
+    const encoded_string = globals.encoder.encode(input + '\r')
+    console.log("Binary Contents: ", encoded_string);
+    globals.writer.write(encoded_string);
+}
diff --git a/src/uiHelpers.js b/src/uiHelpers.js
new file mode 100644
index 0000000..3ea3e7c
--- /dev/null
+++ b/src/uiHelpers.js
@@ -0,0 +1,63 @@
+import * as globals from './globals.js';
+
+export function onPortConnect() {
+    globals.connectButton.classList.remove('bg-emerald-500', 'hover:bg-emerald-600', 'focus:ring-emerald-500');
+    globals.connectButton.classList.add('bg-red-500', 'hover:bg-red-600', 'focus:ring-red-500');
+    globals.connectButton.textContent = 'Disconnect';
+}
+
+export function onPortDisconnect() {
+    globals.connectButton.classList.remove('bg-red-500', 'hover:bg-red-600', 'focus:ring-red-500');
+    globals.connectButton.classList.add('bg-emerald-500', 'hover:bg-emerald-600', 'focus:ring-emerald-500');
+    globals.connectButton.textContent = 'Connect';
+}
+
+export function buildPortOption(port) {
+    const option = document.createElement('option');
+    option.value = port;
+
+    try {
+        const info = port.getInfo();
+        if (info && 'usbVendorId' in info && 'usbProductId' in info) {
+            const { usbVendorId, usbProductId } = info;
+            option.text = `Device ${usbVendorId}:${usbProductId}`;
+        } else {
+            console.error('getInfo() did not return expected properties:', info);
+            option.text = 'Unknown Device';
+        }
+    } catch (error) {
+        console.error('Error retrieving port information:', error);
+        option.text = 'Unknown Device';
+    }
+
+    return option;
+}
+
+export function addText(text) {
+    const newText = document.createElement('p');
+    newText.textContent = `${new Date().toLocaleTimeString()} ${text}`;
+    globals.scrollableElement.appendChild(newText);
+
+    if (globals.autoscroll) {
+        scrollToBottom();
+    }
+}
+
+export function scrollToBottom() {
+    globals.scrollableElement.scrollTop = globals.scrollableElement.scrollHeight;
+}
+
+export async function updateSerialSelect(ports) {
+    if (ports.length < 1) {
+        const option = document.createElement('option');
+        option.text = globals.addDeviceMessage;
+        globals.select.innerHTML = ''
+        globals.select.appendChild(option)
+        return;
+    }
+    globals.select.innerHTML = ''
+    ports.forEach(port => {
+        const option = buildPortOption(port)
+        globals.select.appendChild(option);
+    });
+}
\ No newline at end of file
diff --git a/vite.config.js b/vite.config.js
new file mode 100644
index 0000000..0d2d4fb
--- /dev/null
+++ b/vite.config.js
@@ -0,0 +1,29 @@
+import { defineConfig } from 'vite';
+import { resolve } from 'path';
+
+export default defineConfig({
+    root: resolve(__dirname, 'src'),
+    base: './',
+    build: {
+        outDir: resolve(__dirname, 'dist'),
+        rollupOptions: {
+            input: {
+                main: resolve(__dirname, 'src/index.html'),
+            },
+            output: {
+                entryFileNames: 'script.[hash].js',
+                chunkFileNames: 'chunk.[hash].js',
+                assetFileNames: '[name].[hash].[ext]',
+            },
+        },
+        minify: 'esbuild',
+    },
+    css: {
+        postcss: {
+            plugins: [
+                require('tailwindcss'),
+                require('autoprefixer'),
+            ],
+        },
+    },
+});