ArchitectureDevelopment

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 } into index.html (and renders the page <title> itself). Without a locale, the reader uses the manifest's meta.default_locale.
  • Gating is the shell's own: paywall.rules and "x-locked": true stubs 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 any in 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/* off master.

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:

  1. Export the symbol (and its supporting types) from public-api.ts and document it in docs/API.md.
  2. Keep type definitions in src/lib/types/ aligned with @panelwave/types and the schema — format changes start in the schema, then @panelwave/types, then the player, in one release train.
  3. Renaming or removing an input/output is a breaking change (major version).
  4. Balloon geometry lives in files synced from the CMS editor — see Balloon Renderer before touching comic-balloon.ts or balloon-geometry.ts.

Continuous integration

WorkflowRuns onWhat it does
cievery push to master and every pull requestLint, unit tests with coverage, production library build, then lint, tests and build of the reader app, Playwright E2E (chromium + mobile)
size-limitpull requestsFails 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-pagesevery push to masterBuilds the demo app and deploys it to panelwave.github.io/player
releasemanualPublishes 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:

  1. A maintainer runs the workflow and picks the semver bump (patch, minor, major or prerelease).
  2. The workflow lints, builds, bumps projects/player/package.json, rebuilds, and publishes dist/player using 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.
  3. It commits the version bump, tags vX.Y.Z and 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 to dist/player/.
  • src/assets/ (balloon fonts, UI translation JSON) is copied into the package, except files matching amespro-* — the commercial Ames Pro font is never distributed. The default balloon config falls back to Comic Neue when Ames is absent.
  • The library package.json sets sideEffects: false for tree-shaking.