@missing-elements/h5p-embed 0.7.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -30,7 +30,7 @@ a subdomain shares your site's cookies.
30
30
  | `--packages <origins>` | Play packages and fetch library bundles only from these origins (the site's own is always allowed). Add `https://cdn.jsdelivr.net` for `libraries=pack` on a site built with `--no-libraries`. Default: any https host |
31
31
  | `--ancestors <origins>` | Only these sites may frame the page (`frame-ancestors`, header only). Default: any site |
32
32
  | `--default-libraries <sources>` | The `libraries` value for addresses that name none: `pack` or URLs. Checked against `--packages`. Default: none, so exports without libraries are refused unless the address asks |
33
- | `--no-libraries` | Leave out the 13.7 MB library pack; `libraries=pack` then fetches it from jsDelivr, where allowed |
33
+ | `--no-libraries` | Leave out the library pack, 13.8 MB in `libraries/`, one archive per library, of which a learner's browser fetches only those a package needs; `libraries=pack` then fetches them from jsDelivr, where allowed |
34
34
  | `--force` | Write into a non-empty folder, replacing only this tool's files |
35
35
  | `-h`, `--help` / `-v`, `--version` | |
36
36
 
@@ -65,7 +65,7 @@ The headers themselves, for a host not listed:
65
65
  |---|---|
66
66
  | every path | `Content-Security-Policy` (the policy the command prints, with `frame-ancestors` from `--ancestors`), `X-Content-Type-Options: nosniff`, `Referrer-Policy: strict-origin-when-cross-origin` |
67
67
  | `/h5p-sw.js` | `Cache-Control: no-cache`, so an update reaches learners on their next visit |
68
- | `/frame-assets/*`, `/resizer.js`, `/libraries.h5p` | `Cache-Control: public, max-age=3600, stale-while-revalidate=86400` |
68
+ | `/frame-assets/*`, `/resizer.js`, `/libraries/*` | `Cache-Control: public, max-age=3600, stale-while-revalidate=86400` |
69
69
 
70
70
  To update, run the command again with `--force` and redeploy.
71
71
 
@@ -74,7 +74,8 @@ To update, run the command again with `--force` and redeploy.
74
74
  | Parameter | Effect |
75
75
  |---|---|
76
76
  | `src=<url>` | The package, required. Encode `&`, `#`, `+`, `%` and spaces |
77
- | `libraries=pack`, `<url>` or `none` | Libraries for an export without them; several are tried in order. `pack` is this domain's copy, or jsDelivr's without one; `hub` means `pack`; `none` turns off `--default-libraries` |
77
+ | `libraries=pack`, `<url>` or `none` | Where libraries come from; several are tried in order. `pack` is this domain's copy, or jsDelivr's without one: a package gets the libraries it lacks from it, and those it carries at the pack's newer versions. `hub` means `pack`; `none` turns off `--default-libraries` |
78
+ | `refresh-libraries=off` | Play the libraries a package carries as exported; `pack` then only fills gaps |
78
79
  | `frame`, `copyright`, `export`, `icon`, `reporting` | H5P's action bar and its buttons |
79
80
  | `fullscreen=off` | No fullscreen button |
80
81
  | `preload=auto` | Start fetching media at once |
@@ -112,7 +113,7 @@ Whatever `xapi=` says, the frame also posts to its parent:
112
113
  { context: 'h5p-offline-player', action: 'report',
113
114
  source, // { type: 'range-http' | 'chunked' | 'file', size }
114
115
  metadata, // { title, license, licenseVersion, authors, mainLibrary }, from h5p.json; show as text
115
- libraryBundle, // { url, origin, fromCache }, or null
116
+ libraryBundle, // { url, origin, fromCache, refreshed? }, or null; refreshed: [{ library, from, to }]
116
117
  elapsedMs }
117
118
  { context: 'h5p-offline-player', action: 'error', code, message } // code 'refused', 'no-src' or the load's
118
119
  ```
@@ -127,19 +128,21 @@ await buildSite({ out: 'public/player', packages: ['https://cdn.example.org'] })
127
128
  `buildSite` takes `out`, `libraries` (default `true`), `packages`, `ancestors`, `defaultLibraries`
128
129
  and `force`, as the flags. `@missing-elements/h5p-embed/embed.js` exports `startEmbed()`, the
129
130
  page's script, which reads the page's `<h5p-player>`, `#loader` and `#notice` (see
130
- `site/index.html` and `site/embed.css`). Its options: `librariesPack` (the URL `libraries=pack`
131
- names), `packages` (allowed origins, `null` for any), `defaultLibraries`, `runtime` (the
131
+ `site/index.html` and `site/embed.css`). Its options: `pack` (the `@missing-elements/h5p-libraries`
132
+ export, which `libraries=pack` then plays from) or `packBase` (the folder of a copy of its
133
+ `parts/`), `packages` (allowed origins, `null` for any), `defaultLibraries`, `runtime` (the
132
134
  `@missing-elements/h5p-runtime` export, for a bundled element) and `askInOwnFrame` (default
133
135
  `true`; `false` plays a package from another origin as it is when a same-origin page frames it).
136
+ `librariesPack`, the URL of a whole `libraries.h5p`, still works, as a bundle that only fills gaps.
134
137
 
135
138
  ```js
136
139
  import '@missing-elements/h5p-offline-player'
137
140
  import { runtime } from '@missing-elements/h5p-runtime'
138
- import librariesPack from '@missing-elements/h5p-libraries/libraries.h5p?url'
141
+ import { pack } from '@missing-elements/h5p-libraries'
139
142
  import '@missing-elements/h5p-embed/embed.css'
140
143
  import { startEmbed } from '@missing-elements/h5p-embed/embed.js'
141
144
 
142
- startEmbed({ runtime, librariesPack, defaultLibraries: 'pack' })
145
+ startEmbed({ runtime, pack, defaultLibraries: 'pack' })
143
146
  ```
144
147
 
145
148
  ## Privacy
package/lib/build.mjs CHANGED
@@ -7,8 +7,9 @@ import { fileURLToPath } from 'node:url'
7
7
  * Writes the embed page as a static site: one folder, deployable to any static host, that a site
8
8
  * frames from a player domain of its own. Everything comes from installed packages — the element
9
9
  * and its workers from the player's `dist/`, the H5P runtime from its own package as
10
- * `frame-assets/`, the library pack from `@missing-elements/h5p-libraries` — so the versions are
11
- * the ones this package was published with, and nothing is fetched.
10
+ * `frame-assets/`, the library pack's parts from `@missing-elements/h5p-libraries` as
11
+ * `libraries/` — so the versions are the ones this package was published with, and nothing is
12
+ * fetched.
12
13
  */
13
14
 
14
15
  export class EmbedError extends Error {}
@@ -149,7 +150,7 @@ function headerRules(csp) {
149
150
  { path: '/h5p-sw.js', headers: { 'Cache-Control': 'no-cache' } },
150
151
  { path: '/frame-assets/*', headers: { 'Cache-Control': revalidate } },
151
152
  { path: '/resizer.js', headers: { 'Cache-Control': revalidate } },
152
- { path: '/libraries.h5p', headers: { 'Cache-Control': revalidate } }
153
+ { path: '/libraries/*', headers: { 'Cache-Control': revalidate } }
153
154
  ]
154
155
  }
155
156
 
@@ -255,8 +256,9 @@ function notice({ version, libraries }) {
255
256
  frame-assets/ @missing-elements/h5p-runtime, the H5P core runtime: GPL-3.0-only. Its
256
257
  LICENSE.txt and NOTICE.txt say what it is and where its source is; keep them
257
258
  with it.
258
- ${libraries ? ` libraries.h5p @missing-elements/h5p-libraries, the H5P hub's libraries, each under its
259
- own licence, listed in libraries.txt.
259
+ ${libraries ? ` libraries/ @missing-elements/h5p-libraries, the H5P hub's libraries, one archive per
260
+ library and index.json, each under its own licence, listed in libraries.txt.
261
+ The player downloads only the ones a package needs.
260
262
  ` : ''}
261
263
  Serve it from a domain that holds nothing else, and frame it:
262
264
 
@@ -287,7 +289,7 @@ async function occupied(dir) {
287
289
  *
288
290
  * @param {object} options
289
291
  * @param {string} options.out
290
- * @param {boolean} [options.libraries] include the library pack, for `libraries=pack` (default true)
292
+ * @param {boolean} [options.libraries] include the library pack's parts, for `libraries=pack` (default true)
291
293
  * @param {string[] | string | null} [options.packages] the only origins packages may come from
292
294
  * @param {string[] | string | null} [options.ancestors] the only origins that may frame the page
293
295
  * @param {string | null} [options.defaultLibraries] the `libraries` value for addresses that name none
@@ -308,7 +310,7 @@ export async function buildSite({ out, libraries = true, packages = null, ancest
308
310
 
309
311
  const player = dirname(installed('@missing-elements/h5p-offline-player/dist/h5p-player.js', 'Run `pnpm build` in the workspace, or reinstall this package.'))
310
312
  const runtime = dirname(installed('@missing-elements/h5p-runtime/dist/h5p.css', 'Run `pnpm build` in the workspace, or reinstall this package.'))
311
- const pack = libraries ? installed('@missing-elements/h5p-libraries/libraries.h5p', 'Reinstall this package, or pass --no-libraries.') : null
313
+ const pack = libraries ? dirname(installed('@missing-elements/h5p-libraries/parts/index.json', 'Reinstall this package, or pass --no-libraries.')) : null
312
314
  const version = (await readFile(join(player, 'VERSION'), 'utf8').catch(() => 'unknown')).trim()
313
315
 
314
316
  await mkdir(target, { recursive: true })
@@ -316,11 +318,13 @@ export async function buildSite({ out, libraries = true, packages = null, ancest
316
318
  for (const name of PLAYER_FILES) await cp(join(player, name), join(target, name))
317
319
  await rm(join(target, 'frame-assets'), { recursive: true, force: true })
318
320
  await cp(runtime, join(target, 'frame-assets'), { recursive: true })
321
+ // A site written before the pack was split carried it whole; that file is gone either way.
322
+ await rm(join(target, 'libraries.h5p'), { force: true })
323
+ await rm(join(target, 'libraries'), { recursive: true, force: true })
319
324
  if (pack) {
320
- await cp(pack, join(target, 'libraries.h5p'))
325
+ await cp(pack, join(target, 'libraries'), { recursive: true })
321
326
  await cp(join(dirname(pack), 'libraries.txt'), join(target, 'libraries.txt'))
322
327
  } else {
323
- await rm(join(target, 'libraries.h5p'), { force: true })
324
328
  await rm(join(target, 'libraries.txt'), { force: true })
325
329
  }
326
330
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@missing-elements/h5p-embed",
3
- "version": "0.7.0",
3
+ "version": "0.8.0",
4
4
  "description": "Self-hosted H5P embeds: writes the H5P embed page as a static site for a player domain of your own, then any site frames it with an iframe and one script line. No H5P server, no third-party service; optional allowlists for package hosts and embedding sites.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -43,9 +43,9 @@
43
43
  "cli"
44
44
  ],
45
45
  "dependencies": {
46
- "@missing-elements/h5p-libraries": "^0.2.0",
47
- "@missing-elements/h5p-offline-player": "^0.9.5",
48
- "@missing-elements/h5p-runtime": "^0.2.4"
46
+ "@missing-elements/h5p-libraries": "^0.3.0",
47
+ "@missing-elements/h5p-runtime": "^0.2.4",
48
+ "@missing-elements/h5p-offline-player": "^0.10.0"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@types/node": "^26.6.5",
@@ -55,7 +55,7 @@
55
55
  "vitest": "^5.0.3"
56
56
  },
57
57
  "engines": {
58
- "node": ">=20"
58
+ "node": ">=22"
59
59
  },
60
60
  "scripts": {
61
61
  "test": "pnpm --filter @missing-elements/h5p-offline-player build && vitest run",
package/site/embed.js CHANGED
@@ -5,8 +5,8 @@
5
5
  * `h5p-embed` was told when it wrote the site, and the demo's `/embed` with the demo's copy of the
6
6
  * library pack.
7
7
  *
8
- * ?src=<package url>[&libraries=pack|<url> …|none][&preload=auto][&xapi=<parent origin>]
9
- * [&frame][&copyright][&export][&icon][&reporting][&fullscreen=off]
8
+ * ?src=<package url>[&libraries=pack|<url> …|none][&refresh-libraries=off][&preload=auto]
9
+ * [&xapi=<parent origin>][&frame][&copyright][&export][&icon][&reporting][&fullscreen=off]
10
10
  * [&activity-id=<IRI>][&custom-css=<stylesheet url>][&lang=<language tag>]
11
11
  *
12
12
  * Upward it speaks H5P's own resizer protocol — the `hello` / `resize` exchange that h5p.org's
@@ -23,8 +23,12 @@ const PACK_CDN_ORIGIN = 'https://cdn.jsdelivr.net'
23
23
 
24
24
  /**
25
25
  * @param {object} [options]
26
- * @param {string | null} [options.librariesPack] the URL of a copy of `@missing-elements/h5p-libraries`
27
- * on this site, which `libraries=pack` names; without one, `pack` is the element's copy on jsDelivr
26
+ * @param {string | null} [options.packBase] the folder of this site's copy of the library pack's
27
+ * parts (`parts/` of `@missing-elements/h5p-libraries`), which `libraries=pack` plays from
28
+ * @param {{ libraries: object[] } | null} [options.pack] the same, as the `pack` export a bundler
29
+ * resolves; it wins over `packBase`. Without either, `pack` is the element's copy on jsDelivr
30
+ * @param {string | null} [options.librariesPack] the URL of a whole `libraries.h5p` on this site,
31
+ * for a page written before the pack was split: played as any bundle, with no refresh
28
32
  * @param {string[] | null} [options.packages] the origins packages and library bundles may come
29
33
  * from, besides this page's own; `null` plays any, one from another origin `untrusted` when the
30
34
  * storage is this origin's
@@ -38,11 +42,15 @@ const PACK_CDN_ORIGIN = 'https://cdn.jsdelivr.net'
38
42
  * page of its own origin, whose storage it shares. A site whose own preview frames the page for
39
43
  * a package the visitor just chose passes `false`
40
44
  */
41
- export function startEmbed({ librariesPack = null, packages = null, defaultLibraries = null, runtime = null, askInOwnFrame = true } = {}) {
45
+ export function startEmbed({ packBase = null, pack = null, librariesPack = null, packages = null, defaultLibraries = null, runtime = null, askInOwnFrame = true } = {}) {
42
46
  const params = new URLSearchParams(location.search)
43
47
  const player = document.querySelector('h5p-player')
44
- // Before `src`: the element resolves the runtime's files when a package is set.
48
+ // Before `src`: the element resolves the runtime's files, and reads where the pack is, when a
49
+ // package is set.
45
50
  if (runtime) player.runtime = runtime
51
+ if (pack) player.pack = pack
52
+ else if (packBase) player.setAttribute('pack-base', packBase)
53
+ const ownPack = Boolean(pack || packBase)
46
54
  const notice = document.querySelector('#notice')
47
55
  const loader = document.querySelector('#loader')
48
56
  const framed = window.parent !== window
@@ -237,12 +245,13 @@ export function startEmbed({ librariesPack = null, packages = null, defaultLibra
237
245
  if (value === 'none') return { value: '' }
238
246
  for (const token of value.split(/\s+/).filter(Boolean)) {
239
247
  if (token === 'pack' || token === 'hub') {
240
- // This site's copy of the pack; without one, the element's own `pack`, the copy on
241
- // jsDelivr, where that may be reached, so a snippet written for `pack` keeps playing after
242
- // a rebuild with --no-libraries. `hub` has meant `pack` since player 0.6: the one H5P hub
243
- // host that answers a browser serves a catalogue older than current exports need.
244
- if (librariesPack) sources.push(librariesPack)
245
- else if (permitted(PACK_CDN_ORIGIN)) sources.push('pack')
248
+ // This site's copy of the pack, which the element was pointed at above; without one, the
249
+ // element's own `pack`, the copy on jsDelivr, where that may be reached, so a snippet
250
+ // written for `pack` keeps playing after a rebuild with --no-libraries. `hub` has meant
251
+ // `pack` since player 0.6: the one H5P hub host that answers a browser serves a
252
+ // catalogue older than current exports need.
253
+ if (ownPack || (!librariesPack && permitted(PACK_CDN_ORIGIN))) sources.push('pack')
254
+ else if (librariesPack) sources.push(librariesPack)
246
255
  else return { error: 'This player was set up without the library pack, so libraries=pack is not available here.' }
247
256
  } else {
248
257
  const origin = urlOrigin(token)
@@ -256,7 +265,8 @@ export function startEmbed({ librariesPack = null, packages = null, defaultLibra
256
265
  /**
257
266
  * The element's display options, by their attribute names, for the embedding page to ask for:
258
267
  * `&frame&copyright&export` shows H5P's action bar with those buttons, `&fullscreen=off` takes
259
- * that one away, `&activity-id=` names the statements' object and `&custom-css=` restyles the
268
+ * that one away, `&refresh-libraries=off` plays a package's own libraries even where the pack
269
+ * has newer ones, `&activity-id=` names the statements' object and `&custom-css=` restyles the
260
270
  * content to the embedding site's taste. `&lang=` is the embedding page's language, which this
261
271
  * page cannot read across origins: H5P's own buttons and dialogs follow it. Not `custom-js`,
262
272
  * `embed-code` or `user`: a script is a capability on this origin that a link should not hand
@@ -267,6 +277,9 @@ export function startEmbed({ librariesPack = null, packages = null, defaultLibra
267
277
  if (params.has(name) && params.get(name) !== 'off') player.setAttribute(name, '')
268
278
  }
269
279
  if (params.get('fullscreen') === 'off') player.setAttribute('fullscreen', 'off')
280
+ // The pack's newer versions of the libraries a package carries, unless the embedding page
281
+ // needs the package exactly as exported.
282
+ if (params.get('refresh-libraries') === 'off') player.setAttribute('refresh-libraries', 'off')
270
283
  for (const name of ['activity-id', 'custom-css']) {
271
284
  const value = params.get(name)?.trim()
272
285
  if (value) player.setAttribute(name, value)
@@ -358,11 +371,11 @@ export function startEmbed({ librariesPack = null, packages = null, defaultLibra
358
371
  // as the host's own: an untrusted package plays with this site's sources alone, the page's
359
372
  // default or its pack, and the address's only once the click has trusted it.
360
373
  const own = defaultLibraries?.trim() ? librarySources(defaultLibraries.trim()).value : ''
361
- const pack = own || librarySources('pack').value
362
- if (!pack) {
374
+ const sitesSources = own || librarySources('pack').value
375
+ if (!sitesSources) {
363
376
  askToTrust()
364
377
  return
365
378
  }
366
379
  player.untrusted = true
367
- start(src, pack)
380
+ start(src, sitesSources)
368
381
  }
package/site/main.js CHANGED
@@ -1,11 +1,11 @@
1
1
  /*! @missing-elements/h5p-embed. MIT. */
2
2
  // The page's entry: `config.js` is what `h5p-embed` was told when it wrote this site — whether
3
- // the library pack is here, and which hosts packages may come from.
3
+ // the library pack's parts are here, and which hosts packages may come from.
4
4
  import config from './config.js'
5
5
  import { startEmbed } from './embed.js'
6
6
 
7
7
  startEmbed({
8
- librariesPack: config.libraries ? new URL('./libraries.h5p', import.meta.url).href : null,
8
+ packBase: config.libraries ? new URL('./libraries/', import.meta.url).href : null,
9
9
  packages: config.packages,
10
10
  defaultLibraries: config.defaultLibraries
11
11
  })