Development & Contributing
Contributing to the PanelWave Player — repository layout, building the library, the demo app, unit and E2E tests, CI checks, releases to npm, and the pull-request process.
The player is MIT-licensed and developed in the open at github.com/panelwave/player. Issues and pull requests are welcome. This page covers the developer workflow; for the internal design, start with Player Architecture. The repository's CONTRIBUTING.md is the short version of the same rules.
Repository layout
player/
├── angular.json # Angular CLI workspace (projects: player, demo, reader)
├── package.json # Workspace scripts and dev dependencies
├── CHANGELOG.md # What changed in each release
├── CONTRIBUTING.md
├── SAMPLE_PLUGIN.html # Runnable plugin example
├── e2e/ # Playwright specs (drive the demo app)
├── .github/workflows/ # ci, release, demo-pages, size-limit
├── projects/
│ ├── player/ # The library (published as @panelwave/player)
│ │ ├── ng-package.json # ng-packagr config → dist/player/
│ │ ├── package.json # Library package manifest (version, peer deps)
│ │ ├── README.md # The README shown on npm
│ │ └── src/
│ │ ├── public-api.ts # THE public API surface
│ │ ├── assets/ # Balloon fonts, UI i18n JSON (en, de)
│ │ └── lib/ # types/ utils/ services/ components/ entitlement/
│ ├── demo/ # Demo app (see Demo App)
│ └── reader/ # Public reader app, the read.panelwave.org frontend (see Reader app)
└── docs/ # Per-feature implementation notes and docs/API.md
The docs/ folder holds background notes per feature — useful, but the source code is authoritative when they disagree.
Setup
You need Node.js 24 (CI uses 24) with npm 11 — the lockfile is written by npm 11, and npm 10 rejects it in npm ci. The library is built with Angular 20 (partial compilation) with TypeScript ~5.8 and RxJS 7.8, and is consumed by Angular 20, 21 and 22 apps; runtime dependencies are deliberately small (json-logic-js for conditions, @ngx-translate/core 17 or 18 as a peer for UI strings — the repo builds and tests against 17).
To prove a host Angular version, run the consumer smoke test after a production build: npm run smoke:consumer -- --angular 22 (or 20 / 21; add --zone for a zone.js app instead of zoneless, --ngx-translate 17 to pin the translate major). It packs dist/player, generates a fresh app with that major's CLI, installs the tarball with strict peer resolution, wires it up as the package README describes, builds for production and renders the demo manifest in headless Chromium. CI runs it for every major in the peer range; when you change the range, change the consumer-smoke matrix in .github/workflows/ci.yml with it.
Clone and install
git clone https://github.com/panelwave/player.git
cd player
npm install
Build the library
ng build player
Builds via ng-packagr into dist/player/. Use ng build player --watch for incremental rebuilds.
Run the demo app
npm start
Serves the demo app at http://localhost:4200.
The demo resolves @panelwave/player from dist/player (a tsconfig path), not from the library sources. Rebuild the library after every library change — otherwise the demo and the E2E suite keep running the old code. Running ng build player --watch in a second terminal keeps them in sync.
Reader app
projects/reader is the full-window reader behind read.panelwave.org: a small Angular app that fetches a manifest and mounts <pw-player-shell> (toolbar hidden; the PanelWave icon in the bottom-right corner opens it). It is not part of the npm package.
- Boot config. In production, the reader server injects
window.__PW_READER__ = { manifestUrl, embed, locale, mode }intoindex.html(and renders the page<title>itself). Without alocale, the reader uses the manifest'smeta.default_locale. - Gating is the shell's own:
paywall.rulesand"x-locked": truestubs are evaluated for an anonymous reader, so paywall and age gates work as in any embed. - Review mode.
mode: 'review'(review links) cuts every paywall rule down to its age part before the manifest reaches the shell: purchase and subscription gates disappear, age gates stay — the age is never pre-verified.
For local development, a development build also reads the boot config from the query string (production builds ignore it). Like the demo, the reader imports the built library:
npx ng build player # the reader runs against dist/player
npx ng serve reader # http://localhost:4300/?manifest=<url>[&embed=1][&mode=review]
npx ng test reader --watch=false --browsers=ChromeHeadless
npx ng lint reader
npx ng build reader --base-href /
Testing
Unit tests
Jasmine + Karma, with .spec.ts files next to their sources:
ng test player # watch mode
ng test player --watch=false --browsers=ChromeHeadless # single run
ng test player --watch=false --browsers=ChromeHeadless --code-coverage # report in coverage/
On Windows, point Karma at the system Chrome: CHROME_BIN="C:\Program Files\Google\Chrome\Application\chrome.exe". Services with pure logic (FlowEngineService, VariableStoreService, the paywall evaluator, the utilities) are the most test-dense areas — extend the existing specs when you change behavior there.
End-to-end tests
Playwright drives the demo app, which the suite starts itself on port 4222. It is hermetic — remote images are stubbed — and uses the demo's URL parameters (?manifest=, ?deny=, ?vars=, ?device=, ?devtools=) to reach specific states.
ng build player # the demo runs against dist/player
npm run e2e # chromium + mobile (Pixel 7 emulation)
npm run e2e:all # + firefox + webkit
npm run e2e:ui # Playwright UI mode
Besides feature flows, the suite includes an accessibility sweep (axe over every dialog, overlay and view mode), reduced-motion and focus checks, a console-hygiene check (fails on any console error or warning), and heap-stability and frame-rate guards.
Linting and code style
ng lint player
The workspace uses angular-eslint with strict typescript-eslint rules plus Prettier, and lint is kept at zero problems for both the library and the demo. The rules that matter most:
- Standalone components,
ChangeDetectionStrategy.OnPush,pw-selector prefix,inject()over constructor injection. - No
anyin library code (specs may use it). - The manifest is untrusted input: never render manifest text as HTML, check ids against the manifest, evaluate conditions only through
evaluateJsonLogic. - Accessibility is enforced by lint: every clickable element is focusable and keyboard-operable; dialogs close on Escape and backdrop click; respect reduced motion.
Commits, branches and pull requests
- Conventional commits:
feat:,fix:,docs:,refactor:,test:,chore:, … (e.g.fix(viewport): keep hover nav arrows above the speech bubbles). - Branches:
feature/*,bugfix/*,hotfix/*offmaster.
Open the pull request against master
Describe the behavior change and how you verified it (which suites ran).
CI must be green
Lint, unit tests, the production library build and the E2E suite run on every pull request (see CI).
Note it in the changelog
Add a line under Unreleased in CHANGELOG.md for anything a library consumer would notice.
Review
One approving review from a maintainer; the maintainer merges.
Working on the public API
Everything a host can import comes from projects/player/src/public-api.ts. When you add a service, component, or utility that hosts should reach:
- Export the symbol (and its supporting types) from
public-api.tsand document it indocs/API.md. - Keep type definitions in
src/lib/types/aligned with@panelwave/typesand the schema — format changes start in the schema, then@panelwave/types, then the player, in one release train. - Renaming or removing an input/output is a breaking change (major version).
- Balloon geometry lives in files synced from the CMS editor — see Balloon Renderer before touching
comic-balloon.tsorballoon-geometry.ts.
Continuous integration
| Workflow | Runs on | What it does |
|---|---|---|
ci | every push to master and every pull request | Lint, unit tests with coverage, production library build, then lint, tests and build of the reader app, Playwright E2E (chromium + mobile) |
size-limit | pull requests | Fails when the library bundle, minified + gzipped (what a host app ships), grows past 180 KB — currently about 106 KB. The raw, unminified bundle gzipped (about 192 KB) is reported for reference only |
demo-pages | every push to master | Builds the demo app and deploys it to panelwave.github.io/player |
release | manual | Publishes a new version to npm (below) |
The bundle budget is a hard limit: a change that needs more room has to make room elsewhere.
Releases
@panelwave/player is published to npm by the release GitHub Actions workflow, never from a developer machine:
- A maintainer runs the workflow and picks the semver bump (
patch,minor,majororprerelease). - The workflow lints, builds, bumps
projects/player/package.json, rebuilds, and publishesdist/playerusing npm trusted publishing — the registry trusts this repository's workflow via OIDC, so there is no stored npm token, and every version carries a provenance statement linking it to the workflow run. - It commits the version bump, tags
vX.Y.Zand creates the GitHub release.
Versioning follows semver: fixes are patches, new inputs/outputs/features are minors, and removed or renamed API is a major. The npm package page shows the package README (projects/player/README.md), so README changes appear there with the next release.
To see exactly what would be published:
ng build player --configuration production
cd dist/player && npm pack --dry-run
Notes from projects/player/ng-package.json:
- The entry file is
src/public-api.ts; output goes todist/player/. src/assets/(balloon fonts, UI translation JSON) is copied into the package, except files matchingamespro-*— the commercial Ames Pro font is never distributed. The default balloon config falls back to Comic Neue when Ames is absent.- The library
package.jsonsetssideEffects: falsefor tree-shaking.
Related pages
- Installation — consuming the published package
- Demo App — manifest selector, device frames, dev tools
- Inputs & Outputs — the shell's public contract
- Core Services and Components — where to make your change