Skip to content

Vendor the H5P core from h5p-php-library and retire h5p-standalone - #46

Merged
alekswebnet merged 3 commits into
mainfrom
feat/vendor-core
Oct 18, 2026
Merged

alekswebnet merged 3 commits into
mainfrom
feat/vendor-core

Conversation

@alekswebnet

@alekswebnet alekswebnet commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

Closes #37.

The runtime no longer uses h5p-standalone. It vendors the H5P core from h5p-php-library at a pinned commit, and its boot script loads the core itself, in the order the worker gives.

What changed

The core, vendored (packages/runtime/)

  • pnpm core:sync [<ref>] (scripts/sync-core.mjs) fetches h5p-php-library at a commit into vendor/h5p-core/, which is committed. It is pinned at cb64a1f (2026-08-11, core API 1.28), the commit content-check.ts is already ported from.
  • What it takes, and in what order, is upstream's own list: H5PCore::$scripts and H5PCore::$styles in h5p.classes.php at that commit. It also takes the fonts and image the stylesheets reach, their licence files, and the player's English strings from getLocalization(), ending where the hub's sharing-wizard strings begin.
  • Those English strings are the same 67 h5p-standalone carried, word for word, so l10n-rules.mjs reads them from strings.json and no language file changes.
  • frame.bundle.js is the ten scripts joined in upstream's order and minified with top-level names kept. They are global scripts, as a site loads them, and H5P's jQuery.noConflict(true) needs jQuery's global (h5p-standalone built that file as an ES module to get the same effect).
  • Size: 48.5 kB gzipped, against 54.6 kB for frame.bundle.js and main.bundle.js together before.
  • h5p.css is built as before, from the eight sheets. NOTICE.txt and MANIFEST.json now name the commit, so the corresponding source is exact.
  • The h5p-standalone dependency is gone, and main.bundle.js with it.

The boot (src/frame-boot.ts)

  • The boot script builds H5PIntegration itself, field for field as h5p-standalone did, then mounts the .h5p-content div, adds the core's and the libraries' stylesheets and scripts (marked data-h5p, waited for), and holds H5P.init back with the core's own preventInit.
  • The upgrade, the content check, the user-data shim and the init sequence are unchanged.
  • url is now the package's root rather than a random id. No library in the pack reads it, and getContentPath was already answered from contentUrl.

What to load: the worker's boot route (packages/player/src/sw/library-load.ts, mount.ts)

  • …/h5p/boot/<load> answers a LibraryLoad:
    • the main library at the version served, in full;
    • every library's scripts and styles, dependencies first;
    • libraryDirectories for a library in a folder its version does not name, which the core's H5P.getLibraryPath reads;
    • the upgrades list.
  • Why the frame asks for it instead of the worker writing it into the frame document: only a request from the frame can reopen a picked file's reader after a worker restart. The page is asked for the file through the frame, which doesn't exist yet while its own document is being served.
  • The order is h5p-standalone's to the letter: its loadDependencies levels, its sortDependencies edges, and toposort 2.0.2 with its tie-breaks, including the phantom node a library without dependencies gets. Two deliberate differences:
    • A library is named by the folder that serves it, so one folder answering two requested versions loads once.
    • A dependency cycle is broken where it closes; h5p-standalone threw on one.
  • A library nothing provides makes the route refuse the load, naming the library.

Across versions

  • A record that names mainJs came from an element paired with runtime ≤ 0.2. For it, the worker still writes the old document: the loader script, and the upgrade list in the config.
  • Checked by hand: runtime 0.2.5 from npm, handed over through the runtime property, plays against this worker.
  • The new boot script, given a document from an older worker (no boot URL), fails with a message to update the player and h5p-sw.js together.

Testing

  • Same order as before:
    • The new tests/unit/library-load.test.ts runs h5p-standalone's own loadDependencies, sortDependencies and toposort beside the port on 500 random dependency graphs.
    • In the frame, I captured what loads for 68 packages on main and on this branch: the six demo packages, nine fixtures and the sweep's 55. All 68 load the same files in the same order. 62 are byte-identical. In the other six, only folder names changed: H5P.Question-1.5/… where h5p-standalone requested 1.4/… and the server answered from 1.5.
  • One package that failed now plays: boardgame.h5p (in the fixtures folder, not built by the repo). Its folders are named without versions. With the pack, the refreshed parts sit in versioned folders beside them, and h5p-standalone named every folder by its first dependency's probe, so it ended on "Unexpected end of JSON input". The new fixture unversioned-pack.h5p and a test in pack.test.ts pin the fix.
  • Sweep: 110 of 110 plays pass on main and on this branch. The reports are identical apart from timings, so every content type sends the same statements in both modes.
  • Suites:
    • Chromium, player: unit 341/341 (incl. the new tests), and 527/527 across all of its projects.
    • Firefox test:engine: 182/182, plus the app's 24 (3 Chromium-only skipped).
    • WebKit test:engine: 182/182, plus the app's 24.
    • Demo app in Chromium: 27/27.
    • Embed: 26/26 (its test listed main.bundle.js among a written site's files).
    • Verify: 15/15. Runtime: 180/180.
    • pnpm typecheck passes; pnpm build:demo builds.
  • Verifier: h5p-standalone's own test/h5p-test.h5p passes, and so do the demo quiz and video.
  • One intermittent failure: stalling-host.test.ts's 35-second outage test failed 4 times in a row with the tail arriving at 26–32 s. That was while Spotlight was indexing a worktree, with load average 20. It then passed 5 times and in the full run once the machine was quiet. main passed it twice at the same time, so I can't fully rule out this branch. What fits load is the server giving up after twice the stall bound when the Jobs worker's liveness notices are delayed. Keep an eye on it in CI.

Not covered

  • Safari and iOS by hand.
  • A runtime ≤ 0.2 through assets-base with this player: the element no longer names main.bundle.js, so that pairing does not work and is not meant to.
  • An element from before this change against a stale h5p-sw.js copy: the old worker cannot write a document for a runtime without mainJs. That is the existing "update h5p-sw.js with the element" case.

Release

Runtime and player go together:

  • The runtime's runtime export loses mainJs and its dist/ loses main.bundle.js.
  • Its boot script needs the boot route.
  • The player's peer range has to move to the new runtime at release (the worker still serves 0.2 records, so it can include ^0.2).
  • The release notes name the core's commit.

🤖 Generated with Claude Code

…lone

The runtime took the core from h5p-standalone, which copies it about once a year and records no
upstream revision, and its loader walked every library.json from the frame. Now `pnpm core:sync`
vendors h5p-php-library at a commit (cb64a1f, 2026-08-11, core API 1.28) into
packages/runtime/vendor/h5p-core/, taking what and in what order from upstream's own
H5PCore::$scripts and $styles; the build joins and minifies the scripts into frame.bundle.js
(48.5 kB gzipped, against 54.6 for the two bundles it replaces) and rebuilds the stylesheets as
before. The boot script builds H5PIntegration itself, field for field as h5p-standalone did, with
the core's English strings from getLocalization() at the same commit, and loads what the
worker's new `boot` route names: every library's scripts and styles in h5p-standalone's order,
ported to the letter (its walk, its edges, toposort 2.0.2 and its tie-breaks).

The order is checked against h5p-standalone's own code on 500 random graphs, and on 68 packages
(the demo's, nine fixtures, the sweep's 55) captured from the frame before and after: the same
files in the same order. One fix fell out: a package with folders named without versions, played
with the pack, whose parts are named with them, ended h5p-standalone's boot on a 404, since it
named every folder by its first dependency's probe; the walk tries each library both ways.

BREAKING CHANGE: the runtime has no main.bundle.js and its `runtime` export no `mainJs`; its boot
script needs a worker that answers the `boot` route. The worker still writes the old document for
a runtime up to 0.2 handed over with `mainJs`. The player's peer range moves at release.

Closes #37

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment thread packages/runtime/vendor/h5p-core/js/h5p-x-api-event.js Fixed
Comment thread packages/runtime/vendor/h5p-core/js/h5p-x-api-event.js Fixed
alekswebnet and others added 2 commits October 18, 2026 22:38
The core under packages/runtime/vendor/h5p-core/ is h5p-php-library's
code at a pinned commit, unchanged. CodeQL's two findings in it
(js/insecure-randomness, the anonymous learner id H5P.createUUID makes
with Math.random) are upstream's, and before #37 the same code came from
node_modules/, which was not scanned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ssing library

On a host without Range, a lookup the worker's walk could not finish
because the download stopped moving fell through to "the package does
not carry H5P.Foo", a 422 naming a library the package may well carry,
after waiting the stall bound twice. It answers 503 with Retry-After
now, as an entry's request does. And the walk goes on with the reader
`awaitEntry` hands back, as `serveEntry` does: once the archive is whole
that is a new reader, and every later lookup on the old partial one
opened another.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@alekswebnet
alekswebnet merged commit d02855e into main Oct 18, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Vendor the H5P core from h5p-php-library and retire h5p-standalone

2 participants