@luceosports/play-rendering
v2.13.2
Published
Core canvas rendering library for the LuceoSports platform. Renders play JSON (players, lines, shapes, animations) onto an HTML5 canvas. Published as a UMD bundle via webpack. Consumed by the `animation-playback` and `drawing-animation` frontend submodule
Readme
@luceosports/play-rendering
Core canvas rendering library for the LuceoSports platform. Renders play JSON
(players, lines, shapes, animations) onto an HTML5 canvas. Published as a UMD
bundle via webpack. Consumed by the animation-playback and drawing-animation
frontend submodules.
Install
npm install @luceosports/play-renderingUsage
import { PlayModel, AnimationModel, PlayConstructData } from '@luceosports/play-rendering';
await PlayModel.init(); // load shared assets once
const play = new PlayModel(data, { width: 800 });
const ctx = canvas.getContext('2d')!;
const anim = new AnimationModel(ctx, play);
anim.start(onFinish, onProgress);The complete public type surface lives in src/play-rendering.ts (source) and
dist/types/play-rendering.d.ts (emitted). Anything not exported there is
internal — do not deep-import from dist/types/... subpaths.
Development
npm install # install deps
npm run build # webpack -> dist/play-rendering.js
npm run build:dev # webpack --mode=development
npm run build:types # rimraf dist/types && tsc && node scripts/prune-types.js
npm run check:types-fresh # verify committed dist/types matches a fresh emit
npm run lint # eslint
npm run lint:fix # eslint --fix
npm run verify:pack # build everything + npm pack, prints next stepsThere are no automated tests. Verification is via:
npx tsc --noEmit --emitDeclarationOnly false— type-check sourcenpm run lint- Building both consumer repos (
drawing-animation,animation-playback) against annpm link-ed copy of this lib
The Husky pre-commit hook runs lint-staged plus check:types-fresh. It will
reject any commit where dist/types/ would differ from a fresh build:types
emit — i.e. source changes that didn't regenerate the published types are
caught before they land.
Release process
Releases go directly from master to npm. The library is published as
@luceosports/play-rendering and the dist/ folder is the only thing
shipped in the tarball (controlled by "files": ["dist"] in package.json).
Step 1 — Merge work to master
If you've been working on a feature branch, fast-forward merge to master:
git checkout master
git pull
git merge --ff-only <feature-branch>Confirm working tree is clean before continuing.
Step 2 — Smoke-test the actual publishable tarball
npm run verify:packThat runs npm run build, npm run build:types, then npm pack. It prints the
absolute path to the produced .tgz plus copy-paste commands for installing
into the consumer repos. Run those commands and build each consumer:
cd ../drawing-animation
npm install --no-save "<path-to-tgz>"
npm run build
cd ../animation-playback
npm install --no-save "<path-to-tgz>"
npm run buildIf both build clean, the tarball is safe to publish. The point of this step
over npm link is that npm pack builds the exact tarball npm publish
would ship (respecting "files" and .npmignore), so it catches missing-dist
regressions that a symlinked dev install would mask.
After the smoke-test you can either re-link to keep editing locally:
cd ../drawing-animation && npm link @luceosports/play-rendering
cd ../animation-playback && npm link @luceosports/play-rendering…or leave them on the tarball install until you bump and publish.
Step 3 — Bump version
npm version <patch|minor|major>That edits package.json, creates a commit (default message is the new
version, e.g. 2.9.0), and creates an annotated git tag (v2.9.0).
Semver guidance for this library:
- patch — internal fixes, no public-API change. Bug fixes inside model methods, performance improvements, comment-only docs changes.
- minor — additive public-API change (new exported class/method) or cleanup that's transparent for the known consumers but technically shifts the type surface (e.g. a stricter return type on an existing method).
- major — breaking change to the exports from
src/play-rendering.ts: removed identifier, removed method, changed required parameter, return type widened in a way consumers can't handle. Per.claude/CLAUDE.md's rule: treatsrc/play-rendering.tsas a public contract.
Step 4 — Push commit + tag
git push --follow-tags--follow-tags pushes the v<version> tag along with the version-bump commit.
Step 5 — Publish to npm
Run every npm command that touches your account through a modern npm — login
above all. This repo pins node 16.15.0 with volta, and volta's default
package manager is npm 8.19.4, so a bare npm here is npm 8. It predates
npm's current two-factor flow: npm login asks for a typed one-time password
that the account can no longer produce (see below), and npm whoami answers a
misleading 401 even with a perfectly good token. Override the pin from inside
the repo instead of leaving the directory:
volta run --node 24.13.0 --npm 11.6.2 npm login
volta run --node 24.13.0 --npm 11.6.2 npm whoami
volta run --node 24.13.0 --npm 11.6.2 npm publish --dry-run
volta run --node 24.13.0 --npm 11.6.2 npm publish(volta list all shows which runtimes and package managers are installed;
volta install npm@11 would make the modern one the default everywhere.)
Second factor is WebAuthn only — npm no longer supports authenticator apps.
Both npm login and npm publish go through it: the CLI prints
Open https://www.npmjs.com/login/<id> to use your security key; confirm on
that page and the terminal continues on its own. Two traps:
- Run it in a real terminal window. Without a TTY — CI, an agent shell, a
piped command — npm skips the browser step and fails immediately with
EOTP, asking for an--otp=<code>that the account can no longer produce. - Never type a 2FA recovery code into the OTP prompt. Using one is a "security-sensitive action": npm puts the account into a 72-hour read-only state that blocks publishing, token management, package visibility and org/team changes. It lifts on its own and each new recovery code restarts the clock. If your security key isn't on the machine you publish from, add a second one (Windows Hello / Touch ID) at npmjs.com → Two-Factor Authentication → Add Security Key.
If the account is already in that state, the publish fails after the 2FA
step with 403 ... temporarily suspended due to a recent security-sensitive
action. Nothing is half-published — confirm with
npm view @luceosports/play-rendering version. To unblock consumers while it
lasts, point them at the pushed tag instead of the registry:
"@luceosports/play-rendering": "git+ssh://[email protected]/luceosports/playbook-play-rendering-library.git#v<version>"That works because dist/ is committed here and there is no prepare /
prepack / postinstall script — npm installs the built package as-is. Swap
the semver range back once the version is on the registry.
Long-lived 2FA-bypass automation tokens are not a way around this: since August 2026 they no longer skip 2FA for sensitive operations, and npm is removing their publish rights around January 2027.
A dry-run is cheap and surfaces what would upload:
volta run --node 24.13.0 --npm 11.6.2 npm publish --dry-runThe output lists every file going into the tarball plus the final shasum. When you're satisfied, do the real publish:
volta run --node 24.13.0 --npm 11.6.2 npm publishA prebuilt tarball can be published directly, which sidesteps the node pin entirely — useful when a publish has to be retried later from anywhere:
volta run --node 24.13.0 --npm 11.6.2 npm publish <path-to>.tgzThat uploads to https://registry.npmjs.org/@luceosports/play-rendering/-/play-rendering-<version>.tgz
and updates the latest dist-tag. Published versions are effectively
permanent — npm unpublish only works for the first 72 hours and gets
increasingly restricted. Treat publish as irreversible.
Step 6 — Verify the publish landed
npm view @luceosports/play-rendering version
npm view @luceosports/play-rendering@<version> dist.shasumThe shasum should match what npm publish --dry-run reported in Step 5 — if it
does, what landed on npm is byte-identical to what you tested.
Step 7 — Update consumers
Bump the version range in each consumer's package.json and reinstall. For a
clean install (not symlinked), make sure any prior npm link is gone first.
# in each consumer repo:
npm install --save '@luceosports/play-rendering@^<new-version>'
npm run build # smoke-test against the published versionIf a consumer still has a leftover npm link symlink, npm install --save of
a real version replaces the symlink with the published tarball. The
package.json and package-lock.json diffs there should be:
package.json: bumped semver range (^2.8.0→^2.9.0)package-lock.json: new integrity hash + resolved URL pointing at registry.npmjs.org
Commit those in the consumer repo per its own convention.
Optional — pre-release / RC channel
For a soak period before promoting to latest, use the next dist-tag:
npm version prerelease --preid=rc # 2.9.0 -> 2.10.0-rc.0
git push --follow-tags
npm publish --tag next
# consumers opt in: npm install @luceosports/play-rendering@next
# after the soak:
npm dist-tag add @luceosports/play-rendering@<version> latestThis is overkill for the current scale (~2 known consumers, both maintained in this workspace). Skip unless you're shipping something risky enough to want a multi-day bake-in window.
Architecture pointers
- Source layout:
src/index.tsis the runtime entry (webpack reads it for the JS bundle);src/play-rendering.tsis the type entry (tscreads it for the published declarations). Both files must export the same identifier set — the consumer build (Phase 2 verification) catches drift; long-term the structuralconst X: { new (...): I } = XImplassignment insideplay-rendering.tscatches impl-vs-interface drift at compile time. - Pruning:
scripts/prune-types.jsruns aftertscand deletes anydist/types/*.d.tsfile the public entry can't reach. Without it,tscships ~200 internal layer/sport/trait declaration files that no consumer can ever import. - Curated classes:
PlayerModel,LineModel,ShapeModel,NoteModel,FrameModel,AnimationModel,PlayModel,Bezier— each has an explicit interface insrc/play-rendering.tsthat the impl class must satisfy. Adding a method to one of these requires updating the interface, rebuildingdist/types/, and committing both together.
License
ISC. See package.json.
