This is the normative contract for publishable PIE element packages. It covers the JavaScript runtime API and the npm package surface that players, builders, and server-side tooling may rely on.
docs/API_REFERENCE.md provides examples and element-specific API detail. docs/PUBLISHING.md describes release process. When either disagrees with this document, this document is the contract.
Every element consumes a model, a session, and an environment object.
- The model is authored item data. It must include a stable
idand anelementpackage name such as@pie-element/multiple-choice. - The session is learner response data. Its shape is element-specific and must be treated as immutable by players; emit a replacement object when it changes.
- The environment describes mode and role. Supported modes are
gather,view,evaluate, andconfigure; supported roles arestudentandinstructor.
Element-specific model and session fields are part of that element's public contract once published. Breaking shape changes require normal semver treatment.
Controller-bearing elements expose their controller at @pie-element/<name>/controller.
The required controller entry point is:
model(model, session, env, updateSession?): returns the view model used by delivery, author, or print rendering.
Controller modules may also expose these helpers when the element supports the capability:
outcome(model, session, env): returns scoring output.createDefaultModel(partial?): returns a default authoring model.validate(model, config?): returns authoring validation errors.createCorrectResponseSession(model, env?): returns a correct-answer session.
When a helper is exported, its name and behavior are public API. Do not rename or remove it without a breaking release.
Published element view modules export custom element classes. They must not register their authored top-level PIE tag at package import time. Runtime registration of authored item tags belongs to the player or host.
If a package renders private child custom elements inside its own implementation, the package's browser artifact must define those private child tags itself before the first render depends on them. Private child tags are not authored content dependencies and players must not discover them from package dependencies, DOM snippets, or element-specific knowledge.
Private child tag names are global custom element names, so packages must scope
them by the owning package version (for example with a --version-<encoded>
suffix), use the same scoped name for registration and rendering, and keep
registration idempotent with customElements.get(...) guards. This preserves
side-by-side loading of multiple package versions and mirrors the IIFE behavior
where private child implementations are resolved from the owning package's
dependency tree at build time.
Players set data via properties, not attributes:
element.model = modelelement.session = sessionelement.env = env
Elements emit session changes as DOM events with the replacement session in event.detail. Authoring views may emit model changes with the replacement model in event.detail.
Publishable packages expose generated artifacts only. Package entry points must resolve to dist files except for the root controller.js compatibility shim described below.
Package exports, main, module, types, unpkg, and jsdelivr entries must point at generated dist artifacts. Packages must not publish or expose raw src, .ts, .tsx, .svelte, or .svelte.ts files as public API.
Sourcemaps may be published, but they must include source content so consumers can debug without unpacked source files.
Controller-bearing packages must publish all of these:
pie.controller:@pie-element/<name>/controllerexports["./controller"]:./dist/controller/index.jsexports["./controller.js"]: same JS and type targets as./controller- root
controller.js files: includescontroller.jsand covers thedistoutput
The root controller.js file must contain exactly:
export * from './dist/controller/index.js';Standard ESM consumers use exports["./controller"]. The root shim exists for alias-based legacy builders such as pie-api-aws.
Author-capable packages must keep ./author as the modern ESM entry and also
publish a legacy configure alias for older production builders:
pie.configure:@pie-element/<name>/configureexports["./configure"]: same JS and type targets as the author/configure implementation- root
configure.js files: includesconfigure.jsand covers thedistoutput
When the modern source entry is src/author, the legacy configure export points
at the same generated author artifact:
{
"./author": { "types": "./dist/author/index.d.ts", "default": "./dist/author/index.js" },
"./configure": { "types": "./dist/author/index.d.ts", "default": "./dist/author/index.js" }
}The root configure.js file must re-export that default class:
export { default } from './dist/author/index.js';
export * from './dist/author/index.js';Standard ESM consumers should prefer exports["./author"]. The configure alias
exists so pie-api-aws can build editor.js for legacy pie-author consumers
without learning the modern author subpath first.
Browser ESM is the player-facing module surface. Element packages that support browser ESM expose static files under:
dist/browser/delivery/index.jsdist/browser/author/index.jsdist/browser/print/index.jsdist/browser/controller/index.js
The package exports expose those files as:
./browser/delivery./browser/author./browser/print./browser/controller
Browser ESM entries must not rely on CDN transforms such as jsDelivr +esm for element package code. They are built files published by the package.
Browser ESM entries use the shared policy in tools/vite/browser-esm-policy.json:
- Bare imports are allowed only when listed in
allowedBareImports. - Shared browser singleton versions are exact and declared in
pie.browserSharedDependencies. - The browser ESM React contract is React 18. Synced packages must not preserve React 16/17 compatibility shims in browser-facing dependency policy.
dependenciesandpeerDependenciesare install metadata only; they are not browser runtime singleton contracts.- Browser JS output must stay within the policy size budget unless the policy is intentionally changed.
- Browser ESM output must not leak runtime
requirecalls. The shared browser build may rewrite known Rolldown CJS helper calls only for the allow-listed interop targets documented inPACKAGING_ARCHITECTURE.md; unsupported helper targets must fail the build.
If a new dependency should become a shared browser singleton, update tools/vite/browser-esm-policy.json, package generation, publish checks, and pie-players import-map handling in the same change.
An element may expose ./runtime-support when it has runtime support constraints. If present, the export must point at publishable files covered by files.
Packages that do not publish ./browser/* exports must expose ./runtime-support and mark browser ESM unsupported, for example supports.esm.delivery = false. This makes unsupported ESM explicit instead of letting players discover it through missing CDN files.
Packages that publish ./browser/* exports may omit ./runtime-support unless they need to disable a runtime strategy or view.
The same npm package must be usable by three runtime strategies.
pie-players loads static browser ESM entries and builds an import map for shared browser dependencies from pie.browserSharedDependencies.
When multiple elements request different minor or patch versions of a shared singleton, the player may select the highest same-major version and report the conflict through console and instrumentation. Different major versions, or attempts to upgrade a singleton after it has already been injected, fail the load and are also reported.
IIFE is a legacy runtime strategy but remains supported. Builders import package exports, including @pie-element/<name>/controller and @pie-element/<name>/configure, and may rely on the root controller.js and configure.js shims for filesystem alias compatibility.
Element package runtime entry points must not require raw source files to be present in the npm tarball.
Preloaded mode is not a separate package format. It means the expected custom element tag has already been registered by the host before the player renders.
Package modules must therefore export classes without self-registering tags. This lets ESM, IIFE, and host-preloaded flows share the same class exports while leaving tag ownership to the runtime.
Run the aggregate verifier before publishing:
bun run verify:element-contractsThe aggregate verifier runs the contract-relevant checks for:
- npm publish surface and browser ESM policy,
- controller/configure packaging and compatibility shims,
- runtime-support export coverage,
- sourcemap source content.
Release publishing and full lint checks must include this verifier so new elements and regenerated packages cannot silently drift from the contract.