@byre/vellum 1.0.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/LICENSE +21 -0
- package/README.md +57 -0
- package/dist/index.cjs +2 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +504 -0
- package/dist/index.js.map +1 -0
- package/dist/register.cjs +2 -0
- package/dist/register.cjs.map +1 -0
- package/dist/register.js +8 -0
- package/dist/register.js.map +1 -0
- package/dist/spec.md +1292 -0
- package/package.json +58 -0
package/dist/spec.md
ADDED
|
@@ -0,0 +1,1292 @@
|
|
|
1
|
+
# Vellum Specification v2.0.0 RC3
|
|
2
|
+
|
|
3
|
+
**Version**: 1.0.0
|
|
4
|
+
**Date**: January 30, 2026
|
|
5
|
+
**Status**: Release Candidate 4
|
|
6
|
+
|
|
7
|
+
## 1. Purpose and Scope
|
|
8
|
+
|
|
9
|
+
### 1.1 What This Specification Defines
|
|
10
|
+
|
|
11
|
+
Vellum is a specification for mod loaders in web applications. It defines:
|
|
12
|
+
|
|
13
|
+
- Host and mod custom element interfaces
|
|
14
|
+
- Mod discovery and loading behavior
|
|
15
|
+
- Dependency resolution
|
|
16
|
+
- Lifecycle events
|
|
17
|
+
|
|
18
|
+
A conforming implementation provides a mod loader that adheres to this specification.
|
|
19
|
+
|
|
20
|
+
### 1.2 What This Specification Does Not Define
|
|
21
|
+
|
|
22
|
+
This specification does not define:
|
|
23
|
+
|
|
24
|
+
- Action/event systems (ecosystem mods MAY provide these)
|
|
25
|
+
- Rendering or layout management
|
|
26
|
+
- State management
|
|
27
|
+
- Logging or debugging utilities
|
|
28
|
+
- Any application-level functionality
|
|
29
|
+
|
|
30
|
+
These capabilities MAY be provided by mods and are not part of the kernel.
|
|
31
|
+
|
|
32
|
+
### 1.3 Conformance
|
|
33
|
+
|
|
34
|
+
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
|
|
35
|
+
"SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this
|
|
36
|
+
document are to be interpreted as described in RFC 2119.
|
|
37
|
+
|
|
38
|
+
### 1.4 Design Philosophy
|
|
39
|
+
|
|
40
|
+
Vellum is a kernel, not a framework. It loads mods and coordinates their
|
|
41
|
+
initialization. Everything else is delegated to mods.
|
|
42
|
+
|
|
43
|
+
**The DOM is the source of truth.** Wherever possible, Vellum state is
|
|
44
|
+
expressed through DOM structure and element attributes, not internal
|
|
45
|
+
JavaScript state. For example:
|
|
46
|
+
|
|
47
|
+
- **Add a mod element to the DOM** → mod loads
|
|
48
|
+
- **Remove mod element from DOM** → mod unloads
|
|
49
|
+
- **`disabled` attribute** → mod skipped/unloaded
|
|
50
|
+
- **`src` attribute changes** → mod reloads with new source
|
|
51
|
+
- **`loading` attribute** → mod is loading
|
|
52
|
+
- **`loaded` attribute** → mod loaded successfully
|
|
53
|
+
- **`failed` attribute** → mod failed to load
|
|
54
|
+
|
|
55
|
+
Inspecting the document reveals application structure without tracing
|
|
56
|
+
JavaScript state. DevTools, CSS selectors, and mutation observers can all
|
|
57
|
+
interact with mod lifecycle.
|
|
58
|
+
|
|
59
|
+
This declarative approach has practical benefits:
|
|
60
|
+
|
|
61
|
+
- Server-rendered HTML can include mod elements
|
|
62
|
+
- CSS can target mod states (`my-mod[loading] { opacity: 0.5; }`)
|
|
63
|
+
- Tests can assert on DOM attributes
|
|
64
|
+
- Debugging requires only element inspection
|
|
65
|
+
|
|
66
|
+
### 1.5 Architecture
|
|
67
|
+
|
|
68
|
+
Vellum provides two custom elements:
|
|
69
|
+
|
|
70
|
+
- **`<vellum-app>`** provides a host element for mod discovery and loading.
|
|
71
|
+
- **`<vellum-mod>`** is a mod element, providing a source and dependencies.
|
|
72
|
+
|
|
73
|
+
The tag names are configurable.
|
|
74
|
+
|
|
75
|
+
## 2. Definitions
|
|
76
|
+
|
|
77
|
+
- **Dependency** A mod that must be loaded before another mod can load.
|
|
78
|
+
- **Host element** The custom element that manages mod elements (e.g., `<vellum-app>`)
|
|
79
|
+
- **Lifecycle event** A CustomEvent dispatched by the host element to signal state changes.
|
|
80
|
+
- **Mod element** A custom element representing a module. Describes source and dependencies.
|
|
81
|
+
- **Mod module** The imported module object returned by `import()`
|
|
82
|
+
- **Mod script** The ES module file referenced by `src`
|
|
83
|
+
- **Priority** A number used to determine load order among peer mods. Lower values load first.
|
|
84
|
+
|
|
85
|
+
## 3. Host Element
|
|
86
|
+
|
|
87
|
+
### 3.1 Class Export
|
|
88
|
+
|
|
89
|
+
A conforming implementation MUST export a host element class that extends `HTMLElement`.
|
|
90
|
+
|
|
91
|
+
The implementation MUST NOT call `customElements.define()`. Tag name registration is the consumer's responsibility.
|
|
92
|
+
|
|
93
|
+
**Example Invocation**
|
|
94
|
+
|
|
95
|
+
_In your JavaScript_
|
|
96
|
+
```javascript
|
|
97
|
+
import { VellumApp } from '@byre/vellum';
|
|
98
|
+
customElements.define('vellum-app', VellumApp);
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
_In your HTML_
|
|
102
|
+
```html
|
|
103
|
+
<vellum-app></vellum-app>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 3.2 Attributes
|
|
107
|
+
|
|
108
|
+
| Attribute | Required | Default | Description |
|
|
109
|
+
|-----------|----------|--------------|----------------------------------------|
|
|
110
|
+
| `mod-tag` | No | `vellum-mod` | Tag name used to discover mod elements |
|
|
111
|
+
| `debug` | No | — | If present, implementation MAY emit debug events |
|
|
112
|
+
|
|
113
|
+
### 3.3 Behavior
|
|
114
|
+
|
|
115
|
+
When connected to the DOM, the host element MUST:
|
|
116
|
+
|
|
117
|
+
1. Wait for descendant elements to be parsed
|
|
118
|
+
2. Discover all descendant mod elements matching `mod-tag`, excluding:
|
|
119
|
+
- Elements with `disabled` attribute
|
|
120
|
+
- Elements that have a closer ancestor host element (see Section 3.6)
|
|
121
|
+
3. Resolve dependencies and determine load order (see Section 5)
|
|
122
|
+
4. Load each mod element in resolved order (see Section 3.5)
|
|
123
|
+
- Mod elements with all dependencies resolved MAY be loaded in parallel
|
|
124
|
+
5. Begin observing for mod element additions, removals, and attribute changes
|
|
125
|
+
|
|
126
|
+
When a mod element is added to the host's subtree after initial load:
|
|
127
|
+
|
|
128
|
+
1. The host MUST detect the addition via MutationObserver
|
|
129
|
+
2. If the mod element has `disabled` attribute, the host MUST skip it
|
|
130
|
+
3. If the mod element has a closer ancestor host, the host MUST skip it
|
|
131
|
+
4. The host MUST resolve dependencies against already-loaded mod elements
|
|
132
|
+
5. The host MUST load the new mod element
|
|
133
|
+
6. If the mod element's hard dependencies cannot be satisfied, the host MUST
|
|
134
|
+
set the `failed` attribute and dispatch `vellum:mod-failed` with a
|
|
135
|
+
`VellumDependencyError`
|
|
136
|
+
|
|
137
|
+
When a mod element is removed from the host's subtree:
|
|
138
|
+
|
|
139
|
+
1. The host MUST detect the removal via MutationObserver
|
|
140
|
+
2. If the mod element has `loaded` attribute, the host MUST unload it (see Section 3.5)
|
|
141
|
+
|
|
142
|
+
When a mod element's attributes change:
|
|
143
|
+
|
|
144
|
+
1. The host MUST observe attribute changes via MutationObserver for at least
|
|
145
|
+
`src` and `disabled` attributes
|
|
146
|
+
2. If `src` changes on a mod element with `loaded` attribute:
|
|
147
|
+
- The host MUST unload the current module
|
|
148
|
+
- The host MUST load from the new source
|
|
149
|
+
3. If `src` changes on a mod element with `loading` attribute:
|
|
150
|
+
- The host MUST abort the current load (via AbortSignal)
|
|
151
|
+
- The host MUST load from the new source
|
|
152
|
+
4. If `disabled` is added to a mod element with `loaded` attribute:
|
|
153
|
+
- The host MUST unload the module
|
|
154
|
+
5. If `disabled` is removed from a mod element without `loaded` or `loading`:
|
|
155
|
+
- The host MUST initiate loading (respecting dependencies)
|
|
156
|
+
|
|
157
|
+
When disconnected from the DOM, the host element MUST:
|
|
158
|
+
|
|
159
|
+
1. Stop observing for mod changes
|
|
160
|
+
2. Dispatch `vellum:teardown` event
|
|
161
|
+
|
|
162
|
+
### 3.4 Events Dispatched
|
|
163
|
+
|
|
164
|
+
| Event | Detail | When |
|
|
165
|
+
|---------------------------|--------------------------------------|--------------------------|
|
|
166
|
+
| `vellum:loading-started` | `{ total: number }` | Before first mod loads |
|
|
167
|
+
| `vellum:loading-complete` | `{ loaded: number, failed: number }` | After all mods processed |
|
|
168
|
+
| `vellum:teardown` | `{}` | On disconnect |
|
|
169
|
+
| `vellum:debug` | Implementation-defined | MAY be emitted if `debug` attribute present |
|
|
170
|
+
|
|
171
|
+
Events MUST bubble and MUST be composed (cross shadow DOM boundaries).
|
|
172
|
+
|
|
173
|
+
### 3.5 Loading and Unloading
|
|
174
|
+
|
|
175
|
+
The host element is solely responsible for loading and unloading mod elements.
|
|
176
|
+
Mod elements MUST NOT load themselves.
|
|
177
|
+
|
|
178
|
+
**Loading a mod element:**
|
|
179
|
+
|
|
180
|
+
1. If `loading`, `loaded`, `failed`, or `disabled` attribute is present, return
|
|
181
|
+
2. Validate attributes:
|
|
182
|
+
- If both `src` and `ref` are present, set `failed` attribute, dispatch
|
|
183
|
+
`vellum:mod-failed` with `VellumAttributeError`, and stop
|
|
184
|
+
- If neither `src` nor `ref` is present, set `failed` attribute, dispatch
|
|
185
|
+
`vellum:mod-failed` with `VellumAttributeError`, and stop
|
|
186
|
+
3. Set `loading` attribute
|
|
187
|
+
4. Obtain the mod definition:
|
|
188
|
+
- If `src` is present: Import the mod script via dynamic `import()`
|
|
189
|
+
- If `ref` is present: Resolve the key to a mod definition (resolution
|
|
190
|
+
mechanism is implementation-defined)
|
|
191
|
+
5. If obtaining the mod definition fails:
|
|
192
|
+
- Remove `loading` attribute
|
|
193
|
+
- Set `failed` attribute
|
|
194
|
+
- Dispatch `vellum:mod-failed` with `VellumLoadError` (for `src`) or
|
|
195
|
+
`VellumReferenceError` (for `ref`)
|
|
196
|
+
- Stop
|
|
197
|
+
6. If the mod definition exports a `mount` function:
|
|
198
|
+
- Create an AbortController
|
|
199
|
+
- Call `mount({ element, host, signal })` where:
|
|
200
|
+
- `element` is the mod element
|
|
201
|
+
- `host` is the host element
|
|
202
|
+
- `signal` is the AbortController's signal
|
|
203
|
+
- If `mount` throws or rejects:
|
|
204
|
+
- Remove `loading` attribute
|
|
205
|
+
- Set `failed` attribute
|
|
206
|
+
- Dispatch `vellum:mod-failed` with `VellumMountError`
|
|
207
|
+
- Stop
|
|
208
|
+
7. Remove `loading` attribute
|
|
209
|
+
8. Set `loaded` attribute
|
|
210
|
+
9. Dispatch `vellum:mod-loaded` with `{ element, module }`
|
|
211
|
+
|
|
212
|
+
**Unloading a mod element:**
|
|
213
|
+
|
|
214
|
+
1. If the `loading` attribute is present, abort the current load then continue.
|
|
215
|
+
2. If the `unloading` attribute is present, return
|
|
216
|
+
3. If neither the `loaded` nor `failed` attributes are present, return
|
|
217
|
+
4. If `loaded` attribute is present:
|
|
218
|
+
- Set `unloading` attribute
|
|
219
|
+
- If the mod definition exported an `unmount` function during load:
|
|
220
|
+
- Call `unmount({ element, host })`
|
|
221
|
+
- Remove `unloading` attribute
|
|
222
|
+
5. Remove `loading`, `loaded`, and `failed` attributes
|
|
223
|
+
6. Dispatch `vellum:mod-unloaded` with `{ element, module, wasLoaded }` where:
|
|
224
|
+
- `module` is the mod definition (or `null` if load failed before resolution)
|
|
225
|
+
- `wasLoaded` is `true` if `loaded` attribute was present, `false` otherwise
|
|
226
|
+
7. Release any references to the mod definition.
|
|
227
|
+
|
|
228
|
+
### 3.6 Nested Hosts
|
|
229
|
+
|
|
230
|
+
Host elements MAY be nested within other host elements.
|
|
231
|
+
|
|
232
|
+
A mod element belongs to its **closest ancestor host element**. When
|
|
233
|
+
discovering mod elements, a host MUST NOT process mod elements that have a
|
|
234
|
+
closer ancestor host.
|
|
235
|
+
|
|
236
|
+
```html
|
|
237
|
+
<outer-app mod-tag="my-mod">
|
|
238
|
+
<my-mod src="./a.mjs" name="a"></my-mod> <!-- belongs to outer-app -->
|
|
239
|
+
<inner-app mod-tag="my-mod">
|
|
240
|
+
<my-mod src="./b.mjs" name="b"></my-mod> <!-- belongs to inner-app -->
|
|
241
|
+
</inner-app>
|
|
242
|
+
<my-mod src="./c.mjs" name="c"></my-mod> <!-- belongs to outer-app -->
|
|
243
|
+
</outer-app>
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Each host element manages only its direct mod elements. Events from inner
|
|
247
|
+
hosts bubble normally and MAY be observed by outer hosts.
|
|
248
|
+
|
|
249
|
+
Host elements MAY catch events from within their own subtree and prevent
|
|
250
|
+
them from propagating.
|
|
251
|
+
|
|
252
|
+
A host MUST NOT wait for nested hosts to complete before dispatching its
|
|
253
|
+
own `vellum:loading-complete` event.
|
|
254
|
+
|
|
255
|
+
### 3.7 Registration Requirements
|
|
256
|
+
|
|
257
|
+
Consumers MUST register both the host element class and the mod element class
|
|
258
|
+
as custom elements before connecting the host element to the DOM.
|
|
259
|
+
|
|
260
|
+
The tag name used for `customElements.define()` on the mod element class MUST
|
|
261
|
+
match the host element's `mod-tag` attribute (or the default `vellum-mod`).
|
|
262
|
+
|
|
263
|
+
If the host encounters elements matching `mod-tag` that are not registered
|
|
264
|
+
custom elements, the host SHOULD log a warning and skip those elements.
|
|
265
|
+
|
|
266
|
+
## 4. Mod Element
|
|
267
|
+
|
|
268
|
+
### 4.1 Purpose
|
|
269
|
+
|
|
270
|
+
The mod element is a declarative marker representing a loadable module. It
|
|
271
|
+
carries configuration (source URL, dependencies, priority) but does not
|
|
272
|
+
perform loading itself.
|
|
273
|
+
|
|
274
|
+
**Loading is the sole responsibility of the host element.**
|
|
275
|
+
|
|
276
|
+
The mod element is inert until processed by a host.
|
|
277
|
+
|
|
278
|
+
### 4.2 Class Export
|
|
279
|
+
|
|
280
|
+
A conforming implementation MUST export a mod element class that extends
|
|
281
|
+
`HTMLElement`.
|
|
282
|
+
|
|
283
|
+
The implementation MUST NOT call `customElements.define()`. Tag name
|
|
284
|
+
registration is the consumer's responsibility.
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
### 4.3 Attributes
|
|
288
|
+
|
|
289
|
+
| Attribute | Type | Required | Default | Description |
|
|
290
|
+
|------------|--------|----------|----------------|-----------------------------------------------|
|
|
291
|
+
| `src` | Input | No* | — | URL of the mod script |
|
|
292
|
+
| `ref` | Input | No* | — | Key identifying a pre-registered mod definition |
|
|
293
|
+
| `name` | Input | No | Value of `src` or `ref` | Unique identifier for dependency resolution |
|
|
294
|
+
| `require` | Input | No | — | Comma-separated list of hard dependencies |
|
|
295
|
+
| `after` | Input | No | — | Comma-separated list of soft dependencies |
|
|
296
|
+
| `priority` | Input | No | — | Integer; lower values load first among peers |
|
|
297
|
+
| `disabled` | Input | No | — | If present, mod will not load |
|
|
298
|
+
| `loading` | Output | — | — | Set by host while load is in progress |
|
|
299
|
+
| `loaded` | Output | — | — | Set by host on successful load |
|
|
300
|
+
| `failed` | Output | — | — | Set by host on failed load |
|
|
301
|
+
| `unloading`| Output | — | — | Set by host while unload is in progress |
|
|
302
|
+
|
|
303
|
+
*Exactly one of `src` or `ref` MUST be present. If both are present or neither
|
|
304
|
+
is present, the host MUST set the `failed` attribute and dispatch
|
|
305
|
+
`vellum:mod-failed` with a `VellumAttributeError`.
|
|
306
|
+
|
|
307
|
+
The `loading`, `loaded`, `failed`, and `unloading` attributes are reserved
|
|
308
|
+
and output-only. They MUST NOT be set by consumers. They are managed
|
|
309
|
+
exclusively by the host element.
|
|
310
|
+
|
|
311
|
+
**Hard vs Soft Dependencies:**
|
|
312
|
+
|
|
313
|
+
- `require`: The mod element MUST NOT load until all required mod elements have
|
|
314
|
+
loaded successfully. If a required mod element is missing or fails, this mod
|
|
315
|
+
element MUST fail.
|
|
316
|
+
|
|
317
|
+
- `after`: The mod element SHOULD load after these mod elements if they exist
|
|
318
|
+
and are loading/loaded. If an `after` target is missing, this mod element
|
|
319
|
+
loads anyway. If an `after` target fails, this mod element still attempts
|
|
320
|
+
to load.
|
|
321
|
+
|
|
322
|
+
**Priority:**
|
|
323
|
+
|
|
324
|
+
The `priority` attribute serves as a tiebreaker when dependency order does
|
|
325
|
+
not determine load sequence:
|
|
326
|
+
|
|
327
|
+
1. For mod elements with equivalent dependency status, lower `priority` values
|
|
328
|
+
load first
|
|
329
|
+
2. If priorities are equal, DOM order determines sequence
|
|
330
|
+
|
|
331
|
+
### 4.4 Behavior
|
|
332
|
+
|
|
333
|
+
The mod element has no autonomous behavior. It MUST NOT initiate loading,
|
|
334
|
+
unloading, or any lifecycle operations.
|
|
335
|
+
|
|
336
|
+
When connected to the DOM, the mod element remains inert until the host
|
|
337
|
+
element processes it.
|
|
338
|
+
|
|
339
|
+
When disconnected from the DOM, the host element is responsible for detecting
|
|
340
|
+
the removal and performing cleanup (see Section 3.3).
|
|
341
|
+
|
|
342
|
+
The mod element MAY provide convenience getters for attribute access:
|
|
343
|
+
|
|
344
|
+
```javascript
|
|
345
|
+
get src() { return this.getAttribute('src'); }
|
|
346
|
+
get name() { return this.getAttribute('name') || this.src; }
|
|
347
|
+
get dependencies() {
|
|
348
|
+
const req = this.getAttribute('require');
|
|
349
|
+
return req ? req.split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
350
|
+
}
|
|
351
|
+
get softDependencies() {
|
|
352
|
+
const after = this.getAttribute('after');
|
|
353
|
+
return after ? after.split(',').map(s => s.trim()).filter(Boolean) : [];
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### 4.5 Child Content
|
|
358
|
+
|
|
359
|
+
Mod elements MAY contain child elements. This specification does not define
|
|
360
|
+
semantics for child content.
|
|
361
|
+
|
|
362
|
+
Mod scripts MAY access children via standard DOM APIs on the `element`
|
|
363
|
+
parameter passed to `mount()`:
|
|
364
|
+
|
|
365
|
+
```javascript
|
|
366
|
+
export function mount({ element, host, signal }) {
|
|
367
|
+
const template = element.querySelector('template');
|
|
368
|
+
const config = element.querySelector('script[type="application/json"]');
|
|
369
|
+
// ...
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
### 4.6 Events
|
|
374
|
+
|
|
375
|
+
All lifecycle events are dispatched by the host element, using the mod
|
|
376
|
+
element as the event target:
|
|
377
|
+
|
|
378
|
+
| Event | Detail | When |
|
|
379
|
+
|-----------------------|-------------------------------------------------|---------------------------------|
|
|
380
|
+
| `vellum:mod-loaded` | `{ element: HTMLElement, module: object }` | After successful load and mount |
|
|
381
|
+
| `vellum:mod-failed` | `{ element: HTMLElement, error: Error }` | After load or mount failure |
|
|
382
|
+
| `vellum:mod-unloaded` | `{ element: HTMLElement, module: object, wasLoaded: boolean }` | After unload |
|
|
383
|
+
|
|
384
|
+
Events MUST bubble and MUST be composed.
|
|
385
|
+
|
|
386
|
+
### 4.7 State Machine
|
|
387
|
+
|
|
388
|
+
The mod element's state is represented by output attributes. The following
|
|
389
|
+
state transitions are valid:
|
|
390
|
+
|
|
391
|
+
```
|
|
392
|
+
┌─────────────────────────────────┐
|
|
393
|
+
│ │
|
|
394
|
+
▼ │
|
|
395
|
+
┌─────────┐ host triggers ┌───────────┐ import+mount ┌────────┐ │
|
|
396
|
+
│ Initial │ ───────────────►│ Loading │ ───────────────►│ Loaded │ │
|
|
397
|
+
└─────────┘ └───────────┘ └────────┘ │
|
|
398
|
+
▲ │ │ │
|
|
399
|
+
│ │ import or │ │
|
|
400
|
+
│ │ mount fails │ │
|
|
401
|
+
│ ▼ │ │
|
|
402
|
+
│ ┌────────┐ │ │
|
|
403
|
+
│ DOM removal │ Failed │ │ │
|
|
404
|
+
└───────────────────────┴────────┘ │ │
|
|
405
|
+
▲ │ │
|
|
406
|
+
│ ┌───────────┐ │ │
|
|
407
|
+
│ unmount completes │ Unloading │◄────────────────────┘ │
|
|
408
|
+
└───────────────────────┴───────────┘ DOM removal / │
|
|
409
|
+
disabled / │
|
|
410
|
+
src change │
|
|
411
|
+
│
|
|
412
|
+
▲ │
|
|
413
|
+
│ disabled present (no-op) │
|
|
414
|
+
└────────────────────────────────────────────────────────────────┘
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
**State-to-attribute mapping:**
|
|
418
|
+
|
|
419
|
+
| State | Attributes Present |
|
|
420
|
+
|-----------|--------------------|
|
|
421
|
+
| Initial | (none) |
|
|
422
|
+
| Loading | `loading` |
|
|
423
|
+
| Loaded | `loaded` |
|
|
424
|
+
| Failed | `failed` |
|
|
425
|
+
| Unloading | `unloading` |
|
|
426
|
+
|
|
427
|
+
A mod element MUST have at most one of these output attributes present.
|
|
428
|
+
|
|
429
|
+
## 5. Dependency Resolution
|
|
430
|
+
|
|
431
|
+
### 5.1 Resolution Algorithm
|
|
432
|
+
|
|
433
|
+
The host MUST resolve mod load order using:
|
|
434
|
+
|
|
435
|
+
1. **Hard dependencies (`require`)**: Mod elements MUST load after all required
|
|
436
|
+
mod elements have successfully loaded
|
|
437
|
+
2. **Soft dependencies (`after`)**: Mod elements SHOULD load after listed mod
|
|
438
|
+
elements if those mod elements exist and haven't failed
|
|
439
|
+
3. **Priority**: Among mod elements with satisfied dependencies, lower
|
|
440
|
+
`priority` values load first
|
|
441
|
+
4. **DOM order**: Among mod elements with equal priority, earlier DOM position
|
|
442
|
+
loads first
|
|
443
|
+
|
|
444
|
+
### 5.2 Hard Dependency Failures
|
|
445
|
+
|
|
446
|
+
If a mod element's required dependency (via `require`) is missing or has failed:
|
|
447
|
+
|
|
448
|
+
- The mod element MUST NOT attempt to load
|
|
449
|
+
- The host MUST set the `failed` attribute on the mod element
|
|
450
|
+
- The host MUST dispatch `vellum:mod-failed` with a `VellumDependencyError`
|
|
451
|
+
|
|
452
|
+
### 5.3 Soft Dependency Handling
|
|
453
|
+
|
|
454
|
+
If a mod element's `after` dependency is missing:
|
|
455
|
+
|
|
456
|
+
- The mod element MUST proceed as if the dependency was satisfied
|
|
457
|
+
- The mod element loads in its normal priority/DOM order position
|
|
458
|
+
|
|
459
|
+
If a mod element's `after` dependency has failed:
|
|
460
|
+
|
|
461
|
+
- The mod element MUST still attempt to load
|
|
462
|
+
- The mod script's `mount` function MAY check for the failed dependency if needed
|
|
463
|
+
|
|
464
|
+
Mod scripts using `after` dependencies SHOULD check for dependent mod state
|
|
465
|
+
if they require conditional behavior based on whether the soft dependency
|
|
466
|
+
loaded successfully.
|
|
467
|
+
|
|
468
|
+
### 5.4 Circular Dependencies
|
|
469
|
+
|
|
470
|
+
Circular dependencies in `require` attributes produce a fatal error.
|
|
471
|
+
|
|
472
|
+
If circular hard dependencies are detected:
|
|
473
|
+
|
|
474
|
+
- The host MUST NOT attempt to load any mod element in the cycle
|
|
475
|
+
- The host MUST set the `failed` attribute on all mod elements in the cycle
|
|
476
|
+
- The host MUST dispatch `vellum:mod-failed` for each with a
|
|
477
|
+
`VellumCircularDependencyError`
|
|
478
|
+
- The error message MUST include the cycle path (e.g., "a → b → c → a")
|
|
479
|
+
|
|
480
|
+
Circular dependencies in `after` attributes are permitted and do not cause
|
|
481
|
+
errors, since `after` is non-blocking.
|
|
482
|
+
|
|
483
|
+
### 5.5 Dynamic Mod Elements
|
|
484
|
+
|
|
485
|
+
When a mod element is added after initial load:
|
|
486
|
+
|
|
487
|
+
- Its `require` dependencies MUST already be loaded (or the mod element fails
|
|
488
|
+
with `VellumDependencyError`)
|
|
489
|
+
- Its `after` dependencies refer to currently loaded or loading mod elements;
|
|
490
|
+
missing targets are treated as satisfied
|
|
491
|
+
- Already-loaded mod elements MUST NOT be reloaded
|
|
492
|
+
- The new mod element MUST wait for any pending mod elements it depends on
|
|
493
|
+
|
|
494
|
+
## 6. Mod Script Interface
|
|
495
|
+
|
|
496
|
+
### 6.1 Module Format
|
|
497
|
+
|
|
498
|
+
Mod scripts MUST be ES modules.
|
|
499
|
+
|
|
500
|
+
All valid ES module specifiers are permitted for the `src` attribute,
|
|
501
|
+
including:
|
|
502
|
+
|
|
503
|
+
- Relative URLs (resolved relative to the document)
|
|
504
|
+
- Absolute URLs
|
|
505
|
+
- Data URLs
|
|
506
|
+
- Blob URLs
|
|
507
|
+
|
|
508
|
+
Cross-origin mod scripts are subject to CORS restrictions per the ES module
|
|
509
|
+
specification.
|
|
510
|
+
|
|
511
|
+
### 6.2 Exports
|
|
512
|
+
|
|
513
|
+
#### 6.2.1 `mount` function
|
|
514
|
+
|
|
515
|
+
Mod scripts MAY export a `mount` function. The `mount` function MAY be async.
|
|
516
|
+
|
|
517
|
+
```typescript
|
|
518
|
+
mount(context: {
|
|
519
|
+
element: HTMLElement,
|
|
520
|
+
host: HTMLElement,
|
|
521
|
+
signal: AbortSignal
|
|
522
|
+
}): void | Promise<void>
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
Parameters (passed as a single object):
|
|
526
|
+
|
|
527
|
+
- `element`: The mod element that initiated the load
|
|
528
|
+
- `host`: The host element coordinating the load
|
|
529
|
+
- `signal`: An AbortSignal that is aborted if the load is cancelled (e.g.,
|
|
530
|
+
due to `src` attribute change or element removal during load)
|
|
531
|
+
|
|
532
|
+
If `mount` is not exported, the mod script runs its top-level code only.
|
|
533
|
+
|
|
534
|
+
The `mount` function SHOULD:
|
|
535
|
+
|
|
536
|
+
- Store the `host` reference if needed for later access
|
|
537
|
+
- Attach an abort listener to `signal` if cleanup is needed on cancellation
|
|
538
|
+
- Return or resolve when initialization is complete
|
|
539
|
+
|
|
540
|
+
#### 6.2.2 `unmount` function
|
|
541
|
+
|
|
542
|
+
Mod scripts MAY export an `unmount` function for cleanup:
|
|
543
|
+
|
|
544
|
+
```typescript
|
|
545
|
+
unmount(context: {
|
|
546
|
+
element: HTMLElement,
|
|
547
|
+
host: HTMLElement
|
|
548
|
+
}): void | Promise<void>
|
|
549
|
+
```
|
|
550
|
+
|
|
551
|
+
Parameters (passed as a single object):
|
|
552
|
+
|
|
553
|
+
- `element`: The mod element being unloaded
|
|
554
|
+
- `host`: The host element coordinating the unload
|
|
555
|
+
|
|
556
|
+
The `unmount` function is called when:
|
|
557
|
+
|
|
558
|
+
- The mod element is removed from the DOM
|
|
559
|
+
- The `disabled` attribute is added
|
|
560
|
+
- The `src` attribute changes
|
|
561
|
+
- (There is no public `unload()` method; these are the only triggers)
|
|
562
|
+
|
|
563
|
+
The `unmount` function SHOULD:
|
|
564
|
+
|
|
565
|
+
- Remove event listeners
|
|
566
|
+
- Cancel pending operations
|
|
567
|
+
- Release resources
|
|
568
|
+
- Clean up any DOM modifications
|
|
569
|
+
|
|
570
|
+
If `unmount` is not exported, no cleanup is performed.
|
|
571
|
+
|
|
572
|
+
#### 6.2.3 Other exports
|
|
573
|
+
|
|
574
|
+
A mod script MAY export any other functions or variables. The host MUST NOT
|
|
575
|
+
store or access exports other than `mount` and `unmount`.
|
|
576
|
+
|
|
577
|
+
### 6.3 Dispatching Events
|
|
578
|
+
|
|
579
|
+
Mod scripts communicate via native DOM events:
|
|
580
|
+
|
|
581
|
+
```javascript
|
|
582
|
+
export async function mount({ element, host, signal }) {
|
|
583
|
+
element.dispatchEvent(new CustomEvent('my:event', {
|
|
584
|
+
bubbles: true,
|
|
585
|
+
composed: true,
|
|
586
|
+
detail: { /* ... */ }
|
|
587
|
+
}));
|
|
588
|
+
}
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
No toolkit or wrapper is required.
|
|
592
|
+
|
|
593
|
+
## 7. Event Namespacing
|
|
594
|
+
|
|
595
|
+
All events defined by this specification use the `vellum:` prefix.
|
|
596
|
+
|
|
597
|
+
| Event | Source | Target | Purpose |
|
|
598
|
+
|---------------------------|--------|-------------|-------------------------|
|
|
599
|
+
| `vellum:loading-started` | Host | Host | Loading phase beginning |
|
|
600
|
+
| `vellum:loading-complete` | Host | Host | Loading phase complete |
|
|
601
|
+
| `vellum:teardown` | Host | Host | Host disconnecting |
|
|
602
|
+
| `vellum:debug` | Host | Host | Debug information |
|
|
603
|
+
| `vellum:mod-loaded` | Host | Mod element | Mod successfully loaded |
|
|
604
|
+
| `vellum:mod-failed` | Host | Mod element | Mod failed to load |
|
|
605
|
+
| `vellum:mod-unloaded` | Host | Mod element | Mod unloaded |
|
|
606
|
+
|
|
607
|
+
Implementations MUST use these exact event names.
|
|
608
|
+
|
|
609
|
+
Ecosystem mods MAY define additional events with their own namespaces.
|
|
610
|
+
|
|
611
|
+
### 7.1 Reserved Namespaces
|
|
612
|
+
|
|
613
|
+
The following attribute and event prefixes are reserved:
|
|
614
|
+
|
|
615
|
+
- `vellum-*` attributes: Reserved for future specification use
|
|
616
|
+
- `vellum:*` events: Reserved for specification-defined events
|
|
617
|
+
|
|
618
|
+
Consumers SHOULD use `data-*` attributes for custom metadata on mod elements.
|
|
619
|
+
Consumers MAY use any valid, HTML-compliant attributes on mod elements.
|
|
620
|
+
|
|
621
|
+
Ecosystem mods SHOULD use their own event namespace (e.g., `mymod:event`).
|
|
622
|
+
|
|
623
|
+
## 8. Error Types
|
|
624
|
+
|
|
625
|
+
Implementations MUST define and throw the following error types:
|
|
626
|
+
|
|
627
|
+
### 8.1 VellumError
|
|
628
|
+
|
|
629
|
+
Base class for all Vellum errors.
|
|
630
|
+
|
|
631
|
+
```typescript
|
|
632
|
+
class VellumError extends Error {
|
|
633
|
+
name: 'VellumError';
|
|
634
|
+
cause?: Error;
|
|
635
|
+
}
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
### 8.2 VellumDependencyError
|
|
639
|
+
|
|
640
|
+
Thrown when a mod element's hard dependencies cannot be satisfied.
|
|
641
|
+
|
|
642
|
+
```typescript
|
|
643
|
+
class VellumDependencyError extends VellumError {
|
|
644
|
+
name: 'VellumDependencyError';
|
|
645
|
+
modName: string;
|
|
646
|
+
missingDependencies: string[];
|
|
647
|
+
}
|
|
648
|
+
```
|
|
649
|
+
|
|
650
|
+
Message format: `Mod "{modName}" has unmet dependencies: {deps.join(', ')}`
|
|
651
|
+
|
|
652
|
+
### 8.3 VellumCircularDependencyError
|
|
653
|
+
|
|
654
|
+
Thrown when circular hard dependencies are detected.
|
|
655
|
+
|
|
656
|
+
```typescript
|
|
657
|
+
class VellumCircularDependencyError extends VellumError {
|
|
658
|
+
name: 'VellumCircularDependencyError';
|
|
659
|
+
cycle: string[];
|
|
660
|
+
}
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
Message format: `Circular dependency detected: {cycle.join(' → ')}`
|
|
664
|
+
|
|
665
|
+
### 8.4 VellumLoadError
|
|
666
|
+
|
|
667
|
+
Thrown when a mod script fails to import.
|
|
668
|
+
|
|
669
|
+
```typescript
|
|
670
|
+
class VellumLoadError extends VellumError {
|
|
671
|
+
name: 'VellumLoadError';
|
|
672
|
+
src: string;
|
|
673
|
+
}
|
|
674
|
+
```
|
|
675
|
+
|
|
676
|
+
Message format: `Failed to load mod from "{src}"`
|
|
677
|
+
|
|
678
|
+
### 8.5 VellumMountError
|
|
679
|
+
|
|
680
|
+
Thrown when a mod script's `mount` function fails.
|
|
681
|
+
|
|
682
|
+
```typescript
|
|
683
|
+
class VellumMountError extends VellumError {
|
|
684
|
+
name: 'VellumMountError';
|
|
685
|
+
modName: string;
|
|
686
|
+
}
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
Message format: `Mount failed for mod "{modName}"`
|
|
690
|
+
|
|
691
|
+
### 8.6 VellumAttributeError
|
|
692
|
+
|
|
693
|
+
Thrown when a mod element has invalid attribute configuration.
|
|
694
|
+
```typescript
|
|
695
|
+
class VellumAttributeError extends VellumError {
|
|
696
|
+
name: 'VellumAttributeError';
|
|
697
|
+
modName: string;
|
|
698
|
+
}
|
|
699
|
+
```
|
|
700
|
+
|
|
701
|
+
Message format (both present): `Mod "{modName}" has both src and ref attributes; exactly one is required`
|
|
702
|
+
|
|
703
|
+
Message format (neither present): `Mod "{modName}" has neither src nor ref attribute; exactly one is required`
|
|
704
|
+
|
|
705
|
+
### 8.7 VellumReferenceError
|
|
706
|
+
|
|
707
|
+
Thrown when a mod element's `ref` attribute cannot be resolved.
|
|
708
|
+
```typescript
|
|
709
|
+
class VellumReferenceError extends VellumError {
|
|
710
|
+
name: 'VellumReferenceError';
|
|
711
|
+
ref: string;
|
|
712
|
+
}
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
Message format: `Could not resolve mod reference "{ref}"`
|
|
716
|
+
|
|
717
|
+
## 9. Security Considerations
|
|
718
|
+
|
|
719
|
+
### 9.1 Content Security Policy
|
|
720
|
+
|
|
721
|
+
Mod scripts are loaded via dynamic `import()`. For this to succeed, the mod
|
|
722
|
+
script's origin MUST be permitted by the document's `script-src` CSP directive.
|
|
723
|
+
|
|
724
|
+
Implementations MUST NOT bypass or weaken CSP restrictions.
|
|
725
|
+
|
|
726
|
+
Example CSP header allowing mod scripts from same origin and a trusted CDN:
|
|
727
|
+
|
|
728
|
+
```
|
|
729
|
+
Content-Security-Policy: script-src 'self' https://cdn.example.com
|
|
730
|
+
```
|
|
731
|
+
|
|
732
|
+
### 9.2 Subresource Integrity
|
|
733
|
+
|
|
734
|
+
This specification does not require SRI support for mod scripts.
|
|
735
|
+
|
|
736
|
+
Implementations MAY support an `integrity` attribute on mod elements:
|
|
737
|
+
|
|
738
|
+
```html
|
|
739
|
+
<my-mod src="./x.mjs" integrity="sha384-..."></my-mod>
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
If supported, implementations MUST reject modules that fail integrity checks
|
|
743
|
+
and MUST set the `failed` attribute with a `VellumLoadError`.
|
|
744
|
+
|
|
745
|
+
### 9.3 Cross-Origin Loading
|
|
746
|
+
|
|
747
|
+
Mod scripts loaded from different origins are subject to CORS restrictions
|
|
748
|
+
per the ES module specification.
|
|
749
|
+
|
|
750
|
+
Cross-origin scripts MUST be served with appropriate headers:
|
|
751
|
+
|
|
752
|
+
- `Access-Control-Allow-Origin` matching the document origin or `*`
|
|
753
|
+
|
|
754
|
+
If CORS checks fail, the host MUST set the `failed` attribute and dispatch
|
|
755
|
+
`vellum:mod-failed` with a `VellumLoadError`.
|
|
756
|
+
|
|
757
|
+
### 9.4 Module Isolation
|
|
758
|
+
|
|
759
|
+
Implementations SHOULD NOT store arbitrary properties from loaded mod modules.
|
|
760
|
+
Only the `mount` and `unmount` exports (if present) should be retained and
|
|
761
|
+
invoked.
|
|
762
|
+
|
|
763
|
+
This mitigates prototype pollution risks from malicious mod scripts.
|
|
764
|
+
|
|
765
|
+
## 10. Conformance
|
|
766
|
+
|
|
767
|
+
### 10.1 Implementation Conformance
|
|
768
|
+
|
|
769
|
+
A conforming Vellum implementation MUST:
|
|
770
|
+
|
|
771
|
+
1. Export a host element class meeting Section 3 requirements
|
|
772
|
+
2. Export a mod element class meeting Section 4 requirements
|
|
773
|
+
3. Implement dependency resolution per Section 5
|
|
774
|
+
4. Dispatch all required events per Section 7
|
|
775
|
+
5. Define all error types per Section 8
|
|
776
|
+
|
|
777
|
+
A conforming implementation MUST NOT:
|
|
778
|
+
|
|
779
|
+
1. Call `customElements.define()` for either element class
|
|
780
|
+
2. Expose a `load()` method on mod elements
|
|
781
|
+
3. Allow mod elements to initiate their own loading
|
|
782
|
+
|
|
783
|
+
### 10.2 Consumer Conformance
|
|
784
|
+
|
|
785
|
+
A conforming Vellum consumer MUST:
|
|
786
|
+
|
|
787
|
+
1. Register both host and mod element classes as custom elements
|
|
788
|
+
2. Use matching tag names (host's `mod-tag` matches mod element registration)
|
|
789
|
+
3. Register both element classes before connecting the host to the DOM
|
|
790
|
+
|
|
791
|
+
### 10.3 Mod Script Conformance
|
|
792
|
+
|
|
793
|
+
Mod scripts have no conformance requirements. They MAY export `mount`
|
|
794
|
+
and/or `unmount`. They MAY dispatch events.
|
|
795
|
+
|
|
796
|
+
Mod scripts MAY provide user interface elements.
|
|
797
|
+
|
|
798
|
+
## 11. Versioning
|
|
799
|
+
|
|
800
|
+
### 11.1 Specification Versioning
|
|
801
|
+
|
|
802
|
+
This specification follows Semantic Versioning 2.0.0:
|
|
803
|
+
|
|
804
|
+
- **MAJOR**: Breaking changes to element interfaces or required behaviors
|
|
805
|
+
- **MINOR**: New optional features, attributes, or recommendations
|
|
806
|
+
- **PATCH**: Clarifications and corrections
|
|
807
|
+
|
|
808
|
+
### 11.2 Implementation Versioning
|
|
809
|
+
|
|
810
|
+
Implementations SHOULD indicate which specification version they conform to.
|
|
811
|
+
|
|
812
|
+
---
|
|
813
|
+
|
|
814
|
+
## Appendix A: Glossary
|
|
815
|
+
|
|
816
|
+
**Host Element**: The coordinating custom element that manages mod loading.
|
|
817
|
+
|
|
818
|
+
**Mod Element**: A custom element representing a single loadable module. Inert
|
|
819
|
+
until processed by a host.
|
|
820
|
+
|
|
821
|
+
**Mod Script**: The ES module file loaded by a mod element.
|
|
822
|
+
|
|
823
|
+
**Mod Module**: The JavaScript module object returned by `import()`.
|
|
824
|
+
|
|
825
|
+
**Kernel**: The core Vellum implementation (host + mod elements). Provides
|
|
826
|
+
loading only.
|
|
827
|
+
|
|
828
|
+
**Ecosystem Mod**: A mod script providing reusable functionality (e.g.,
|
|
829
|
+
actions, routing, rendering).
|
|
830
|
+
|
|
831
|
+
### Mod Element
|
|
832
|
+
```
|
|
833
|
+
Attributes (input):
|
|
834
|
+
src Module URL (required if ref not present)
|
|
835
|
+
ref Registry key (required if src not present)
|
|
836
|
+
name Identifier for dependencies
|
|
837
|
+
require Comma-separated hard dependency names
|
|
838
|
+
after Comma-separated soft dependency names
|
|
839
|
+
priority Load order tiebreaker (lower = earlier)
|
|
840
|
+
disabled If present, mod will not load
|
|
841
|
+
|
|
842
|
+
Attributes (output, managed by host):
|
|
843
|
+
loading Present while load is in progress
|
|
844
|
+
loaded Present after successful load
|
|
845
|
+
failed Present after failed load
|
|
846
|
+
unloading Present while unload is in progress
|
|
847
|
+
|
|
848
|
+
Methods:
|
|
849
|
+
(none - loading is host's responsibility)
|
|
850
|
+
|
|
851
|
+
Events (dispatched by host on this element):
|
|
852
|
+
vellum:mod-loaded { element, module }
|
|
853
|
+
vellum:mod-failed { element, error }
|
|
854
|
+
vellum:mod-unloaded { element, module, wasLoaded }
|
|
855
|
+
|
|
856
|
+
Behavior:
|
|
857
|
+
- Inert until processed by host
|
|
858
|
+
- No autonomous lifecycle operations
|
|
859
|
+
- Exactly one of src or ref must be present
|
|
860
|
+
- Child content accessible to mod script via element parameter
|
|
861
|
+
```
|
|
862
|
+
|
|
863
|
+
## Appendix C: Example Usages
|
|
864
|
+
|
|
865
|
+
### Basic Setup
|
|
866
|
+
|
|
867
|
+
```javascript
|
|
868
|
+
// app.js
|
|
869
|
+
import { VellumHost, VellumMod } from '@byre/vellum';
|
|
870
|
+
|
|
871
|
+
customElements.define('my-app', VellumHost);
|
|
872
|
+
customElements.define('my-mod', VellumMod);
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
```html
|
|
876
|
+
<my-app mod-tag="my-mod">
|
|
877
|
+
<my-mod src="./layout.mjs" name="layout"></my-mod>
|
|
878
|
+
<my-mod src="./header.mjs" name="header" require="layout"></my-mod>
|
|
879
|
+
<my-mod src="./analytics.mjs" name="analytics" after="layout"></my-mod>
|
|
880
|
+
<my-mod src="./content.mjs" require="layout" after="analytics"></my-mod>
|
|
881
|
+
<my-mod src="./experimental.mjs" disabled></my-mod>
|
|
882
|
+
</my-app>
|
|
883
|
+
```
|
|
884
|
+
|
|
885
|
+
In this example:
|
|
886
|
+
- `header` requires `layout` (fails if layout fails)
|
|
887
|
+
- `analytics` loads after `layout` if present (but would work without it)
|
|
888
|
+
- `content` requires `layout`, and loads after `analytics` if present
|
|
889
|
+
- `experimental` is present but won't load until `disabled` is removed
|
|
890
|
+
|
|
891
|
+
### Mod Script with Mount and Unmount
|
|
892
|
+
|
|
893
|
+
```javascript
|
|
894
|
+
// layout.mjs
|
|
895
|
+
let container;
|
|
896
|
+
let hostRef;
|
|
897
|
+
|
|
898
|
+
export function mount({ element, host, signal }) {
|
|
899
|
+
hostRef = host;
|
|
900
|
+
|
|
901
|
+
// Handle cancellation
|
|
902
|
+
signal.addEventListener('abort', () => {
|
|
903
|
+
container?.remove();
|
|
904
|
+
container = null;
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
container = document.createElement('div');
|
|
908
|
+
container.id = 'layout-root';
|
|
909
|
+
host.appendChild(container);
|
|
910
|
+
|
|
911
|
+
element.dispatchEvent(new CustomEvent('layout:ready', {
|
|
912
|
+
bubbles: true,
|
|
913
|
+
composed: true,
|
|
914
|
+
detail: { container }
|
|
915
|
+
}));
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
export function unmount({ element, host }) {
|
|
919
|
+
container?.remove();
|
|
920
|
+
container = null;
|
|
921
|
+
hostRef = null;
|
|
922
|
+
}
|
|
923
|
+
```
|
|
924
|
+
|
|
925
|
+
### Mod Script Listening to Another Mod
|
|
926
|
+
|
|
927
|
+
```javascript
|
|
928
|
+
// header.mjs
|
|
929
|
+
let headerElement;
|
|
930
|
+
|
|
931
|
+
export function mount({ element, host, signal }) {
|
|
932
|
+
// Wait for layout to be ready
|
|
933
|
+
host.addEventListener('layout:ready', (e) => {
|
|
934
|
+
headerElement = document.createElement('header');
|
|
935
|
+
headerElement.textContent = 'My Application';
|
|
936
|
+
e.detail.container.prepend(headerElement);
|
|
937
|
+
}, { once: true });
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
export function unmount({ element, host }) {
|
|
941
|
+
headerElement?.remove();
|
|
942
|
+
headerElement = null;
|
|
943
|
+
}
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
### Bundle Registration
|
|
947
|
+
```javascript
|
|
948
|
+
// mybundle.mjs - a mod that registers other mods
|
|
949
|
+
export function mount({ element, host, signal }) {
|
|
950
|
+
// Registration mechanism is implementation-defined
|
|
951
|
+
// This example assumes host.register() exists
|
|
952
|
+
host.register('mybundle.mod-a', {
|
|
953
|
+
mount({ element, host, signal }) {
|
|
954
|
+
console.log('mod-a mounted');
|
|
955
|
+
},
|
|
956
|
+
unmount({ element, host }) {
|
|
957
|
+
console.log('mod-a unmounted');
|
|
958
|
+
}
|
|
959
|
+
});
|
|
960
|
+
|
|
961
|
+
host.register('mybundle.mod-b', {
|
|
962
|
+
mount({ element, host, signal }) {
|
|
963
|
+
console.log('mod-b mounted');
|
|
964
|
+
}
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
```html
|
|
970
|
+
<!-- Load the bundle first -->
|
|
971
|
+
<my-mod src="mybundle.mjs" name="mybundle"></my-mod>
|
|
972
|
+
|
|
973
|
+
<!-- These resolve from the bundle's registrations -->
|
|
974
|
+
<my-mod ref="mybundle.mod-a" require="mybundle"></my-mod>
|
|
975
|
+
<my-mod ref="mybundle.mod-b" require="mybundle"></my-mod>
|
|
976
|
+
```
|
|
977
|
+
|
|
978
|
+
In this example:
|
|
979
|
+
- `mybundle` loads via `src` and registers two mod definitions during mount
|
|
980
|
+
- `mybundle.mod-a` and `mybundle.mod-b` use `ref` to resolve from registrations
|
|
981
|
+
- `require="mybundle"` ensures the bundle mounts before resolution is attempted
|
|
982
|
+
- The registration mechanism (`host.register()`) is implementation-defined
|
|
983
|
+
|
|
984
|
+
### Using Child Content
|
|
985
|
+
|
|
986
|
+
```html
|
|
987
|
+
<my-mod src="./configurable.mjs" name="configurable">
|
|
988
|
+
<script type="application/json">
|
|
989
|
+
{ "theme": "dark", "language": "en" }
|
|
990
|
+
</script>
|
|
991
|
+
<template>
|
|
992
|
+
<div class="custom-content">
|
|
993
|
+
<slot></slot>
|
|
994
|
+
</div>
|
|
995
|
+
</template>
|
|
996
|
+
</my-mod>
|
|
997
|
+
```
|
|
998
|
+
|
|
999
|
+
```javascript
|
|
1000
|
+
// configurable.mjs
|
|
1001
|
+
export function mount({ element, host, signal }) {
|
|
1002
|
+
const configScript = element.querySelector('script[type="application/json"]');
|
|
1003
|
+
const config = configScript ? JSON.parse(configScript.textContent) : {};
|
|
1004
|
+
|
|
1005
|
+
const template = element.querySelector('template');
|
|
1006
|
+
if (template) {
|
|
1007
|
+
const content = template.content.cloneNode(true);
|
|
1008
|
+
// Use content...
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
console.log('Config:', config);
|
|
1012
|
+
}
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
### Using Default Tag Names
|
|
1016
|
+
|
|
1017
|
+
```javascript
|
|
1018
|
+
// For users who want vellum-app / vellum-mod
|
|
1019
|
+
import '@byre/vellum/register';
|
|
1020
|
+
```
|
|
1021
|
+
|
|
1022
|
+
```html
|
|
1023
|
+
<vellum-app>
|
|
1024
|
+
<vellum-mod src="./my-mod.mjs"></vellum-mod>
|
|
1025
|
+
</vellum-app>
|
|
1026
|
+
```
|
|
1027
|
+
|
|
1028
|
+
### DOM-Driven Lifecycle
|
|
1029
|
+
|
|
1030
|
+
```javascript
|
|
1031
|
+
const host = document.querySelector('my-app');
|
|
1032
|
+
|
|
1033
|
+
// Load a mod: add to DOM
|
|
1034
|
+
const mod = document.createElement('my-mod');
|
|
1035
|
+
mod.setAttribute('src', './new-feature.mjs');
|
|
1036
|
+
host.appendChild(mod);
|
|
1037
|
+
|
|
1038
|
+
// Unload a mod: remove from DOM
|
|
1039
|
+
mod.remove();
|
|
1040
|
+
|
|
1041
|
+
// Reload a mod: remove and re-add
|
|
1042
|
+
const parent = mod.parentElement;
|
|
1043
|
+
const next = mod.nextSibling;
|
|
1044
|
+
mod.remove();
|
|
1045
|
+
parent.insertBefore(mod, next);
|
|
1046
|
+
|
|
1047
|
+
// Disable a mod (unloads if loaded)
|
|
1048
|
+
mod.setAttribute('disabled', '');
|
|
1049
|
+
|
|
1050
|
+
// Enable a mod (loads if dependencies satisfied)
|
|
1051
|
+
mod.removeAttribute('disabled');
|
|
1052
|
+
|
|
1053
|
+
// Change source (unloads old, loads new)
|
|
1054
|
+
mod.setAttribute('src', './new-feature-v2.mjs');
|
|
1055
|
+
```
|
|
1056
|
+
|
|
1057
|
+
### CSS Targeting Mod State
|
|
1058
|
+
|
|
1059
|
+
```css
|
|
1060
|
+
/* Style based on load state */
|
|
1061
|
+
my-mod[loading] {
|
|
1062
|
+
opacity: 0.5;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
my-mod[loaded] {
|
|
1066
|
+
/* loaded indicator */
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1069
|
+
my-mod[failed] {
|
|
1070
|
+
outline: 2px solid red;
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
my-mod[failed]::after {
|
|
1074
|
+
content: ' (failed)';
|
|
1075
|
+
color: red;
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
my-mod[disabled] {
|
|
1079
|
+
display: none;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
my-mod[unloading] {
|
|
1083
|
+
opacity: 0.3;
|
|
1084
|
+
pointer-events: none;
|
|
1085
|
+
}
|
|
1086
|
+
```
|
|
1087
|
+
|
|
1088
|
+
### Nested Hosts
|
|
1089
|
+
|
|
1090
|
+
```html
|
|
1091
|
+
<my-app mod-tag="my-mod">
|
|
1092
|
+
<my-mod src="./shell.mjs" name="shell"></my-mod>
|
|
1093
|
+
|
|
1094
|
+
<!-- Inner application with its own mods -->
|
|
1095
|
+
<my-app mod-tag="my-mod" id="inner-app">
|
|
1096
|
+
<my-mod src="./inner-feature.mjs" name="inner-feature"></my-mod>
|
|
1097
|
+
</my-app>
|
|
1098
|
+
|
|
1099
|
+
<my-mod src="./outer-feature.mjs" name="outer-feature"></my-mod>
|
|
1100
|
+
</my-app>
|
|
1101
|
+
```
|
|
1102
|
+
|
|
1103
|
+
```javascript
|
|
1104
|
+
// Outer host can listen to inner host events
|
|
1105
|
+
document.querySelector('my-app').addEventListener('vellum:mod-loaded', (e) => {
|
|
1106
|
+
console.log('A mod loaded somewhere:', e.detail.element.getAttribute('name'));
|
|
1107
|
+
});
|
|
1108
|
+
```
|
|
1109
|
+
|
|
1110
|
+
## Appendix D: Migration from v1.x
|
|
1111
|
+
|
|
1112
|
+
### Removed Features
|
|
1113
|
+
|
|
1114
|
+
| v1.x Feature | v2.0 Equivalent |
|
|
1115
|
+
|--------------------------|----------------------------------|
|
|
1116
|
+
| Built-in action registry | Use `@byre/vellum-actions` mod |
|
|
1117
|
+
| Toolkit object | Use native DOM APIs |
|
|
1118
|
+
| `dispatchAction()` | Use `element.dispatchEvent()` |
|
|
1119
|
+
| Built-in rendering | Use rendering mod |
|
|
1120
|
+
| Logging utilities | Use debug mod or browser console |
|
|
1121
|
+
| `mod.load()` | (removed - host loads mods) |
|
|
1122
|
+
| `mod.reload()` | Remove and re-add element |
|
|
1123
|
+
| `mod.unload()` | Remove element from DOM |
|
|
1124
|
+
|
|
1125
|
+
### Changed Behaviors
|
|
1126
|
+
|
|
1127
|
+
| Behavior | v1.x | v2.0 |
|
|
1128
|
+
|-------------------|--------------------------|-----------------------------------------------------|
|
|
1129
|
+
| Mod element | Inert DOM element | Inert custom element (still inert, but typed) |
|
|
1130
|
+
| Mod state | Internal | Visible via `loading`/`loaded`/`failed`/`unloading` |
|
|
1131
|
+
| Event prefix | `harness:` / none | `vellum:` |
|
|
1132
|
+
| Setup function | `init(toolkit)` | `mount({ element, host, signal })` |
|
|
1133
|
+
| Cleanup function | Not supported | `unmount({ element, host })` |
|
|
1134
|
+
| Load trigger | Public `load()` on mod | Host-only (no public method) |
|
|
1135
|
+
| Unload trigger | Not supported | DOM removal or `disabled` attribute |
|
|
1136
|
+
| Soft dependencies | Not supported | Use `after` attribute |
|
|
1137
|
+
| Disable mod | Not supported | Use `disabled` attribute |
|
|
1138
|
+
| Circular deps | Resolved via priority | Fatal error (`VellumCircularDependencyError`) |
|
|
1139
|
+
|
|
1140
|
+
### Migration Steps
|
|
1141
|
+
|
|
1142
|
+
1. Register both `VellumHost` and `VellumMod` as custom elements
|
|
1143
|
+
2. Add `@byre/vellum-actions` mod if using action system
|
|
1144
|
+
3. Rename `init(toolkit)` to `mount({ element, host, signal })`
|
|
1145
|
+
4. Add `unmount({ element, host })` for any cleanup logic
|
|
1146
|
+
5. Replace `toolkit.dispatchAction()` with native `dispatchEvent()`
|
|
1147
|
+
6. Update event listeners from `harness:action` to mod-specific events
|
|
1148
|
+
7. Change optional dependencies from `require` to `after`
|
|
1149
|
+
8. Use `disabled` attribute instead of conditional loading logic
|
|
1150
|
+
9. Remove any direct calls to `mod.load()` (loading is automatic)
|
|
1151
|
+
10. Replace `mod.reload()` with remove-and-re-add pattern
|
|
1152
|
+
11. Replace `mod.unload()` with `mod.remove()`
|
|
1153
|
+
12. Handle `VellumCircularDependencyError` if you relied on cycle resolution
|
|
1154
|
+
|
|
1155
|
+
## Appendix E: Implementation Notes
|
|
1156
|
+
|
|
1157
|
+
This appendix provides non-normative guidance for implementers.
|
|
1158
|
+
|
|
1159
|
+
### E.1 MutationObserver Configuration
|
|
1160
|
+
|
|
1161
|
+
The host element should observe its subtree for:
|
|
1162
|
+
|
|
1163
|
+
- Child list changes (mod additions and removals)
|
|
1164
|
+
- Attribute changes on mod elements (`src`, `disabled` at minimum)
|
|
1165
|
+
|
|
1166
|
+
Recommended configuration:
|
|
1167
|
+
|
|
1168
|
+
```javascript
|
|
1169
|
+
const observer = new MutationObserver(callback);
|
|
1170
|
+
observer.observe(hostElement, {
|
|
1171
|
+
childList: true,
|
|
1172
|
+
subtree: true,
|
|
1173
|
+
attributes: true,
|
|
1174
|
+
attributeFilter: ['src', 'disabled'],
|
|
1175
|
+
attributeOldValue: true
|
|
1176
|
+
});
|
|
1177
|
+
```
|
|
1178
|
+
|
|
1179
|
+
Note: Observing `subtree: true` with `attributes: true` can be expensive for
|
|
1180
|
+
large DOM trees. Implementations MAY use separate observers for child list
|
|
1181
|
+
and attributes, or MAY track mod elements and observe them individually.
|
|
1182
|
+
|
|
1183
|
+
### E.2 Handling Shadow DOM Boundaries
|
|
1184
|
+
|
|
1185
|
+
If mod elements are placed inside shadow roots, the host's `querySelectorAll`
|
|
1186
|
+
will not find them by default.
|
|
1187
|
+
|
|
1188
|
+
Implementations MAY:
|
|
1189
|
+
|
|
1190
|
+
- Document that mod elements must be in the light DOM
|
|
1191
|
+
- Use `querySelectorAll` with `{ root: document }` (where supported)
|
|
1192
|
+
- Provide a mechanism for shadow roots to register with the host
|
|
1193
|
+
|
|
1194
|
+
The simplest approach is to require mod elements in light DOM.
|
|
1195
|
+
|
|
1196
|
+
### E.3 Performance Considerations for Large Mod Counts
|
|
1197
|
+
|
|
1198
|
+
For applications with many mod elements (50+):
|
|
1199
|
+
|
|
1200
|
+
- **Batch processing**: Process mod additions/removals in batches using
|
|
1201
|
+
`requestAnimationFrame` or `queueMicrotask`
|
|
1202
|
+
- **Parallel loading**: Load mod elements with satisfied dependencies in
|
|
1203
|
+
parallel, respecting any implementation-defined concurrency limit
|
|
1204
|
+
- **Lazy attribute observation**: Only observe attributes on mod elements
|
|
1205
|
+
that have been loaded (not disabled/failed)
|
|
1206
|
+
|
|
1207
|
+
### E.4 AbortSignal Usage
|
|
1208
|
+
|
|
1209
|
+
The `signal` passed to `mount()` enables cancellation. Common patterns:
|
|
1210
|
+
|
|
1211
|
+
```javascript
|
|
1212
|
+
export async function mount({ element, host, signal }) {
|
|
1213
|
+
// Check if already aborted
|
|
1214
|
+
if (signal.aborted) return;
|
|
1215
|
+
|
|
1216
|
+
// Cancel fetch requests
|
|
1217
|
+
const response = await fetch(url, { signal });
|
|
1218
|
+
|
|
1219
|
+
// Cancel timers
|
|
1220
|
+
const timeoutId = setTimeout(doSomething, 1000);
|
|
1221
|
+
signal.addEventListener('abort', () => clearTimeout(timeoutId));
|
|
1222
|
+
|
|
1223
|
+
// Cancel event listeners
|
|
1224
|
+
const handler = (e) => { /* ... */ };
|
|
1225
|
+
document.addEventListener('click', handler);
|
|
1226
|
+
signal.addEventListener('abort', () => {
|
|
1227
|
+
document.removeEventListener('click', handler);
|
|
1228
|
+
});
|
|
1229
|
+
}
|
|
1230
|
+
```
|
|
1231
|
+
|
|
1232
|
+
### E.5 Testing Recommendations
|
|
1233
|
+
|
|
1234
|
+
Implementations should test:
|
|
1235
|
+
|
|
1236
|
+
- All state transitions in the state machine
|
|
1237
|
+
- Dependency resolution with complex graphs
|
|
1238
|
+
- Circular dependency detection
|
|
1239
|
+
- Dynamic mod addition/removal
|
|
1240
|
+
- Attribute change handling during various states
|
|
1241
|
+
- Nested host behavior
|
|
1242
|
+
- Error propagation and error types
|
|
1243
|
+
- AbortSignal cancellation during load
|
|
1244
|
+
|
|
1245
|
+
## Appendix F: Related Work
|
|
1246
|
+
|
|
1247
|
+
Vellum draws inspiration from and differs from several existing systems:
|
|
1248
|
+
|
|
1249
|
+
### Module Loaders
|
|
1250
|
+
|
|
1251
|
+
**SystemJS / RequireJS**: Full module loaders with resolution, bundling
|
|
1252
|
+
concerns, and format transformation. Vellum delegates to native ES modules
|
|
1253
|
+
and focuses only on lifecycle coordination.
|
|
1254
|
+
|
|
1255
|
+
**ES Module Shims**: Polyfills for import maps and other module features.
|
|
1256
|
+
Vellum works alongside such tools but doesn't replace them.
|
|
1257
|
+
|
|
1258
|
+
### Web Component Libraries
|
|
1259
|
+
|
|
1260
|
+
**Lit / Stencil / etc.**: Component authoring libraries. Vellum doesn't
|
|
1261
|
+
provide component authoring—it loads arbitrary modules that MAY use any
|
|
1262
|
+
component library.
|
|
1263
|
+
|
|
1264
|
+
### Micro-Frontend Frameworks
|
|
1265
|
+
|
|
1266
|
+
**single-spa / Module Federation**: Orchestrate multiple applications.
|
|
1267
|
+
Vellum is lower-level—it loads modules, not applications. A micro-frontend
|
|
1268
|
+
framework could be built as a Vellum mod.
|
|
1269
|
+
|
|
1270
|
+
### Key Differentiators
|
|
1271
|
+
|
|
1272
|
+
1. **DOM-centric**: State visible in DOM, not JavaScript
|
|
1273
|
+
2. **Kernel philosophy**: Minimal core, everything else is a mod
|
|
1274
|
+
3. **No opinions**: No rendering, routing, state management built-in
|
|
1275
|
+
4. **Native ES modules**: Uses `import()`, no custom loader
|
|
1276
|
+
|
|
1277
|
+
---
|
|
1278
|
+
|
|
1279
|
+
## References
|
|
1280
|
+
|
|
1281
|
+
**Normative:**
|
|
1282
|
+
- [RFC 2119](https://www.rfc-editor.org/rfc/rfc2119): Key words for requirements
|
|
1283
|
+
- [Custom Elements](https://html.spec.whatwg.org/multipage/custom-elements.html): WHATWG specification
|
|
1284
|
+
- [ES Modules](https://tc39.es/ecma262/#sec-modules): ECMAScript specification
|
|
1285
|
+
- [Semantic Versioning 2.0.0](https://semver.org/)
|
|
1286
|
+
|
|
1287
|
+
**Informative:**
|
|
1288
|
+
- [MutationObserver](https://dom.spec.whatwg.org/#mutationobserver): DOM observation
|
|
1289
|
+
- [CustomEvent](https://dom.spec.whatwg.org/#customevent): Event interface
|
|
1290
|
+
- [AbortController](https://dom.spec.whatwg.org/#abortcontroller): Cancellation API
|
|
1291
|
+
- [Content Security Policy](https://www.w3.org/TR/CSP3/): CSP Level 3
|
|
1292
|
+
- [Subresource Integrity](https://www.w3.org/TR/SRI/): SRI specification
|