1
0

Modernize frontend build pipeline with esbuild and ES modules

This commit is contained in:
2026-03-19 17:37:34 +01:00
parent 9cc4069aca
commit 9349896763
33 changed files with 1406 additions and 99 deletions

19
.gitignore vendored
View File

@@ -11,21 +11,26 @@ kobopatch-wasm/testdata/
kobopatch-wasm/kobopatch.wasm
kobopatch-wasm/wasm_exec.js
# Test artifacts in webroot
web/public/_test_firmware.zip
# Generated files in src (written by build scripts, regenerated on demand)
web/src/wasm/kobopatch.wasm
web/src/js/wasm_exec.js
web/src/nickelmenu/NickelMenu.zip
web/src/nickelmenu/kobo-config.zip
# WASM artifacts copied to webroot for serving
web/public/wasm/kobopatch.wasm
web/public/js/wasm_exec.js
# Build output
web/dist/
# Node
web/node_modules/
# E2E tests
tests/e2e/node_modules/
tests/e2e/test-results/
tests/e2e/playwright-report/
tests/e2e/test_firmware.zip
# NickelMenu build artifacts
nickelmenu/kobo-config/
web/public/nickelmenu/
# Claude
.claude
.claude

100
README.md
View File

@@ -34,54 +34,57 @@ If you choose to apply custom patches, **patching happens fully client-side**
## File structure
```
web/public/ # Webroot — serve this directory
index.html # Single-page app, multi-step wizard
css/
style.css
js/
app.js # Step navigation, flow orchestration, firmware download with progress
kobo-device.js # KOBO_MODELS (serial prefix → name), FIRMWARE_DOWNLOADS (version+prefix → URL),
# getDevicesForVersion(), getFirmwareURL(), KoboDevice class (File System Access API)
nickelmenu.js # NickelMenuInstaller: downloads NickelMenu.zip + kobo-config.zip, installs to
# device or builds download ZIP, handles config file filtering and modification
patch-ui.js # PatchUI class: loads patch zips (JSZip), parses YAML, renders toggle UI,
# generates kobopatch.yaml config with overrides
kobopatch.js # KobopatchRunner: spawns Web Worker per build, handles progress/done/error messages
patch-worker.js # Web Worker: loads wasm_exec.js + kobopatch.wasm, runs patchFirmware(),
# posts progress back, transfers result buffer zero-copy
wasm_exec.js # Go WASM support runtime (copied from Go SDK by setup.sh, gitignored)
jszip.min.js # Bundled JSZip library
wasm/
kobopatch.wasm # Compiled WASM binary (built by build.sh, gitignored)
patches/
index.json # Contains a list of available patches
patches_*.zip # Each contains kobopatch.yaml + src/*.yaml patch files
nickelmenu/ # NickelMenu assets (built by nickelmenu/setup.sh, gitignored)
NickelMenu.zip # NickelMenu release
kobo-config.zip # Curated configuration files (fonts, screensaver, menu items)
web/
src/ # Source assets (committed)
index.html # Single-page app template
css/
style.css
js/
app.js # ES module entry point: step navigation, flow orchestration
kobo-device.js # KOBO_MODELS, FIRMWARE_DOWNLOADS, KoboDevice class
nickelmenu.js # NickelMenuInstaller: downloads/bundles NickelMenu + config
patch-ui.js # PatchUI: loads patches, parses YAML, renders toggle UI
kobopatch.js # KobopatchRunner: spawns Web Worker per build
patch-worker.js # Web Worker: loads WASM, runs patchFirmware()
wasm_exec.js # Go WASM runtime (generated by build.sh, gitignored)
wasm/
kobopatch.wasm # Compiled WASM binary (generated by build.sh, gitignored)
patches/
index.json # Available patch manifest
patches_*.zip # Patch files per firmware version
nickelmenu/ # NickelMenu assets (generated by setup.sh, gitignored)
NickelMenu.zip
kobo-config.zip
favicon/
dist/ # Build output (gitignored, fully regenerable)
bundle.js # esbuild output (minified, content-hashed)
index.html # Generated with cache-busted references
css/ favicon/ patches/ nickelmenu/ wasm/ js/
build.mjs # esbuild build script + asset copy
package.json # esbuild, jszip
nickelmenu/
setup.sh # Downloads NickelMenu.zip and bundles kobo-config.zip from kobo-config repo
setup.sh # Downloads NickelMenu.zip and bundles kobo-config.zip
kobopatch-wasm/ # WASM build
main.go # Go entry point: jsPatchFirmware() → patchFirmware() pipeline
kobopatch-wasm/
main.go # Go entry point
go.mod
setup.sh # Clones kobopatch source, copies wasm_exec.js
build.sh # GOOS=js GOARCH=wasm go build, copies .wasm to web/public/wasm/
integration_test.go # Go integration test: validates SHA1 checksums of patched binaries
test-integration.sh # Downloads firmware and runs integration_test.go
build.sh # Compiles WASM, copies to web/dist/wasm/ and web/src/js/
integration_test.go
test-integration.sh
tests/
e2e/ # Playwright E2E tests
integration.spec.js # Full browser tests: NickelMenu flows, custom patches, mock device
e2e/
integration.spec.js # Playwright E2E tests
playwright.config.js
run-e2e.sh # E2E runner (downloads firmware, sets up NickelMenu assets, installs browser)
run-e2e.sh
```
## Adding a new software version
1. Add the patch zip to `web/public/patches/` and update `index.json`
2. Add download URLs to `FIRMWARE_DOWNLOADS` in `js/kobo-device.js` (keyed by version then serial prefix)
1. Add the patch zip to `web/src/patches/` and update `index.json`
2. Add download URLs to `FIRMWARE_DOWNLOADS` in `web/src/js/kobo-device.js` (keyed by version then serial prefix)
3. The Kobo CDN prefix per device family (e.g. `kobo12`, `kobo13`) is stable; the date path segment changes per release
## Building the WASM binary
@@ -91,7 +94,7 @@ Requires Go 1.21+.
```bash
cd kobopatch-wasm
./setup.sh # first time only
./build.sh # compiles WASM, copies to web/public/wasm/
./build.sh # compiles WASM, copies to web/dist/wasm/
```
## Setting up NickelMenu assets
@@ -100,7 +103,18 @@ cd kobopatch-wasm
nickelmenu/setup.sh
```
This downloads `NickelMenu.zip` and clones/updates the [kobo-config](https://github.com/nicoverbruggen/kobo-config) repo to bundle `kobo-config.zip` into `web/public/nickelmenu/`.
This downloads `NickelMenu.zip` and clones/updates the [kobo-config](https://github.com/nicoverbruggen/kobo-config) repo to bundle `kobo-config.zip` into `web/src/nickelmenu/`.
## Building the frontend
The JS source lives in `web/src/js/` as ES modules. esbuild bundles them into a single `web/dist/bundle.js`.
```bash
cd web
npm install
npm run build # production build (minified)
npm run dev # dev server with watch mode on :8889
```
## Running locally
@@ -108,7 +122,13 @@ This downloads `NickelMenu.zip` and clones/updates the [kobo-config](https://git
./serve-locally.sh
```
This serves the app at `http://localhost:8888`. If the WASM binary or NickelMenu assets haven't been set up yet, the script handles that automatically.
This serves the app at `http://localhost:8888`. The script automatically:
1. Sets up NickelMenu assets if missing (`web/src/nickelmenu/`)
2. Builds the JS bundle (`web/dist/bundle.js`)
3. Builds the WASM binary if missing (`web/dist/wasm/kobopatch.wasm`)
You can delete the entire `web/dist/` folder and re-run `serve-locally.sh` to regenerate everything.
## Testing
@@ -166,7 +186,7 @@ The WASM patcher performs several checks on each patched binary before including
## Credits
Built on [kobopatch](https://github.com/pgaskin/kobopatch) and [NickelMenu](https://pgaskin.net/NickelMenu/) by pgaskin. Uses [JSZip](https://stuk.github.io/jszip/) for client-side ZIP handling. Software patches and discussion on the [MobileRead forums](https://www.mobileread.com/forums/forumdisplay.php?f=247).
Built on [kobopatch](https://github.com/pgaskin/kobopatch) and [NickelMenu](https://pgaskin.net/NickelMenu/) by pgaskin. Uses [JSZip](https://stuk.github.io/jszip/) for client-side ZIP handling and [esbuild](https://esbuild.github.io/) for bundling. Software patches and discussion on the [MobileRead forums](https://www.mobileread.com/forums/forumdisplay.php?f=247).
## License

View File

@@ -8,26 +8,14 @@ if [ ! -d "$SCRIPT_DIR/kobopatch-src" ]; then
exit 1
fi
PUBLIC_DIR="$SCRIPT_DIR/../web/public"
echo "Building kobopatch WASM..."
cd "$SCRIPT_DIR"
GOOS=js GOARCH=wasm go build -o kobopatch.wasm .
echo "WASM binary size: $(du -h kobopatch.wasm | cut -f1)"
# Cache-busting timestamp
TS=$(date +%s)
echo "Copying artifacts..."
cp kobopatch.wasm "$SCRIPT_DIR/../web/src/wasm/kobopatch.wasm"
cp wasm_exec.js "$SCRIPT_DIR/../web/src/js/wasm_exec.js"
echo "Copying artifacts to $PUBLIC_DIR..."
mkdir -p "$PUBLIC_DIR/wasm"
cp kobopatch.wasm "$PUBLIC_DIR/wasm/kobopatch.wasm"
cp wasm_exec.js "$PUBLIC_DIR/js/wasm_exec.js"
# Update cache-busting timestamps
sed -i "s|kobopatch\.wasm?ts=[0-9]*|kobopatch.wasm?ts=$TS|g" "$PUBLIC_DIR/js/patch-worker.js"
sed -i "s|\.js?ts=[0-9]*|.js?ts=$TS|g" "$PUBLIC_DIR/index.html"
sed -i "s|\.css?ts=[0-9]*|.css?ts=$TS|g" "$PUBLIC_DIR/index.html"
echo "Build timestamp: $TS"
echo "Done."

View File

@@ -33,7 +33,7 @@ func TestIntegrationPatch(t *testing.T) {
}
// Read patch files from the patches zip.
patchesZipPath := "../web/public/patches/patches_4.45.23646.zip"
patchesZipPath := "../web/src/patches/patches_4.45.23646.zip"
patchesZip, err := os.ReadFile(patchesZipPath)
if err != nil {
t.Fatalf("could not read patches zip: %v", err)
@@ -82,9 +82,9 @@ overrides:
// with only "Remove footer (row3) on new home screen" enabled.
expectedSHA1 := map[string]string{
"usr/local/Kobo/libnickel.so.1.0.0": "ef64782895a47ac85f0829f06fffa4816d23512d",
"usr/local/Kobo/nickel": "80a607bac515457a6864be8be831df631a01005c",
"usr/local/Kobo/libadobe.so": "02dc99c71c4fef75401cd49ddc2e63f928a126e1",
"usr/local/Kobo/librmsdk.so.1.0.0": "e3819260c9fc539a53db47e9d3fe600ec11633d5",
"usr/local/Kobo/nickel": "80a607bac515457a6864be8be831df631a01005c",
"usr/local/Kobo/libadobe.so": "02dc99c71c4fef75401cd49ddc2e63f928a126e1",
"usr/local/Kobo/librmsdk.so.1.0.0": "e3819260c9fc539a53db47e9d3fe600ec11633d5",
}
// Extract the output tgz and check SHA1 of each patched binary.

View File

@@ -2,7 +2,7 @@
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PUBLIC_DIR="$SCRIPT_DIR/../web/public/nickelmenu"
PUBLIC_DIR="$SCRIPT_DIR/../web/src/nickelmenu"
mkdir -p "$PUBLIC_DIR"

View File

@@ -1,7 +1,7 @@
providers = ["python"]
[phases.setup]
nixPkgs = ["git", "curl", "python3Minimal", "zip", "gnutar"]
nixPkgs = ["git", "curl", "python3Minimal", "zip", "gnutar", "nodejs_22"]
paths = ["/usr/local/go/bin"]
cmds = [
"curl -sSfL https://go.dev/dl/go1.23.12.linux-amd64.tar.gz | tar -xz -C /usr/local",
@@ -12,7 +12,8 @@ cmds = [
"cd kobopatch-wasm && bash setup.sh",
"cd kobopatch-wasm && bash build.sh",
"cd nickelmenu && bash setup.sh",
"cd web && npm install && npm run build",
]
[start]
cmd = "python3 -m http.server ${PORT:-8080} -d web/public"
cmd = "python3 -m http.server ${PORT:-8080} -d web/dist"

View File

@@ -3,13 +3,19 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
WASM_DIR="$SCRIPT_DIR/kobopatch-wasm"
DIST_DIR="$SCRIPT_DIR/web/dist"
if [ ! -f "$SCRIPT_DIR/web/public/nickelmenu/NickelMenu.zip" ]; then
if [ ! -f "$DIST_DIR/nickelmenu/NickelMenu.zip" ]; then
echo "NickelMenu assets not found, downloading..."
"$SCRIPT_DIR/nickelmenu/setup.sh"
fi
if [ ! -f "$SCRIPT_DIR/web/public/wasm/kobopatch.wasm" ]; then
echo "Building JS bundle..."
cd "$SCRIPT_DIR/web"
npm install --silent
npm run build
if [ ! -f "$DIST_DIR/wasm/kobopatch.wasm" ]; then
echo "WASM binary not found, building..."
if [ ! -d "$WASM_DIR/kobopatch-src" ]; then
"$WASM_DIR/setup.sh"
@@ -18,4 +24,4 @@ if [ ! -f "$SCRIPT_DIR/web/public/wasm/kobopatch.wasm" ]; then
fi
echo "Serving at http://localhost:8888"
python3 -m http.server -d "$SCRIPT_DIR/web/public/" 8888
python3 -m http.server -d "$DIST_DIR" 8888

View File

@@ -18,7 +18,7 @@ const EXPECTED_SHA1 = {
const FIRMWARE_PATH = process.env.FIRMWARE_ZIP
|| path.resolve(__dirname, '..', '..', 'kobopatch-wasm', 'testdata', 'kobo-update-4.45.23646.zip');
const WEBROOT = path.resolve(__dirname, '..', '..', 'web', 'public');
const WEBROOT = path.resolve(__dirname, '..', '..', 'web', 'dist');
const WEBROOT_FIRMWARE = path.join(WEBROOT, '_test_firmware.zip');
// SHA1 of the original unmodified KoboRoot.tgz inside firmware 4.45.23646.

View File

@@ -17,7 +17,7 @@ module.exports = defineConfig({
},
},
webServer: {
command: 'python3 -m http.server -d ../../web/public 8889',
command: 'cd ../../kobopatch-wasm && bash build.sh && cd ../web && node build.mjs && python3 -m http.server -d dist 8889',
port: 8889,
reuseExistingServer: true,
},

View File

@@ -12,7 +12,7 @@ set -euo pipefail
# Prerequisites:
# - kobopatch.wasm built (run kobopatch-wasm/build.sh first)
# - Firmware zip cached at kobopatch-wasm/testdata/ (downloaded automatically)
# - NickelMenu assets in web/public/nickelmenu/ (set up automatically)
# - NickelMenu assets in web/src/nickelmenu/ (set up automatically)
cd "$(dirname "$0")"
@@ -46,13 +46,13 @@ FIRMWARE_DIR="../../kobopatch-wasm/testdata"
FIRMWARE_FILE="${FIRMWARE_DIR}/kobo-update-${FIRMWARE_VERSION}.zip"
# Check WASM is built.
if [ ! -f "../../web/public/wasm/kobopatch.wasm" ]; then
if [ ! -f "../../web/dist/wasm/kobopatch.wasm" ]; then
echo "ERROR: kobopatch.wasm not found. Run kobopatch-wasm/build.sh first."
exit 1
fi
# Set up NickelMenu assets if not present.
NM_DIR="../../web/public/nickelmenu"
NM_DIR="../../web/src/nickelmenu"
if [ ! -f "$NM_DIR/NickelMenu.zip" ] || [ ! -f "$NM_DIR/kobo-config.zip" ]; then
echo "Setting up NickelMenu assets..."
../../nickelmenu/setup.sh

115
web/build.mjs Normal file
View File

@@ -0,0 +1,115 @@
import esbuild from 'esbuild';
import { cpSync, mkdirSync, readFileSync, writeFileSync, existsSync, rmSync, readdirSync, statSync } from 'fs';
import { join, relative } from 'path';
import { createHash } from 'crypto';
const webDir = import.meta.dirname;
const srcDir = join(webDir, 'src');
const distDir = join(webDir, 'dist');
const isDev = process.argv.includes('--dev');
// Clean dist/
if (existsSync(distDir)) rmSync(distDir, { recursive: true });
mkdirSync(distDir, { recursive: true });
// Build JS bundle
await esbuild.build({
entryPoints: [join(srcDir, 'js', 'app.js')],
bundle: true,
format: 'iife',
target: ['es2020'],
outfile: join(distDir, 'bundle.js'),
minify: !isDev,
sourcemap: isDev,
logLevel: 'warning',
});
// Copy all of src/ to dist/, skipping js/ (bundled separately) and index.html (generated)
function copyDir(src, dst, skip = new Set()) {
mkdirSync(dst, { recursive: true });
for (const entry of readdirSync(src)) {
if (skip.has(entry)) continue;
const srcPath = join(src, entry);
const dstPath = join(dst, entry);
if (statSync(srcPath).isDirectory()) {
copyDir(srcPath, dstPath);
} else {
cpSync(srcPath, dstPath);
}
}
}
copyDir(srcDir, distDir, new Set(['js', 'index.html']));
// Copy worker files from src/js/ (not bundled, served separately)
mkdirSync(join(distDir, 'js'), { recursive: true });
// Copy wasm_exec.js as-is
const wasmExecSrc = join(srcDir, 'js', 'wasm_exec.js');
if (existsSync(wasmExecSrc)) {
cpSync(wasmExecSrc, join(distDir, 'js', 'wasm_exec.js'));
}
// Copy patch-worker.js with WASM hash injected
const workerSrc = join(srcDir, 'js', 'patch-worker.js');
if (existsSync(workerSrc)) {
let workerContent = readFileSync(workerSrc, 'utf-8');
const wasmFile = join(distDir, 'wasm', 'kobopatch.wasm');
if (existsSync(wasmFile)) {
const wasmHash = createHash('md5').update(readFileSync(wasmFile)).digest('hex').slice(0, 8);
workerContent = workerContent.replace(
"kobopatch.wasm'",
`kobopatch.wasm?h=${wasmHash}'`
);
}
writeFileSync(join(distDir, 'js', 'patch-worker.js'), workerContent);
}
// Generate cache-busted index.html
const bundleContent = readFileSync(join(distDir, 'bundle.js'));
const bundleHash = createHash('md5').update(bundleContent).digest('hex').slice(0, 8);
const cssContent = readFileSync(join(distDir, 'css/style.css'));
const cssHash = createHash('md5').update(cssContent).digest('hex').slice(0, 8);
let html = readFileSync(join(srcDir, 'index.html'), 'utf-8');
// Remove all <script src="js/..."> tags
html = html.replace(/\s*<script src="js\/[^"]*"><\/script>\n/g, '');
// Add the bundle script before </body>
html = html.replace(
'</body>',
` <script src="/bundle.js?h=${bundleHash}"></script>\n</body>`
);
// Update CSS cache bust
html = html.replace(
/css\/style\.css\?[^"]*/,
`css/style.css?h=${cssHash}`
);
writeFileSync(join(distDir, 'index.html'), html);
console.log(`Built to ${distDir} (bundle: ${bundleHash}, css: ${cssHash})`);
// Dev server mode
if (isDev) {
const ctx = await esbuild.context({
entryPoints: [join(srcDir, 'js', 'app.js')],
bundle: true,
format: 'iife',
target: ['es2020'],
outfile: join(distDir, 'bundle.js'),
minify: false,
sourcemap: true,
logLevel: 'warning',
});
await ctx.watch();
const { host, port } = await ctx.serve({
servedir: distDir,
port: 8889,
});
console.log(`Dev server running at http://${host}:${port}`);
}

581
web/package-lock.json generated Normal file
View File

@@ -0,0 +1,581 @@
{
"name": "kobopatch-webui",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "kobopatch-webui",
"dependencies": {
"jszip": "^3.10.1"
},
"devDependencies": {
"esbuild": "^0.24.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz",
"integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz",
"integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz",
"integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz",
"integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz",
"integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz",
"integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz",
"integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz",
"integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz",
"integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz",
"integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz",
"integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz",
"integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz",
"integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz",
"integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz",
"integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz",
"integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz",
"integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz",
"integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz",
"integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz",
"integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz",
"integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz",
"integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz",
"integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz",
"integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz",
"integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==",
"license": "MIT"
},
"node_modules/esbuild": {
"version": "0.24.2",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz",
"integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.24.2",
"@esbuild/android-arm": "0.24.2",
"@esbuild/android-arm64": "0.24.2",
"@esbuild/android-x64": "0.24.2",
"@esbuild/darwin-arm64": "0.24.2",
"@esbuild/darwin-x64": "0.24.2",
"@esbuild/freebsd-arm64": "0.24.2",
"@esbuild/freebsd-x64": "0.24.2",
"@esbuild/linux-arm": "0.24.2",
"@esbuild/linux-arm64": "0.24.2",
"@esbuild/linux-ia32": "0.24.2",
"@esbuild/linux-loong64": "0.24.2",
"@esbuild/linux-mips64el": "0.24.2",
"@esbuild/linux-ppc64": "0.24.2",
"@esbuild/linux-riscv64": "0.24.2",
"@esbuild/linux-s390x": "0.24.2",
"@esbuild/linux-x64": "0.24.2",
"@esbuild/netbsd-arm64": "0.24.2",
"@esbuild/netbsd-x64": "0.24.2",
"@esbuild/openbsd-arm64": "0.24.2",
"@esbuild/openbsd-x64": "0.24.2",
"@esbuild/sunos-x64": "0.24.2",
"@esbuild/win32-arm64": "0.24.2",
"@esbuild/win32-ia32": "0.24.2",
"@esbuild/win32-x64": "0.24.2"
}
},
"node_modules/immediate": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
"integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
"license": "MIT"
},
"node_modules/inherits": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==",
"license": "MIT"
},
"node_modules/jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
"integrity": "sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==",
"license": "(MIT OR GPL-3.0-or-later)",
"dependencies": {
"lie": "~3.3.0",
"pako": "~1.0.2",
"readable-stream": "~2.3.6",
"setimmediate": "^1.0.5"
}
},
"node_modules/lie": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
"integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
"license": "MIT",
"dependencies": {
"immediate": "~3.0.5"
}
},
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
"integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==",
"license": "(MIT AND Zlib)"
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
"license": "MIT"
},
"node_modules/readable-stream": {
"version": "2.3.8",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz",
"integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==",
"license": "MIT",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==",
"license": "MIT"
},
"node_modules/setimmediate": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
"integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==",
"license": "MIT"
},
"node_modules/string_decoder": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz",
"integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "~5.1.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==",
"license": "MIT"
}
}
}

14
web/package.json Normal file
View File

@@ -0,0 +1,14 @@
{
"name": "kobopatch-webui",
"private": true,
"scripts": {
"build": "node build.mjs",
"dev": "node build.mjs --dev"
},
"devDependencies": {
"esbuild": "^0.24.0"
},
"dependencies": {
"jszip": "^3.10.1"
}
}

File diff suppressed because one or more lines are too long

View File

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.0 KiB

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 3.9 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

View File

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 6.5 KiB

View File

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 16 KiB

View File

@@ -28,7 +28,7 @@
@keyframes spin { to { transform: rotate(360deg); } }
[hidden] { display: none !important; }
</style>
<link rel="stylesheet" href="css/style.css?ts=1773916731">
<link rel="stylesheet" href="css/style.css">
<script src="js/jszip.min.js"></script>
</head>
<body>
@@ -443,12 +443,5 @@
</div>
</div>
</dialog>
<!-- wasm_exec.js loaded by patch-worker.js inside the Web Worker -->
<script src="js/kobo-device.js?ts=1773916731"></script>
<script src="js/kobopatch.js?ts=1773916731"></script>
<script src="js/patch-ui.js?ts=1773916731"></script>
<script src="js/nickelmenu.js?ts=1773916731"></script>
<script src="js/app.js?ts=1773916731"></script>
</body>
</html>

View File

@@ -1,3 +1,9 @@
import { KoboDevice, KOBO_MODELS, getFirmwareURL, getDevicesForVersion } from './kobo-device.js';
import { PatchUI, scanAvailablePatches } from './patch-ui.js';
import { KobopatchRunner } from './kobopatch.js';
import { NickelMenuInstaller } from './nickelmenu.js';
import JSZip from 'jszip';
(() => {
const device = new KoboDevice();
const patchUI = new PatchUI();

View File

@@ -217,3 +217,9 @@ class KoboDevice {
this.deviceInfo = null;
}
}
// Expose on window for E2E test compatibility (tests access these via page.evaluate)
window.KoboDevice = KoboDevice;
window.FIRMWARE_DOWNLOADS = FIRMWARE_DOWNLOADS;
export { KOBO_MODELS, FIRMWARE_DOWNLOADS, getFirmwareURL, getDevicesForVersion, KoboDevice };

View File

@@ -51,3 +51,5 @@ class KobopatchRunner {
});
}
}
export { KobopatchRunner };

View File

@@ -1,3 +1,5 @@
import JSZip from 'jszip';
/**
* NickelMenu installer module.
*
@@ -193,3 +195,5 @@ class NickelMenuInstaller {
return result;
}
}
export { NickelMenuInstaller };

View File

@@ -1,3 +1,5 @@
import JSZip from 'jszip';
/**
* Friendly display names for patch files.
*/
@@ -456,3 +458,5 @@ class PatchUI {
}
}
export { PATCH_FILE_LABELS, parsePatchYAML, parsePatchConfig, scanAvailablePatches, PatchUI };

View File

@@ -10,7 +10,7 @@ async function loadWasm() {
const go = new Go();
const result = await WebAssembly.instantiateStreaming(
fetch('../wasm/kobopatch.wasm?ts=1773916731'),
fetch('../wasm/kobopatch.wasm'),
go.importObject
);
go.run(result.instance);

575
web/src/js/wasm_exec.js Normal file
View File

@@ -0,0 +1,575 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
"use strict";
(() => {
const enosys = () => {
const err = new Error("not implemented");
err.code = "ENOSYS";
return err;
};
if (!globalThis.fs) {
let outputBuf = "";
globalThis.fs = {
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1, O_DIRECTORY: -1 }, // unused
writeSync(fd, buf) {
outputBuf += decoder.decode(buf);
const nl = outputBuf.lastIndexOf("\n");
if (nl != -1) {
console.log(outputBuf.substring(0, nl));
outputBuf = outputBuf.substring(nl + 1);
}
return buf.length;
},
write(fd, buf, offset, length, position, callback) {
if (offset !== 0 || length !== buf.length || position !== null) {
callback(enosys());
return;
}
const n = this.writeSync(fd, buf);
callback(null, n);
},
chmod(path, mode, callback) { callback(enosys()); },
chown(path, uid, gid, callback) { callback(enosys()); },
close(fd, callback) { callback(enosys()); },
fchmod(fd, mode, callback) { callback(enosys()); },
fchown(fd, uid, gid, callback) { callback(enosys()); },
fstat(fd, callback) { callback(enosys()); },
fsync(fd, callback) { callback(null); },
ftruncate(fd, length, callback) { callback(enosys()); },
lchown(path, uid, gid, callback) { callback(enosys()); },
link(path, link, callback) { callback(enosys()); },
lstat(path, callback) { callback(enosys()); },
mkdir(path, perm, callback) { callback(enosys()); },
open(path, flags, mode, callback) { callback(enosys()); },
read(fd, buffer, offset, length, position, callback) { callback(enosys()); },
readdir(path, callback) { callback(enosys()); },
readlink(path, callback) { callback(enosys()); },
rename(from, to, callback) { callback(enosys()); },
rmdir(path, callback) { callback(enosys()); },
stat(path, callback) { callback(enosys()); },
symlink(path, link, callback) { callback(enosys()); },
truncate(path, length, callback) { callback(enosys()); },
unlink(path, callback) { callback(enosys()); },
utimes(path, atime, mtime, callback) { callback(enosys()); },
};
}
if (!globalThis.process) {
globalThis.process = {
getuid() { return -1; },
getgid() { return -1; },
geteuid() { return -1; },
getegid() { return -1; },
getgroups() { throw enosys(); },
pid: -1,
ppid: -1,
umask() { throw enosys(); },
cwd() { throw enosys(); },
chdir() { throw enosys(); },
}
}
if (!globalThis.path) {
globalThis.path = {
resolve(...pathSegments) {
return pathSegments.join("/");
}
}
}
if (!globalThis.crypto) {
throw new Error("globalThis.crypto is not available, polyfill required (crypto.getRandomValues only)");
}
if (!globalThis.performance) {
throw new Error("globalThis.performance is not available, polyfill required (performance.now only)");
}
if (!globalThis.TextEncoder) {
throw new Error("globalThis.TextEncoder is not available, polyfill required");
}
if (!globalThis.TextDecoder) {
throw new Error("globalThis.TextDecoder is not available, polyfill required");
}
const encoder = new TextEncoder("utf-8");
const decoder = new TextDecoder("utf-8");
globalThis.Go = class {
constructor() {
this.argv = ["js"];
this.env = {};
this.exit = (code) => {
if (code !== 0) {
console.warn("exit code:", code);
}
};
this._exitPromise = new Promise((resolve) => {
this._resolveExitPromise = resolve;
});
this._pendingEvent = null;
this._scheduledTimeouts = new Map();
this._nextCallbackTimeoutID = 1;
const setInt64 = (addr, v) => {
this.mem.setUint32(addr + 0, v, true);
this.mem.setUint32(addr + 4, Math.floor(v / 4294967296), true);
}
const setInt32 = (addr, v) => {
this.mem.setUint32(addr + 0, v, true);
}
const getInt64 = (addr) => {
const low = this.mem.getUint32(addr + 0, true);
const high = this.mem.getInt32(addr + 4, true);
return low + high * 4294967296;
}
const loadValue = (addr) => {
const f = this.mem.getFloat64(addr, true);
if (f === 0) {
return undefined;
}
if (!isNaN(f)) {
return f;
}
const id = this.mem.getUint32(addr, true);
return this._values[id];
}
const storeValue = (addr, v) => {
const nanHead = 0x7FF80000;
if (typeof v === "number" && v !== 0) {
if (isNaN(v)) {
this.mem.setUint32(addr + 4, nanHead, true);
this.mem.setUint32(addr, 0, true);
return;
}
this.mem.setFloat64(addr, v, true);
return;
}
if (v === undefined) {
this.mem.setFloat64(addr, 0, true);
return;
}
let id = this._ids.get(v);
if (id === undefined) {
id = this._idPool.pop();
if (id === undefined) {
id = this._values.length;
}
this._values[id] = v;
this._goRefCounts[id] = 0;
this._ids.set(v, id);
}
this._goRefCounts[id]++;
let typeFlag = 0;
switch (typeof v) {
case "object":
if (v !== null) {
typeFlag = 1;
}
break;
case "string":
typeFlag = 2;
break;
case "symbol":
typeFlag = 3;
break;
case "function":
typeFlag = 4;
break;
}
this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
this.mem.setUint32(addr, id, true);
}
const loadSlice = (addr) => {
const array = getInt64(addr + 0);
const len = getInt64(addr + 8);
return new Uint8Array(this._inst.exports.mem.buffer, array, len);
}
const loadSliceOfValues = (addr) => {
const array = getInt64(addr + 0);
const len = getInt64(addr + 8);
const a = new Array(len);
for (let i = 0; i < len; i++) {
a[i] = loadValue(array + i * 8);
}
return a;
}
const loadString = (addr) => {
const saddr = getInt64(addr + 0);
const len = getInt64(addr + 8);
return decoder.decode(new DataView(this._inst.exports.mem.buffer, saddr, len));
}
const testCallExport = (a, b) => {
this._inst.exports.testExport0();
return this._inst.exports.testExport(a, b);
}
const timeOrigin = Date.now() - performance.now();
this.importObject = {
_gotest: {
add: (a, b) => a + b,
callExport: testCallExport,
},
gojs: {
// Go's SP does not change as long as no Go code is running. Some operations (e.g. calls, getters and setters)
// may synchronously trigger a Go event handler. This makes Go code get executed in the middle of the imported
// function. A goroutine can switch to a new stack if the current stack is too small (see morestack function).
// This changes the SP, thus we have to update the SP used by the imported function.
// func wasmExit(code int32)
"runtime.wasmExit": (sp) => {
sp >>>= 0;
const code = this.mem.getInt32(sp + 8, true);
this.exited = true;
delete this._inst;
delete this._values;
delete this._goRefCounts;
delete this._ids;
delete this._idPool;
this.exit(code);
},
// func wasmWrite(fd uintptr, p unsafe.Pointer, n int32)
"runtime.wasmWrite": (sp) => {
sp >>>= 0;
const fd = getInt64(sp + 8);
const p = getInt64(sp + 16);
const n = this.mem.getInt32(sp + 24, true);
fs.writeSync(fd, new Uint8Array(this._inst.exports.mem.buffer, p, n));
},
// func resetMemoryDataView()
"runtime.resetMemoryDataView": (sp) => {
sp >>>= 0;
this.mem = new DataView(this._inst.exports.mem.buffer);
},
// func nanotime1() int64
"runtime.nanotime1": (sp) => {
sp >>>= 0;
setInt64(sp + 8, (timeOrigin + performance.now()) * 1000000);
},
// func walltime() (sec int64, nsec int32)
"runtime.walltime": (sp) => {
sp >>>= 0;
const msec = (new Date).getTime();
setInt64(sp + 8, msec / 1000);
this.mem.setInt32(sp + 16, (msec % 1000) * 1000000, true);
},
// func scheduleTimeoutEvent(delay int64) int32
"runtime.scheduleTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this._nextCallbackTimeoutID;
this._nextCallbackTimeoutID++;
this._scheduledTimeouts.set(id, setTimeout(
() => {
this._resume();
while (this._scheduledTimeouts.has(id)) {
// for some reason Go failed to register the timeout event, log and try again
// (temporary workaround for https://github.com/golang/go/issues/28975)
console.warn("scheduleTimeoutEvent: missed timeout event");
this._resume();
}
},
getInt64(sp + 8),
));
this.mem.setInt32(sp + 16, id, true);
},
// func clearTimeoutEvent(id int32)
"runtime.clearTimeoutEvent": (sp) => {
sp >>>= 0;
const id = this.mem.getInt32(sp + 8, true);
clearTimeout(this._scheduledTimeouts.get(id));
this._scheduledTimeouts.delete(id);
},
// func getRandomData(r []byte)
"runtime.getRandomData": (sp) => {
sp >>>= 0;
crypto.getRandomValues(loadSlice(sp + 8));
},
// func finalizeRef(v ref)
"syscall/js.finalizeRef": (sp) => {
sp >>>= 0;
const id = this.mem.getUint32(sp + 8, true);
this._goRefCounts[id]--;
if (this._goRefCounts[id] === 0) {
const v = this._values[id];
this._values[id] = null;
this._ids.delete(v);
this._idPool.push(id);
}
},
// func stringVal(value string) ref
"syscall/js.stringVal": (sp) => {
sp >>>= 0;
storeValue(sp + 24, loadString(sp + 8));
},
// func valueGet(v ref, p string) ref
"syscall/js.valueGet": (sp) => {
sp >>>= 0;
const result = Reflect.get(loadValue(sp + 8), loadString(sp + 16));
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 32, result);
},
// func valueSet(v ref, p string, x ref)
"syscall/js.valueSet": (sp) => {
sp >>>= 0;
Reflect.set(loadValue(sp + 8), loadString(sp + 16), loadValue(sp + 32));
},
// func valueDelete(v ref, p string)
"syscall/js.valueDelete": (sp) => {
sp >>>= 0;
Reflect.deleteProperty(loadValue(sp + 8), loadString(sp + 16));
},
// func valueIndex(v ref, i int) ref
"syscall/js.valueIndex": (sp) => {
sp >>>= 0;
storeValue(sp + 24, Reflect.get(loadValue(sp + 8), getInt64(sp + 16)));
},
// valueSetIndex(v ref, i int, x ref)
"syscall/js.valueSetIndex": (sp) => {
sp >>>= 0;
Reflect.set(loadValue(sp + 8), getInt64(sp + 16), loadValue(sp + 24));
},
// func valueCall(v ref, m string, args []ref) (ref, bool)
"syscall/js.valueCall": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const m = Reflect.get(v, loadString(sp + 16));
const args = loadSliceOfValues(sp + 32);
const result = Reflect.apply(m, v, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 56, result);
this.mem.setUint8(sp + 64, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 56, err);
this.mem.setUint8(sp + 64, 0);
}
},
// func valueInvoke(v ref, args []ref) (ref, bool)
"syscall/js.valueInvoke": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const args = loadSliceOfValues(sp + 16);
const result = Reflect.apply(v, undefined, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, result);
this.mem.setUint8(sp + 48, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, err);
this.mem.setUint8(sp + 48, 0);
}
},
// func valueNew(v ref, args []ref) (ref, bool)
"syscall/js.valueNew": (sp) => {
sp >>>= 0;
try {
const v = loadValue(sp + 8);
const args = loadSliceOfValues(sp + 16);
const result = Reflect.construct(v, args);
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, result);
this.mem.setUint8(sp + 48, 1);
} catch (err) {
sp = this._inst.exports.getsp() >>> 0; // see comment above
storeValue(sp + 40, err);
this.mem.setUint8(sp + 48, 0);
}
},
// func valueLength(v ref) int
"syscall/js.valueLength": (sp) => {
sp >>>= 0;
setInt64(sp + 16, parseInt(loadValue(sp + 8).length));
},
// valuePrepareString(v ref) (ref, int)
"syscall/js.valuePrepareString": (sp) => {
sp >>>= 0;
const str = encoder.encode(String(loadValue(sp + 8)));
storeValue(sp + 16, str);
setInt64(sp + 24, str.length);
},
// valueLoadString(v ref, b []byte)
"syscall/js.valueLoadString": (sp) => {
sp >>>= 0;
const str = loadValue(sp + 8);
loadSlice(sp + 16).set(str);
},
// func valueInstanceOf(v ref, t ref) bool
"syscall/js.valueInstanceOf": (sp) => {
sp >>>= 0;
this.mem.setUint8(sp + 24, (loadValue(sp + 8) instanceof loadValue(sp + 16)) ? 1 : 0);
},
// func copyBytesToGo(dst []byte, src ref) (int, bool)
"syscall/js.copyBytesToGo": (sp) => {
sp >>>= 0;
const dst = loadSlice(sp + 8);
const src = loadValue(sp + 32);
if (!(src instanceof Uint8Array || src instanceof Uint8ClampedArray)) {
this.mem.setUint8(sp + 48, 0);
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(sp + 40, toCopy.length);
this.mem.setUint8(sp + 48, 1);
},
// func copyBytesToJS(dst ref, src []byte) (int, bool)
"syscall/js.copyBytesToJS": (sp) => {
sp >>>= 0;
const dst = loadValue(sp + 8);
const src = loadSlice(sp + 16);
if (!(dst instanceof Uint8Array || dst instanceof Uint8ClampedArray)) {
this.mem.setUint8(sp + 48, 0);
return;
}
const toCopy = src.subarray(0, dst.length);
dst.set(toCopy);
setInt64(sp + 40, toCopy.length);
this.mem.setUint8(sp + 48, 1);
},
"debug": (value) => {
console.log(value);
},
}
};
}
async run(instance) {
if (!(instance instanceof WebAssembly.Instance)) {
throw new Error("Go.run: WebAssembly.Instance expected");
}
this._inst = instance;
this.mem = new DataView(this._inst.exports.mem.buffer);
this._values = [ // JS values that Go currently has references to, indexed by reference id
NaN,
0,
null,
true,
false,
globalThis,
this,
];
this._goRefCounts = new Array(this._values.length).fill(Infinity); // number of references that Go has to a JS value, indexed by reference id
this._ids = new Map([ // mapping from JS values to reference ids
[0, 1],
[null, 2],
[true, 3],
[false, 4],
[globalThis, 5],
[this, 6],
]);
this._idPool = []; // unused ids that have been garbage collected
this.exited = false; // whether the Go program has exited
// Pass command line arguments and environment variables to WebAssembly by writing them to the linear memory.
let offset = 4096;
const strPtr = (str) => {
const ptr = offset;
const bytes = encoder.encode(str + "\0");
new Uint8Array(this.mem.buffer, offset, bytes.length).set(bytes);
offset += bytes.length;
if (offset % 8 !== 0) {
offset += 8 - (offset % 8);
}
return ptr;
};
const argc = this.argv.length;
const argvPtrs = [];
this.argv.forEach((arg) => {
argvPtrs.push(strPtr(arg));
});
argvPtrs.push(0);
const keys = Object.keys(this.env).sort();
keys.forEach((key) => {
argvPtrs.push(strPtr(`${key}=${this.env[key]}`));
});
argvPtrs.push(0);
const argv = offset;
argvPtrs.forEach((ptr) => {
this.mem.setUint32(offset, ptr, true);
this.mem.setUint32(offset + 4, 0, true);
offset += 8;
});
// The linker guarantees global data starts from at least wasmMinDataAddr.
// Keep in sync with cmd/link/internal/ld/data.go:wasmMinDataAddr.
const wasmMinDataAddr = 4096 + 8192;
if (offset >= wasmMinDataAddr) {
throw new Error("total length of command line and environment variables exceeds limit");
}
this._inst.exports.run(argc, argv);
if (this.exited) {
this._resolveExitPromise();
}
await this._exitPromise;
}
_resume() {
if (this.exited) {
throw new Error("Go program has already exited");
}
this._inst.exports.resume();
if (this.exited) {
this._resolveExitPromise();
}
}
_makeFuncWrapper(id) {
const go = this;
return function () {
const event = { id: id, this: this, args: arguments };
go._pendingEvent = event;
go._resume();
return event.result;
};
}
}
})();

0
web/src/wasm/.gitkeep Normal file
View File