# Development Guide ## Unreleased ScratchJr blocks (issue #9) `waitrandom` takes a maximum of 0–50 tenths of a second. `ontouchsprite` stores a same-page character ID selected through a picture picker. Deleting the target clears references; undo/redo restores the matching script snapshots. These share the renderer across all hosts and require no IPC additions. Older versions may skip unknown blocks, so new-block projects are not promised backward playback compatibility. Practical knowledge for working on ScratchJr Reborn. Covers the build pipeline, the editor layout/limits map, the project file format, testing, and releasing. ## Build pipeline — read this first The renderer is bundled into **one file**: `src/app/dist/app.bundle.js` (esbuild, entry `src/app/renderer-entry.js`). Every HTML page loads **only** that bundle. - **The bundle is gitignored** — it is generated, never committed. A fresh checkout has no bundle, so `npm start` shows a blank app until you run `npm run build:renderer` once. - **`npm run make:zip` builds the bundle automatically** before packaging (`scripts/package-and-zip.js` runs `build-renderer.js` first). Do not remove that step — a release built without it ships an app that cannot load its UI (this actually happened in v1.5.0/v1.5.1 pre-fix). - After editing any renderer `.ts`, rebuild with `npm run build:renderer` before packaging or boot-verifying. - Recommended follow-up: run `build:renderer` from `postinstall` so fresh clones work out of the box. **Verifying a packaged build:** ```bash node scripts/smoke-packaged.js out/ScratchJr-win32-x64 # boots the packaged app, expects [SCRATCHJR_READY] # or check the bundle made it into the asar: node -e "const a=require('@electron/asar');const p='out/ScratchJr-win32-x64/resources/app.asar';console.log(a.extractFile(p,'src\\\\app\\\\dist\\\\app.bundle.js').length)" ``` ## CSS preprocessing CSS files are JS template literals (e.g. `${css_vh(10)}`), evaluated at **load time** by `preprocessAndLoadCss()` (`src/app/src/utils/lib.ts`). Consequences: - `css_vh(N)` → `N% of innerHeight` **in pixels, baked when the page loads**. It is not a live `vh` unit. - `calc(100% - …)` percentages resolve **live** against the containing block — the pattern used to adapt panels to the dynamic layout. - The editor layout is dynamic: `UI.resize()` sets panel heights to `clamp(0.57, 0.60) × docHeight` on wide windows (aspect > 1.45). Do not assume the CSS `61.2vh` panels are their final size. ## Editor layout & limits map | Limit / area | Where | Current value | |---|---|---| | Pages per project | `window.Settings.maxPages` read in `Thumbs.updatePages` | 8 (`src/app/settings.json`) | | Character sidebar height | `#library .spritethumbs` `max-height` | `calc(100% - 10.29vh-px)` — leaves room for the **+ button** | | Add-character (+) button | `.addsprite` sibling below `.spritethumbs` | always visible by construction (see below) | | Add-page (+) button | `#emptypage`, `position: sticky; bottom: 0` in `#pagecc` | pinned while the page list scrolls | **Hard boundary:** the bottom block palette (`#blockspalette`, z-index 10) starts exactly at the left panel's bottom and covers everything below it in the left column. Any left-column UI must end above that line — the character list is sized as `panel − 9.38vh (top) − 10.29vh (button)` so the + button always fits. **Character strip scrolling:** native `scrollTop` on `.spritethumbs` is the single source of truth. Drag scrolling (`UI.spriteScolling`), `spriteInView`, and the reset in `Thumbs.updateSprites` all write `scrollTop`; the custom scrollbar (`#sbthumb`) is synced from a `scroll` listener (`UI.updateSpriteScroll`). Do not reintroduce `style.top` scrolling — it broke wheel input and desynced the scrollbar. ## Project file format (typed contract) Defined in `src/app/src/editor/ui/Project.ts` and shared across the engine: - `ProjectData` — the project bag: `pages: string[]`, `currentPage`, one `PageData` per page id, plus merged undo-action fields - `PageData` — `lastSprite`, `sprites: string[]`, one `SpriteData` per sprite id - `SpriteData` — `id`, `type`, `scripts: EncodedStrip[]`, `sounds`, plus attribute passthrough - `EncodedStrip` — recursive `[blocktype, arg, dx, dy, (nested strips)?]` encoding Facts that matter: - A strip's blocks are linked via `.next`; `encodeStrip` walks the chain from the head — **one strip per script**, not per block. - `'null'` as an arg means "use the spec default" (e.g. `hop` re-encodes its default `2`). Encode with the *current* value for round-trip identity. - Undo snapshots are `Project.getUndo()` + the action descriptor — the bags are the file format; keep `Undo.ts` consumers on the shared types. ## Renderer testing (jsdom) `tests/unit/renderer-*.test.js` run in jsdom (`// @vitest-environment jsdom`) and must **import `./renderer-harness.js` first**. The harness stubs the jsdom gaps: `SVGMatrix`/`WebKitCSSMatrix`, canvas 2d context (fixed-width `measureText`), `img.complete`, `window.Settings`, and the preload/tablet bridges. Per-test setup: - `resetDom()` — fresh `#scriptscontainer`, `#pagesdiv`, stub `ScratchJr.stage` - `stubMedia()` — stub `PlatformBridge.getmedia` (sprite construction kicks off async media loads) - `BlockSpecs.initBlocks()` — populates block defs (module load does not) - Construct `Page` with data (`{ lastSprite: '', sprites: [], layers: [], num: 1 }`) — an empty constructor triggers `emptyPage()` → `createCat()` → `MediaLib` (unavailable in tests) Covered contracts: script-strip decode/re-encode identity, page-bag encode/decode, runtime primitives (`Prims.Home`/`SetSpeed`/`Show`/`Hide`), and `Thumbs.getPagePos` scroll math. ## Releasing ```bash npm version X.Y.Z --no-git-tag-version # bumps package.json + lockfile git commit -m "release: vX.Y.Z" git push origin master git tag vX.Y.Z && git push origin vX.Y.Z # CI builds all 6 targets + MSI + release ``` - The workflow matrix is **include-only** (5 build jobs). Do not switch back to an `os:` axis + `include` — multiple include entries sharing an os value silently collapse (x64 Linux/macOS jobs vanished when it was written that way). - `build:renderer` runs in the workflow before packaging — keep it. - Late fixes after a release: amend, push, then **delete + re-create the tag** (`git tag -d vX.Y.Z && git push origin :refs/tags/vX.Y.Z && git tag vX.Y.Z && git push origin vX.Y.Z`). The release step updates the existing release in place. - Cancelling a bad release: `gh run cancel ` (repo-owner auth). ## Building for Android ScratchJr Android uses a native Kotlin shell (`WebView` + `WebViewAssetLoader` + `@JavascriptInterface`). ### Prerequisites - JDK 17 - Android SDK (API 36 compile/target SDK, minSdk 24) - `ANDROID_HOME` pointing to your Android SDK directory ### Build steps ```bash # 1. Bundle web assets (esbuild target chrome107) and sync to Android asset staging npm run build:android # 2. Build Debug APK cd android ./gradlew assembleDebug # Or build Release APK / Google Play App Bundle ./gradlew assembleRelease bundleRelease ``` Generated packages: - Debug APK: `android/app/build/outputs/apk/debug/app-debug.apk` - Release APK: `android/app/build/outputs/apk/release/app-release-unsigned.apk` - App Bundle: `android/app/build/outputs/bundle/release/app-release.aab` ### Installing & Running on Device / Emulator ```bash # Install debug APK adb install -r android/app/build/outputs/apk/debug/app-debug.apk # Grant hardware permissions adb shell pm grant org.scratchjr.android.debug android.permission.CAMERA adb shell pm grant org.scratchjr.android.debug android.permission.RECORD_AUDIO # Launch application adb shell am start -n org.scratchjr.android.debug/org.scratchjr.android.MainActivity ``` ### Logging & Remote Debugging Filter logcat output by key component tags: ```bash adb logcat -s ScratchJr-JS AndroidBridge AndroidDatabaseManager MainActivity ``` - `ScratchJr-JS` — Console messages and errors from the WebView renderer. - `AndroidBridge` — JavascriptInterface bridge calls, hardware availability, audio, and sharing. - `AndroidDatabaseManager` — SQLite intent queries, statements, and media file CRUD. - `MainActivity` — Activity lifecycle, intent routing (`.sjr` files), permissions, and fullscreen. To inspect the live WebView DOM and JS console: ```bash $pid = (adb shell pidof org.scratchjr.android.debug).Trim() adb forward tcp:9222 "localabstract:webview_devtools_remote_$pid" # Open chrome://inspect in Chromium / Google Chrome ``` ## Web & PWA development ScratchJr Reborn includes a standalone browser-based distribution powered by `sql.js` (WebAssembly SQLite) with IndexedDB persistence, Service Worker offline caching, and PWA manifest. ### Build & run locally ```bash # 1. Compile web bundle and package dist-web/ npm run build:web # 2. Start local static dev server npm run serve:web # 3. Open browser at http://localhost:3000 # - Landing page: http://localhost:3000/ # - Web App: http://localhost:3000/play/ or http://localhost:3000/app/ ``` ### Automated Web Verification ```bash # Run headless Chrome CDP end-to-end smoke test node scripts/smoke-web.js ``` ## Desktop: Lightweight Tauri v2 Workflow ```bash # 1. Dev mode (runs build:renderer + launches Tauri window) npm run tauri:dev # 2. Build release binaries (MSI, NSIS installer, and portable exe) npm run tauri:build # 3. Package to out/tauri-win32-x64/ and generate release zip + SHA256SUMS npm run package:tauri ``` ## Known quirks & follow-ups - `src/types/globals.d.ts` expando declarations must stay mutually consistent (the `HTMLElement.next` vs `ChildNode.next` clash broke element assignability). - The character picker attaches both `tb.onmouseup` and `window.onmouseup` in `Library.selectAsset`; `clickMe` nulls them after the first call. Since v1.5.1-fix, `Library.close` resets `selectedOne`/`clickThumb` and detaches the picker's mouse handlers, and `closeSpriteSelection` guards on `ScratchJr.onHold` — a closed picker can no longer re-add characters via stale handlers or re-entrant adds (this was the "flood of characters on click" bug). - CI boot-verifies packaged artifacts with `scripts/smoke-packaged.js` (build-release.yml) — added to catch packaging regressions like the missing-bundle one. - `MediaLib.keys` is populated asynchronously at runtime — tests must not depend on it (use the `Page`-with-data construction above).