Registry
Profiles are held in a process-wide registry. It is read and written only through the public functions
registerProfile, unregisterProfile, deriveProfile, getProfile and listProfiles; this page
documents what that registry is.
Contents
Section titled “Contents”- A
Mapfrom profile name to a deeply frozenProfileDefinition, seeded lazily with the five built-ins (plain-text-v1,article-v1,ui-v1,email-v1,component-template-v1). - A set of the built-in names, which is how
registerProfileandunregisterProfilerefuse to replace or remove them.
Reading it never touches a DOM global, so getProfile and listProfiles work in Node.
Shared across builds
Section titled “Shared across builds”An application can load both the ESM and CJS builds of the package (its own code as ESM, a dependency via require). Each build is
a separate module instance, so module-level state would exist twice. The registry therefore lives in one object on globalThis,
under the key Symbol.for("@johnhenry/safe-fragment/shared-state/v1"), created lazily inside functions (never at module top level),
non-enumerable and non-writable.
That object also holds the DOMPurify loader, the loader promise, and a per-window cache of DOMPurify instances. The consequences:
- A profile registered through one build is visible to the other.
- There is one DOMPurify instance per window, so the Trusted Types
dompurifypolicy is registered once (Trusted Types and CSP). SafeFragmentErroruses a duck-typedSymbol.hasInstancesoinstanceofalso holds across builds.
The repository’s test/package/dual-package.test.ts loads both built files in Node and checks all of this. Do not add other
module-level mutable state.
Semantics
Section titled “Semantics”- Names are unique. Registering an existing name throws
INVALID_PROFILE;unregisterProfileit first. Built-ins can never be replaced or unregistered. - Profiles are immutable once registered. There is no update call: register a new versioned name instead (Profiles).
- A profile is visible everywhere the name is used:
<safe-fragment profile="...">,sanitizeToFragment({ profile })andderiveProfile(base). - Registering before the first render is required for the element to find it; an unknown profile rejects with
UNKNOWN_PROFILE.