Building from Source
Electron 43, React 19, TypeScript 7, Vite 7, Tailwind 4.
git clone https://github.com/samaBR85/Valendo-TeleprompterSuite.git
cd Valendo-TeleprompterSuite
npm install
npm run dev
If npm install does not fetch the Electron binary, run node node_modules/electron/install.js.
On Windows, Abrir Valendo.bat does the install and the build on the first double-click, for people who do not have a terminal open.
Commands
| Command | What it does |
|---|---|
npm run dev | Development, with hot reload |
npm run build | Bundles main, preload and renderer into out/ |
npm test | The test suite |
npm run typecheck | tsc --noEmit |
npm run start:debug | Runs the built app with remote debugging on port 9222 |
npm run verify | Checks the acceptance criteria against the running app (needs start:debug) |
npm run dist:win | The Windows installer (NSIS) into dist/ |
npm run dist:mac | The macOS disk images, arm64 and x64, into dist/ |
dist:mac only works on macOS — Apple's signing tools do not exist on other systems. The release workflow builds each platform on its own runner.
Intel Macs
There is no Intel download. The app builds and runs on Intel macOS perfectly well — npm run dist:mac -- --x64 produces the disk image — but the build is not published, because GitHub's Intel macOS runners were never available: the job sat queued through three consecutive attempts without ever starting. An installer that cannot be produced reliably is a promise, not a release.
Reopening it is a two-line change in the workflow matrix, and the ffmpeg build script is already architecture-agnostic — it compiles for whatever machine it runs on.
Releases
.github/workflows/release.yml builds Windows and macOS arm64 whenever a v* tag is pushed, and attaches the installers to the release. macos-14 runners are Apple Silicon, which is where the arm64 disk image has to be built.
Running the workflow by hand (Actions → Release → Run workflow) builds the same artifacts without creating a release, and leaves them as downloadable job artifacts — useful for testing a build before deciding it is worth a tag.
The dist:mac build is signed ad-hoc: Apple Silicon refuses to launch a binary with no signature at all, but ad-hoc signing says nothing about who built it. A build you make yourself runs without a fuss, because nothing was downloaded to quarantine it. The .dmg on the download page is a separate build, signed with a Developer ID and notarized by Apple, so it opens with a double-click too.
Layout
src/main/ windows, monitors, authoritative state, persistence
src/preload/ the IPC bridge exposed to the renderer
src/shared/ pure, testable logic: anchor, lines, pacing, history, commands
src/renderer/ prompter (shared), operator interface, broadcast window
scripts/ end-to-end verification over the Chromium protocol
docs/ screenshots used in the README and this wiki
The shape of the thing
There is one authoritative state, in the main process, in src/main/state.ts. Renderers never hold their own copy of anything that matters; they dispatch actions and mirror what comes back.
src/shared has no Electron and no React. It is where the anchor, the line composition, the pacing arithmetic, the undo history and the command registry live, and it is where almost all the tests are — the logic can be proved without launching a window.
Tests
npm test
The suite covers the pure logic and the parts of the main process that can be reached with a mocked userData folder: the anchor after a reflow, line composition, pacing in the three modes, what does and does not travel inside a .valendo, migration of projects written by older versions, and the i18n dictionaries.
The i18n test is worth knowing about: it enforces that all six dictionaries carry exactly the same set of keys, that none is empty, and that {interpolation} marks survive translation. Forgetting one language breaks the build rather than shipping a blank label.
End-to-end checks
scripts/verify.mjs drives the running app over the Chromium DevTools Protocol: it launches with an isolated user-data folder, dispatches real input, and asserts against the real DOM. That is how the acceptance criteria are checked — including the one that matters most, that the word under the reading line does not move when the text above it changes.
npm run start:debug # in one terminal
npm run verify # in another
Versioning
The semantic version is a human decision, set by hand in package.json. The build number rises on its own with every npm run build, through scripts/bump-build.mjs, and shows in the app header and credits as vX.Y.Z - build N.
So a bug report that names an exact vX.Y.Z - build N points at one precise bundle, without anyone having to remember to bump a number before cutting a release.
Redistributed ffmpeg
The two installers carry different binaries, under different licenses.
Windows. ffmpeg 6.1.1 from gyan.dev, pinned in package-lock.json and fetched through ffmpeg-static. GPL 3.0, configured with --enable-gpl --enable-version3 --enable-libx264.
macOS. ffmpeg 6.1.1 compiled from source by this project's own CI, on every release, through scripts/build-ffmpeg-mac.sh. It drops libx264 and encodes through the system's own VideoToolbox instead, which keeps the result LGPL 2.1 — the Mac App Store does not accept GPL software. The script's own guard fails the build if --enable-gpl, --enable-version3 or --enable-nonfree show up in the result, or if the binary does not report itself as LGPL 2.1.
Run it directly to reproduce the macOS binary:
scripts/build-ffmpeg-mac.sh
It takes no arguments beyond an optional FFMPEG_VERSION (defaults to 6.1.1), and prints the resulting configuration: string and license notice at the end.
Either way, the corresponding source for both platforms is the official FFmpeg 6.1.1 release, unmodified: ffmpeg.org/download.html and the official repository, tag n6.1.1. Full license breakdown: Redistributed ffmpeg.
Because ffmpeg is a real executable, it is unpacked beside app.asar rather than inside it — an archived binary cannot be executed.
Contributing
Issues and pull requests are welcome. Two things to know before opening one:
- The comments in this codebase explain why, not what. A comment that restates the line below it will be asked to say something else.
- If a change touches the reading position, it needs a test. That is the one promise the whole app is built on.