diff --git a/e2e/skill.spec.ts b/e2e/skill.spec.ts index b33b33c..74b7f8b 100644 --- a/e2e/skill.spec.ts +++ b/e2e/skill.spec.ts @@ -5,9 +5,10 @@ import { expect, test } from '@playwright/test' /* * /h5p/skill/ as a chat that cannot install the h5p-author skill reads it: a page that writes out - * the full address of every file, since a chat's fetch tool may open only addresses it has seen - * whole; each file there as the pinned author package carries it; and every address SKILL.md gives, - * relative to itself or written out, served by this site. + * the full address of every file, and of the build tool and every bundle in /h5p/bundles/, since a + * chat's fetch tool may open only addresses it has seen whole; each file there as the pinned author + * package carries it; and every address SKILL.md gives, relative to itself or written out, served + * by this site. */ const SITE = 'https://missing-elements.github.io' @@ -38,6 +39,17 @@ test('the page writes out the full address of every file of the skill, as its li expect([...listing.files].sort()).toEqual(files) }) +test('writes out the full address of the build tool and of every bundle the site serves, as its link and its text', async ({ page, request }) => { + // The page names the bundles before Vite writes them; the listing is what Vite wrote. + const listing = await (await request.get('/h5p/bundles/index.json')).json() as { tool: string, bundles: Array<{ file: string }> } + expect(listing.bundles.length).toBeGreaterThan(40) + await page.goto('/h5p/skill/') + const links = await page.locator(`main a[href^="${SITE}/h5p/bundles/"]`).evaluateAll((anchors) => + anchors.map((anchor) => ({ href: anchor.getAttribute('href'), text: anchor.textContent?.trim() }))) + expect(links.map(({ href }) => href)).toEqual([listing.tool, ...listing.bundles.map(({ file }) => file)].map((file) => `${SITE}/h5p/bundles/${file}`)) + for (const { href, text } of links) expect(text).toBe(href) +}) + test('serves each file at its address as the pinned author package carries it', async ({ request }) => { for (const file of await carried()) { const response = await request.get(`/h5p/skill/${file}`) @@ -54,6 +66,9 @@ test('serves every address SKILL.md gives, relative to itself or on this site', // Its addresses on this site, with a content type for the name it leaves to fill in, and no fragment. const filled = skill.replace(//gi, 'H5P.QuestionSet') const onSite = [...new Set([...filled.matchAll(/https:\/\/missing-elements\.github\.io(\/[^\s`)>"'#]*)/g)].map(([, path]) => path))] - expect(onSite).toEqual(expect.arrayContaining(['/h5p/build/types/H5P.QuestionSet.md', '/h5p/bundles/H5P.QuestionSet.zip', '/h5p/build/'])) + expect(onSite).toEqual(expect.arrayContaining(['/h5p/bundles/H5P.QuestionSet.zip', '/h5p/build/'])) + // A type's file, at an address of its own as h5p-author 0.9.0 gives it, or from this page, which + // writes each one out (the first test), as the release after it does (h5p-offline-player#180). + expect(onSite.some((path) => path === '/h5p/build/types/H5P.QuestionSet.md' || path === '/h5p/skill/'), 'a way to a type\'s file').toBe(true) for (const path of [...relativeLinks, ...onSite]) expect((await request.get(path)).ok(), path).toBe(true) }) diff --git a/src/_data/skill.js b/src/_data/skill.js index 1c8551a..59ab7b2 100644 --- a/src/_data/skill.js +++ b/src/_data/skill.js @@ -1,5 +1,6 @@ import { existsSync, readdirSync, readFileSync } from "node:fs"; import { join, relative, sep } from "node:path"; +import { contentTypes, openPack } from "@missing-elements/h5p-author"; // The h5p-author skill as the pinned @missing-elements/h5p-author carries it since 0.9.0: its // SKILL.md, licence and references/, which eleventy.config.js copies to /h5p/skill/ as they are. @@ -14,10 +15,23 @@ const references = readdirSync(`${author}/references`, { recursive: true, withFi .sort(); const types = references.filter((path) => path.startsWith("references/types/")); +// What a build where npm cannot be reached fetches (SKILL.md's step 3), which vite.config.ts +// writes to /h5p/bundles/ after this page: the command line, and a bundle per content type, named +// by the author package's own list of them, the one its `writeBundles` writes from. +const pack = await openPack(); +let bundles; +try { + bundles = (await contentTypes(pack)).map((part) => `${part.library.machineName}.zip`); +} finally { + await pack.close(); +} + export default { version, // What SKILL.md names and the type files, apart, as the page lists them; `files` is every one. named: [...references.filter((path) => !types.includes(path)), "LICENSE"], types, files: ["SKILL.md", ...references.filter((path) => !types.includes(path)), "LICENSE", ...types], + // In /h5p/bundles/, the tool first. + bundles: ["h5p-author.mjs", ...bundles], }; diff --git a/src/h5p/skill/index.njk b/src/h5p/skill/index.njk index bb8ffe7..5066c87 100644 --- a/src/h5p/skill/index.njk +++ b/src/h5p/skill/index.njk @@ -1,7 +1,7 @@ --- layout: base.njk title: The h5p-author skill ยท Missing Elements -description: The h5p-author skill's files, each at its full address, for an AI chat that cannot install the skill. +description: The h5p-author skill's files and those its build fetches, each at its full address, for an AI chat that cannot install the skill. # No script but the site's: base.njk writes the policy first in , and the color-scheme script # is added to script-src by its hash at build (eleventy.config.js). csp: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; object-src 'none'; base-uri 'self'" @@ -9,6 +9,7 @@ csp: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; o {#- Every address is written out whole, as its link's text too: a chat's fetch tool may open only the addresses it has seen in full, and a relative link, or SKILL.md's , is none. -#} {%- set base = site.url + "/h5p/skill/" -%} +{%- set bundlesBase = site.url + "/h5p/bundles/" -%}
{{ site.name }} @@ -17,13 +18,15 @@ csp: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; o {% include "theme-switch.njk" %}

- Its files, each at its full address, for an AI chat that cannot install the skill. + Its files and those its build fetches, each at its full address, for an AI chat that cannot + install the skill.

Read SKILL.md first and follow its steps. A file it names, such as references/authoring.md or a content type's references/types/<Type>.md, - is the one listed here under that path. These are the files of h5p-author {{ skill.version }}, - the release the build page at {{ site.url }}/h5p/build/ runs. + is the one listed here under that path, and a bundle its step 3 fetches is listed last. These + are the files of h5p-author {{ skill.version }}, the release the build page at + {{ site.url }}/h5p/build/ runs.

Where skills can be installed, install it instead: @@ -62,6 +65,21 @@ csp: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; o +

+

To build where npm cannot be reached

+

+ The build tool, which runs with Node and nothing installed, and a bundle per content type: + the libraries a build of that type reads. Fetch the tool and the bundle of the source's own + type, as step 3 of SKILL.md says. +

+
    + {%- for file in skill.bundles %} +
  • {{ bundlesBase }}{{ file }} + {%- if file == "h5p-author.mjs" %}: the build tool{% endif %}
  • + {%- endfor %} +
+
+