Valendo

# 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

CommandWhat it does
npm run devDevelopment, with hot reload
npm run buildBundles main, preload and renderer into out/
npm testThe test suite
npm run typechecktsc --noEmit
npm run start:debugRuns the built app with remote debugging on port 9222
npm run verifyChecks the acceptance criteria against the running app (needs start:debug)
npm run dist:winThe Windows installer (NSIS) into dist/
npm run dist:macThe macOS disk images, arm64 and x64, into dist/

dist:mac only works on macOS — Apple's signing tools do not exist on other systems. There is no way around this; it is why 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 macOS build is signed ad-hoc, never notarised: the project has no paid Apple developer account. Ad-hoc signing is not optional — Apple Silicon refuses to launch a binary with no signature at all — but it does not satisfy Gatekeeper for a downloaded app, which is why Getting Started documents the unblock.

## 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 is deliberately free of Electron and of 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 and sits at 1.0.0 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 v1.0.0 - build N.

So a bug report that says "v1.0.0 - build 199" names an exact bundle, without anyone having to remember to bump a number before cutting a release.

## Redistributed ffmpeg

Both installers carry an ffmpeg, through ffmpeg-static, pinned in package-lock.json. It downloads a different binary per platform, from release b6.1.1 of eugeneware/ffmpeg-static, and each binary arrives with its own .LICENSE and .README beside it.

Windows. A build by gyan.dev, ffmpeg 6.1.1, stated in its README as GPL v3, configured with --enable-gpl --enable-version3 --enable-libx264.

macOS. The .LICENSE shipped with the arm64 binary is FFmpeg's own generic licence file: it explains that FFmpeg is LGPL v2.1+ unless --enable-gpl was passed, but it does not record what that particular build actually enabled. The flags are printed by the binary itself:

/Applications/Valendo.app/Contents/Resources/app.asar.unpacked/node_modules/ffmpeg-static/ffmpeg -version

The first two lines give the version and the full configuration: string.

Either way, the corresponding source for every platform is ffmpeg 6.1.1: ffmpeg.org/download.html and the official repository, tag n6.1.1. Offering it satisfies both licences, which is why this note points at it without waiting to learn which one applies on macOS.

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.