Adding a new export kind
An HTML module has four export kinds: component (a <template>), stylesheet (only <style>), data
(one JSON <script>) and re-export (src). The export kind is this library’s real extension point: elements,
naming and binding are fixed by the PRD, but what an <html-export> can hold is decided in one function and
consumed by two back ends. The stylesheet kind is the best worked example in the package’s history (it shipped with
the PRD implementation in e597271), because it is the one kind that needed its own runtime value, its own binding
behaviour (adopt) and its own exclusion from registration, all of which a new kind may need.
Smallest: a new attribute or payload on an existing kind. record.js’s exportRecord() is a chain of branches
by children shape; a new component attribute with concrete semantics (as delegates-focus was) is one more field in
the component branch, passed through defineHTMLComponent() by the loader and emitted by the compiler. A new JSON
MIME type is a change to one regular expression. And if the new thing is just a value, it is already a data
export: no new kind. The test that separates the two: does the value need its own runtime type that bindings must
treat differently (registered, adopted, or refused), which a JSON value cannot express?
A genuinely new kind. Every existing kind follows one pattern, so a new one does too:
src/record.js: a branch inexportRecord()that recognizes the new children shape and returns{ kind: '<x>', name, default?, … }(copy the stylesheet branch), plus its entry in theExportRecordtypedef. Its validation errors areSyntaxErrors built withdescribe(raw)andwhere, like every other.src/scan.js, only if the payload lives somewhere the scanner does not already capture (it keeps a direct child’stextfor raw-text elements andhtmlfor<template>).rawOf()inrecord.jsis the DOM-side equivalent and must capture the same thing.src/runtime.js: a value class anddefine<X>()(copyHTMLStylesheet/defineHTMLStylesheet), branded with aSymbol.for('html-modules.<x>')so copies of the library recognize each other, anis<X>()predicate, and a label inkindOf()so binding errors name the kind. If bindings treat it specially, that is a branch inapplyBinding()(asadoptis for stylesheets).manifest()andcomponentsOf()exclude non-element values already.- The one part that isn’t boilerplate: the two back ends must build the same value.
linkHTMLModule()insrc/loader.js(runtime) andcompileRecord()insrc/compiler.js(compiled output) each have aswitch (e.kind); add the case to both, and add the newdefine<X>to the compiler’s helper imports. The module record is the contract between them: neither back end looks at HTML, so if the record is right and both cases construct the value from the same record fields, runtime-loaded and compiled modules cannot disagree. The<html-export>element itself needs nothing: exports are read from a module’s source, never executed in place.
Tests, all against local files and linkedom, no browser or network: a record test in test/format.test.js
(both readers, and every new error message with the module URL), a case in test/compiler.test.js’s “runtime and
compiled definitions are equivalent”, and an example module using the new kind under examples/components/, which
test/examples.test.js automatically runs through the DOM reader / scanner agreement check and the loader. Add a
line to examples/04-invalid-modules-fail-with-a-named-syntax-error.mjs for each new error.
Contrast with @johnhenry/fileable’s “Adding a new tag” section, where a new tag relabels itself so the rest of
the pipeline never learns it exists. Here the opposite holds: both back ends must learn the new kind, and the
shared record, plus the equivalence test, is what keeps them in step.
Project layout
Section titled “Project layout”src/ names.js export names, namespaces and delimiters settings.js the settings vocabulary, validation and precedence runtime.js HTML Component Definitions → custom elements; binding (shared by runtime and compiled code) record.js module records; readHTMLModule() from a DOM types.js JSDoc typedefs (type definitions only) template.js data binding: {{attribute}} sites, props, URL escaping form.js form-associated components (ElementInternals) dev-server.js html-module dev: static server, fs.watch, SSE (and dev-client.js, the page half) vite.js the Vite plugin scan.js scanHTMLModule() from source text loader.js resolve, fetch, parse, cache, link dependencies lazy.js lazy loading: what an import waits for, and the watcher html-modules.js createHTMLModules(): load / import / bind elements.js <html-import>, <html-binding>, <html-export>, and the settings elements compiler.js compileHTMLModule() index.js the side-effect-free root entry point browser.js the one-script bootstraptypes/ generated .d.ts files (npm run types), shippedbin/html-module.js the compiler CLIexamples/ numbered Node examples (npm run examples) and the browser demo site (open /examples/)test/ node:test suites (npm test), with linkedom as the test DOMdocs/api.md the API reference (docs/api/*.md)docs/GAP.md the PRD mapped onto the codenpm test # node:test, with linkedom as the test DOMnpm run check # every source file parses; entry points importnpm run examples # the numbered Node examples, each self-verifyingnpm run examples:compile # regenerate examples/compiled/npm run test:browser # Playwright: Chromium, Firefox, WebKitnpm run types # regenerate the .d.ts files; npm run types:check compiles a typed consumernpm run bench # non-gating benchmark