solid-tag-runtime 0.0.2 → 0.0.3
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/ARCHITECTURE.md +199 -24
- package/README.md +112 -75
- package/html.d.ts +92 -5
- package/package.json +1 -1
- package/src/html.js +619 -149
package/src/html.js
CHANGED
|
@@ -9,7 +9,11 @@ const DEFAULT_SELECTOR = [
|
|
|
9
9
|
'script[type="text/solid-module"]',
|
|
10
10
|
].join(",");
|
|
11
11
|
|
|
12
|
+
const RUNTIME_SCOPE_ATTRIBUTE = "data-solid-runtime";
|
|
13
|
+
const elementOwners = new WeakMap();
|
|
14
|
+
const defaultControllers = new WeakMap();
|
|
12
15
|
let anonymousSequence = 0;
|
|
16
|
+
let controllerSequence = 0;
|
|
13
17
|
|
|
14
18
|
export class HTMLModuleError extends SolidTagRuntimeError {
|
|
15
19
|
constructor(message, options = {}) {
|
|
@@ -21,165 +25,522 @@ export class HTMLModuleError extends SolidTagRuntimeError {
|
|
|
21
25
|
}
|
|
22
26
|
}
|
|
23
27
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const root = getRoot(options, "registerHTML");
|
|
32
|
-
const elements = Array.from(root.querySelectorAll(options.selector ?? DEFAULT_SELECTOR));
|
|
33
|
-
|
|
34
|
-
return registerElements(runtime, elements, {
|
|
35
|
-
...options,
|
|
36
|
-
root,
|
|
37
|
-
});
|
|
28
|
+
export class HTMLModuleOwnershipError extends HTMLModuleError {
|
|
29
|
+
constructor(message, options = {}) {
|
|
30
|
+
super(message, options);
|
|
31
|
+
this.name = "HTMLModuleOwnershipError";
|
|
32
|
+
this.ownerScope = options.ownerScope;
|
|
33
|
+
this.requestedScope = options.requestedScope;
|
|
34
|
+
}
|
|
38
35
|
}
|
|
39
36
|
|
|
40
37
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* Existing scripts are registered by default. Every discovered mutation batch
|
|
44
|
-
* preserves the same invariant as registerHTML(): all modules in the batch are
|
|
45
|
-
* defined before any entry module from that batch executes.
|
|
38
|
+
* Create a persistent DOM/HTML adapter for one solid-tag-runtime instance.
|
|
46
39
|
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
40
|
+
* The controller owns DOM-to-module relationships. All ingestion paths
|
|
41
|
+
* (scan, observer, registerElement, append, addModule) converge on the same
|
|
42
|
+
* module-level WeakMap so an element can only be claimed once.
|
|
49
43
|
*/
|
|
50
|
-
export
|
|
44
|
+
export function createHTMLRuntime(runtime, options = {}) {
|
|
51
45
|
assertRuntime(runtime);
|
|
52
46
|
|
|
53
|
-
const
|
|
54
|
-
|
|
47
|
+
const controller = {
|
|
48
|
+
id: `solid-tag-html-${++controllerSequence}`,
|
|
49
|
+
runtime,
|
|
50
|
+
scope: normalizeScope(options.scope),
|
|
51
|
+
root: options.root,
|
|
52
|
+
selector: options.selector ?? DEFAULT_SELECTOR,
|
|
53
|
+
acceptUnscoped: options.acceptUnscoped !== false,
|
|
54
|
+
executeEntries: options.executeEntries !== false,
|
|
55
|
+
fetch: options.fetch,
|
|
56
|
+
baseUrl: options.baseUrl,
|
|
57
|
+
MutationObserver: options.MutationObserver,
|
|
58
|
+
subtree: options.subtree !== false,
|
|
59
|
+
createElement: options.createElement,
|
|
60
|
+
appendTo: options.appendTo,
|
|
61
|
+
bindings: new Map(),
|
|
62
|
+
observer: undefined,
|
|
63
|
+
observerRoot: undefined,
|
|
64
|
+
ignoredExisting: new WeakSet(),
|
|
65
|
+
queue: Promise.resolve(),
|
|
66
|
+
lastError: undefined,
|
|
67
|
+
connected: false,
|
|
68
|
+
initial: emptyRegistrationResult(),
|
|
69
|
+
};
|
|
55
70
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
71
|
+
const api = {
|
|
72
|
+
get runtime() {
|
|
73
|
+
return runtime;
|
|
74
|
+
},
|
|
75
|
+
get scope() {
|
|
76
|
+
return controller.scope;
|
|
77
|
+
},
|
|
78
|
+
get connected() {
|
|
79
|
+
return controller.connected;
|
|
80
|
+
},
|
|
81
|
+
get initial() {
|
|
82
|
+
return controller.initial;
|
|
83
|
+
},
|
|
84
|
+
register,
|
|
85
|
+
observe,
|
|
86
|
+
flush,
|
|
87
|
+
disconnect,
|
|
88
|
+
defineElement,
|
|
89
|
+
registerElement,
|
|
90
|
+
append,
|
|
91
|
+
addModule,
|
|
92
|
+
owns,
|
|
93
|
+
getModuleId,
|
|
94
|
+
getElement,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
controller.api = api;
|
|
98
|
+
return api;
|
|
99
|
+
|
|
100
|
+
/** One-shot scan of matching scripts. */
|
|
101
|
+
async function register(callOptions = {}) {
|
|
102
|
+
const root = getRoot(mergeOptions(controller, callOptions), "HTML runtime register");
|
|
103
|
+
const selector = callOptions.selector ?? controller.selector;
|
|
104
|
+
const elements = Array.from(root.querySelectorAll(selector)).filter(element =>
|
|
105
|
+
acceptsDiscoveredElement(controller, element, callOptions),
|
|
59
106
|
);
|
|
107
|
+
|
|
108
|
+
return processElements(controller, elements, {
|
|
109
|
+
...callOptions,
|
|
110
|
+
root,
|
|
111
|
+
origin: callOptions.origin ?? "initial-scan",
|
|
112
|
+
executeEntries: callOptions.executeEntries ?? controller.executeEntries,
|
|
113
|
+
});
|
|
60
114
|
}
|
|
61
115
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
116
|
+
/**
|
|
117
|
+
* Start MutationObserver-backed discovery. Existing matching scripts are
|
|
118
|
+
* registered by default. The controller itself is returned for chaining.
|
|
119
|
+
*/
|
|
120
|
+
async function observe(callOptions = {}) {
|
|
121
|
+
if (controller.connected) return api;
|
|
122
|
+
|
|
123
|
+
const merged = mergeOptions(controller, callOptions);
|
|
124
|
+
const root = getRoot(merged, "HTML runtime observe");
|
|
125
|
+
const MutationObserverImpl = callOptions.MutationObserver ?? controller.MutationObserver ?? globalThis.MutationObserver;
|
|
126
|
+
|
|
127
|
+
if (typeof MutationObserverImpl !== "function") {
|
|
128
|
+
throw new TypeError(
|
|
129
|
+
"observe() requires MutationObserver support or a MutationObserver option.",
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
controller.observerRoot = root;
|
|
134
|
+
controller.connected = true;
|
|
135
|
+
controller.lastError = undefined;
|
|
136
|
+
controller.ignoredExisting = new WeakSet();
|
|
67
137
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
138
|
+
if (callOptions.registerExisting === false) {
|
|
139
|
+
for (const element of Array.from(root.querySelectorAll(callOptions.selector ?? controller.selector))) {
|
|
140
|
+
if (acceptsDiscoveredElement(controller, element, callOptions)) {
|
|
141
|
+
controller.ignoredExisting.add(element);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
controller.initial = emptyRegistrationResult();
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const observer = new MutationObserverImpl(() => {
|
|
148
|
+
void enqueueObserverScan(controller, callOptions).catch(() => {});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
controller.observer = observer;
|
|
152
|
+
observer.observe(root, {
|
|
153
|
+
childList: true,
|
|
154
|
+
subtree: callOptions.subtree ?? controller.subtree,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
try {
|
|
158
|
+
if (callOptions.registerExisting !== false) {
|
|
159
|
+
controller.initial = await enqueueObserverScan(controller, {
|
|
160
|
+
...callOptions,
|
|
161
|
+
origin: "observer-initial",
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
} catch (error) {
|
|
165
|
+
disconnect();
|
|
166
|
+
throw error;
|
|
71
167
|
}
|
|
168
|
+
|
|
169
|
+
return api;
|
|
72
170
|
}
|
|
73
171
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
);
|
|
172
|
+
/** Wait for observer work, then synchronously scan for newly discoverable scripts. */
|
|
173
|
+
async function flush(callOptions = {}) {
|
|
174
|
+
await controller.queue;
|
|
78
175
|
|
|
79
|
-
if (
|
|
176
|
+
if (controller.lastError) {
|
|
177
|
+
const error = controller.lastError;
|
|
178
|
+
controller.lastError = undefined;
|
|
179
|
+
throw error;
|
|
180
|
+
}
|
|
80
181
|
|
|
81
|
-
return
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
182
|
+
if (!controller.connected) return emptyRegistrationResult();
|
|
183
|
+
return enqueueObserverScan(controller, callOptions);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function disconnect() {
|
|
187
|
+
if (!controller.connected) return;
|
|
188
|
+
controller.connected = false;
|
|
189
|
+
controller.observer?.disconnect();
|
|
190
|
+
controller.observer = undefined;
|
|
191
|
+
controller.observerRoot = undefined;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Define one element without automatically executing an entry module. */
|
|
195
|
+
async function defineElement(element, callOptions = {}) {
|
|
196
|
+
const result = await processElements(controller, [element], {
|
|
197
|
+
...callOptions,
|
|
198
|
+
origin: callOptions.origin ?? "define-element",
|
|
199
|
+
explicit: true,
|
|
200
|
+
executeEntries: false,
|
|
201
|
+
});
|
|
202
|
+
return result.modules[0] ?? definitionFromOwnedElement(controller, element);
|
|
203
|
+
}
|
|
91
204
|
|
|
92
|
-
|
|
93
|
-
|
|
205
|
+
/** Explicitly claim/register an element for this controller. */
|
|
206
|
+
async function registerElement(element, callOptions = {}) {
|
|
207
|
+
const result = await processElements(controller, [element], {
|
|
208
|
+
...callOptions,
|
|
209
|
+
origin: callOptions.origin ?? "register-element",
|
|
210
|
+
explicit: true,
|
|
211
|
+
executeEntries: callOptions.executeEntries ?? controller.executeEntries,
|
|
212
|
+
});
|
|
213
|
+
return result.modules[0] ?? definitionFromOwnedElement(controller, element);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Claim a user-created script before DOM insertion, append it to this
|
|
218
|
+
* controller's target, then register it directly. The observer will see the
|
|
219
|
+
* mutation later but skip it because ownership was already established.
|
|
220
|
+
*/
|
|
221
|
+
async function append(element, callOptions = {}) {
|
|
222
|
+
const claim = claimElement(controller, element, {
|
|
223
|
+
...callOptions,
|
|
224
|
+
origin: callOptions.origin ?? "append",
|
|
225
|
+
explicit: true,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const target = getAppendTarget(controller, callOptions);
|
|
229
|
+
appendElement(target, element);
|
|
230
|
+
|
|
231
|
+
const result = await processClaims(controller, claim.fresh ? [claim.record] : [], {
|
|
232
|
+
...callOptions,
|
|
233
|
+
origin: callOptions.origin ?? "append",
|
|
234
|
+
executeEntries: callOptions.executeEntries ?? controller.executeEntries,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
if (!claim.fresh) await claim.record.promise;
|
|
238
|
+
return result.modules[0] ?? claim.record.definition;
|
|
239
|
+
}
|
|
94
240
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
241
|
+
/** Create a runtime script element and add it through append(). */
|
|
242
|
+
async function addModule(moduleOptions = {}) {
|
|
243
|
+
const format = normalizeFormat(moduleOptions.format ?? inferFormat({
|
|
244
|
+
explicitFormat: undefined,
|
|
245
|
+
language: moduleOptions.language,
|
|
246
|
+
type: moduleOptions.type,
|
|
247
|
+
moduleId: moduleOptions.id ?? moduleOptions.module,
|
|
248
|
+
sourceUrl: moduleOptions.src,
|
|
249
|
+
}));
|
|
250
|
+
|
|
251
|
+
const createElement = moduleOptions.createElement
|
|
252
|
+
?? controller.createElement
|
|
253
|
+
?? getDocumentForCreation(controller, moduleOptions)?.createElement?.bind(getDocumentForCreation(controller, moduleOptions));
|
|
254
|
+
|
|
255
|
+
if (typeof createElement !== "function") {
|
|
256
|
+
throw new TypeError(
|
|
257
|
+
"addModule() requires document.createElement(), a root ownerDocument, or a createElement option.",
|
|
258
|
+
);
|
|
98
259
|
}
|
|
99
260
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
261
|
+
const element = createElement("script");
|
|
262
|
+
if (!element || typeof element.setAttribute !== "function") {
|
|
263
|
+
throw new TypeError("addModule() createElement must return a script element-like object with setAttribute().");
|
|
264
|
+
}
|
|
104
265
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
);
|
|
113
|
-
|
|
114
|
-
|
|
266
|
+
const type = moduleOptions.type ?? (format === "js" ? "solid-js" : "solid-jsx");
|
|
267
|
+
element.setAttribute("type", type);
|
|
268
|
+
|
|
269
|
+
const id = nonEmpty(moduleOptions.id ?? moduleOptions.module);
|
|
270
|
+
if (id) element.setAttribute("module", id);
|
|
271
|
+
if (moduleOptions.src) element.setAttribute("src", String(moduleOptions.src));
|
|
272
|
+
if (moduleOptions.entry) element.setAttribute("entry", "");
|
|
273
|
+
if (moduleOptions.language) element.setAttribute("language", String(moduleOptions.language));
|
|
274
|
+
if (moduleOptions.source != null) element.textContent = String(moduleOptions.source);
|
|
275
|
+
|
|
276
|
+
const definition = await append(element, {
|
|
277
|
+
...moduleOptions,
|
|
278
|
+
format,
|
|
279
|
+
origin: "add-module",
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
...definition,
|
|
284
|
+
element,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
115
287
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
void enqueue().catch(() => {});
|
|
120
|
-
});
|
|
288
|
+
function owns(element) {
|
|
289
|
+
return elementOwners.get(element)?.owner === controller;
|
|
290
|
+
}
|
|
121
291
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
292
|
+
function getModuleId(element) {
|
|
293
|
+
const record = elementOwners.get(element);
|
|
294
|
+
return record?.owner === controller ? record.moduleId : undefined;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function getElement(id) {
|
|
298
|
+
const normalized = normalizeModuleId(runtime, id);
|
|
299
|
+
return controller.bindings.get(normalized)?.element;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Discover runtime module scripts under a Document/Element-like root, define
|
|
305
|
+
* all of them first, then optionally execute scripts marked with `entry`.
|
|
306
|
+
*
|
|
307
|
+
* Backwards-compatible convenience wrapper over the runtime's default HTML
|
|
308
|
+
* controller.
|
|
309
|
+
*/
|
|
310
|
+
export async function registerHTML(runtime, options = {}) {
|
|
311
|
+
return getDefaultController(runtime, options).register(options);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** Backwards-compatible observer convenience wrapper. */
|
|
315
|
+
export async function observeHTML(runtime, options = {}) {
|
|
316
|
+
const controller = getDefaultController(runtime, options);
|
|
317
|
+
await controller.observe(options);
|
|
318
|
+
return controller;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/** Define one <script> element as a runtime module without executing it. */
|
|
322
|
+
export async function defineScript(runtime, element, options = {}) {
|
|
323
|
+
return getDefaultController(runtime, options).defineElement(element, options);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export const htmlModuleSelector = DEFAULT_SELECTOR;
|
|
327
|
+
export const htmlRuntimeScopeAttribute = RUNTIME_SCOPE_ATTRIBUTE;
|
|
328
|
+
|
|
329
|
+
async function enqueueObserverScan(controller, callOptions = {}) {
|
|
330
|
+
const task = controller.queue.then(async () => {
|
|
331
|
+
if (!controller.connected) return emptyRegistrationResult();
|
|
332
|
+
|
|
333
|
+
const root = controller.observerRoot ?? getRoot(mergeOptions(controller, callOptions), "HTML runtime observer");
|
|
334
|
+
const selector = callOptions.selector ?? controller.selector;
|
|
335
|
+
const elements = Array.from(root.querySelectorAll(selector)).filter(element => {
|
|
336
|
+
if (controller.ignoredExisting.has(element)) return false;
|
|
337
|
+
if (!acceptsDiscoveredElement(controller, element, callOptions)) return false;
|
|
338
|
+
|
|
339
|
+
const ownership = elementOwners.get(element);
|
|
340
|
+
if (!ownership) return true;
|
|
341
|
+
if (ownership.owner === controller) return false;
|
|
342
|
+
|
|
343
|
+
throw ownershipError(controller, element, ownership);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
return processElements(controller, elements, {
|
|
347
|
+
...callOptions,
|
|
348
|
+
root,
|
|
349
|
+
origin: callOptions.origin ?? "observer",
|
|
350
|
+
executeEntries: callOptions.executeEntries ?? controller.executeEntries,
|
|
351
|
+
});
|
|
125
352
|
});
|
|
126
353
|
|
|
127
|
-
|
|
354
|
+
controller.queue = task.then(
|
|
355
|
+
() => undefined,
|
|
356
|
+
error => {
|
|
357
|
+
controller.lastError = error;
|
|
358
|
+
if (typeof callOptions.onError === "function") {
|
|
359
|
+
callOptions.onError(error);
|
|
360
|
+
} else {
|
|
361
|
+
globalThis.console?.error?.("solid-tag-runtime/html observer error", error);
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
return task;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
async function processElements(controller, elements, options = {}) {
|
|
370
|
+
const freshRecords = [];
|
|
371
|
+
const existingRecords = [];
|
|
372
|
+
|
|
373
|
+
// Claim the entire batch synchronously before any asynchronous source loading.
|
|
374
|
+
// This prevents observer/manual registration races and reserves module IDs.
|
|
375
|
+
for (const element of elements) {
|
|
376
|
+
if (!options.explicit && !acceptsDiscoveredElement(controller, element, options)) continue;
|
|
377
|
+
|
|
378
|
+
const claim = claimElement(controller, element, options);
|
|
379
|
+
if (claim.fresh) freshRecords.push(claim.record);
|
|
380
|
+
else existingRecords.push(claim.record);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const result = await processClaims(controller, freshRecords, options);
|
|
384
|
+
|
|
385
|
+
// Same-controller repeated registration is idempotent. Await currently pending
|
|
386
|
+
// registrations, but do not report/re-execute them as newly registered modules.
|
|
387
|
+
await Promise.all(existingRecords.map(record => record.promise));
|
|
388
|
+
return result;
|
|
389
|
+
}
|
|
128
390
|
|
|
391
|
+
async function processClaims(controller, records, options = {}) {
|
|
392
|
+
if (records.length === 0) return emptyRegistrationResult();
|
|
393
|
+
|
|
394
|
+
let prepared;
|
|
395
|
+
try {
|
|
396
|
+
prepared = await Promise.all(records.map(record => prepareRecordSource(controller, record, options)));
|
|
397
|
+
} catch (error) {
|
|
398
|
+
for (const record of records) rejectRecord(record, error);
|
|
399
|
+
throw error;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const modules = [];
|
|
129
403
|
try {
|
|
130
|
-
|
|
131
|
-
|
|
404
|
+
// Preserve the graph invariant: every module in this discovered batch is
|
|
405
|
+
// defined before any entry in the batch executes.
|
|
406
|
+
for (let index = 0; index < records.length; index++) {
|
|
407
|
+
const record = records[index];
|
|
408
|
+
const item = prepared[index];
|
|
409
|
+
controller.runtime.define(record.moduleId, item.source, { format: record.format });
|
|
410
|
+
|
|
411
|
+
record.state = "defined";
|
|
412
|
+
record.definition = definitionFromRecord(record);
|
|
413
|
+
record.resolve(record.definition);
|
|
414
|
+
modules.push(record.definition);
|
|
132
415
|
}
|
|
133
416
|
} catch (error) {
|
|
134
|
-
|
|
135
|
-
|
|
417
|
+
for (const record of records) {
|
|
418
|
+
if (record.state === "claimed" || record.state === "loading") rejectRecord(record, error);
|
|
419
|
+
}
|
|
136
420
|
throw error;
|
|
137
421
|
}
|
|
138
422
|
|
|
423
|
+
const entries = modules.filter(module => module.entry);
|
|
424
|
+
const executed = [];
|
|
425
|
+
|
|
426
|
+
if (options.executeEntries !== false) {
|
|
427
|
+
for (const entry of entries) {
|
|
428
|
+
const record = elementOwners.get(entry.element);
|
|
429
|
+
if (record) record.state = "executing";
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
const namespace = await controller.runtime.import(entry.id);
|
|
433
|
+
if (record) record.state = "ready";
|
|
434
|
+
executed.push({ id: entry.id, namespace });
|
|
435
|
+
} catch (error) {
|
|
436
|
+
if (record) {
|
|
437
|
+
record.state = "error";
|
|
438
|
+
record.error = error;
|
|
439
|
+
}
|
|
440
|
+
throw error;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
139
445
|
return {
|
|
140
|
-
|
|
446
|
+
modules,
|
|
447
|
+
entries: entries.map(entry => entry.id),
|
|
448
|
+
executed,
|
|
449
|
+
};
|
|
450
|
+
}
|
|
141
451
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
},
|
|
452
|
+
function claimElement(controller, element, options = {}) {
|
|
453
|
+
assertElement(element, options.origin ?? "HTML runtime");
|
|
145
454
|
|
|
146
|
-
|
|
147
|
-
|
|
455
|
+
const existing = elementOwners.get(element);
|
|
456
|
+
if (existing) {
|
|
457
|
+
if (existing.owner === controller) return { fresh: false, record: existing };
|
|
458
|
+
throw ownershipError(controller, element, existing);
|
|
459
|
+
}
|
|
148
460
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
lastError = undefined;
|
|
152
|
-
throw error;
|
|
153
|
-
}
|
|
461
|
+
assertExplicitScopeCompatibility(controller, element, options);
|
|
462
|
+
stampScope(controller, element, options);
|
|
154
463
|
|
|
155
|
-
|
|
156
|
-
|
|
464
|
+
const metadata = inspectElement(controller, element, options);
|
|
465
|
+
const moduleId = normalizeModuleId(controller.runtime, metadata.id);
|
|
466
|
+
const binding = controller.bindings.get(moduleId);
|
|
157
467
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
468
|
+
if (binding && binding.element !== element) {
|
|
469
|
+
throw new HTMLModuleError(
|
|
470
|
+
`HTML module ${JSON.stringify(moduleId)} is already bound to another script element in this HTML runtime.`,
|
|
471
|
+
{ element, moduleId, sourceUrl: metadata.sourceUrl },
|
|
472
|
+
);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
if (!binding && controller.runtime.has(moduleId)) {
|
|
476
|
+
throw new HTMLModuleError(
|
|
477
|
+
`Runtime module ${JSON.stringify(moduleId)} already exists and is not owned by this HTML runtime. Use runtime.update() for explicit replacement.`,
|
|
478
|
+
{ element, moduleId, sourceUrl: metadata.sourceUrl },
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
let resolve;
|
|
483
|
+
let reject;
|
|
484
|
+
const promise = new Promise((resolvePromise, rejectPromise) => {
|
|
485
|
+
resolve = resolvePromise;
|
|
486
|
+
reject = rejectPromise;
|
|
487
|
+
});
|
|
488
|
+
// A failed background observer registration may not have a caller awaiting
|
|
489
|
+
// this per-element promise. Mark the rejection as observed while preserving
|
|
490
|
+
// the original promise for explicit callers.
|
|
491
|
+
void promise.catch(() => {});
|
|
492
|
+
|
|
493
|
+
const record = {
|
|
494
|
+
owner: controller,
|
|
495
|
+
element,
|
|
496
|
+
moduleId,
|
|
497
|
+
format: metadata.format,
|
|
498
|
+
entry: metadata.entry,
|
|
499
|
+
sourceUrl: metadata.sourceUrl,
|
|
500
|
+
inline: !metadata.sourceUrl,
|
|
501
|
+
state: "claimed",
|
|
502
|
+
origin: options.origin ?? "unknown",
|
|
503
|
+
promise,
|
|
504
|
+
resolve,
|
|
505
|
+
reject,
|
|
506
|
+
definition: undefined,
|
|
507
|
+
error: undefined,
|
|
163
508
|
};
|
|
509
|
+
|
|
510
|
+
elementOwners.set(element, record);
|
|
511
|
+
controller.bindings.set(moduleId, record);
|
|
512
|
+
return { fresh: true, record };
|
|
164
513
|
}
|
|
165
514
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
*/
|
|
169
|
-
export async function defineScript(runtime, element, options = {}) {
|
|
170
|
-
assertRuntime(runtime);
|
|
515
|
+
async function prepareRecordSource(controller, record, options) {
|
|
516
|
+
record.state = "loading";
|
|
171
517
|
|
|
172
|
-
|
|
173
|
-
|
|
518
|
+
let source;
|
|
519
|
+
if (record.sourceUrl) {
|
|
520
|
+
source = await fetchSource(
|
|
521
|
+
record.sourceUrl,
|
|
522
|
+
options.fetch ?? controller.fetch ?? globalThis.fetch,
|
|
523
|
+
record.element,
|
|
524
|
+
record.moduleId,
|
|
525
|
+
);
|
|
526
|
+
} else {
|
|
527
|
+
source = record.element.textContent ?? "";
|
|
174
528
|
}
|
|
175
529
|
|
|
530
|
+
return { source };
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
function inspectElement(controller, element, options = {}) {
|
|
176
534
|
const type = normalizeType(element.getAttribute("type"));
|
|
177
535
|
const entry = hasAttribute(element, "entry");
|
|
178
536
|
const srcAttribute = nonEmpty(element.getAttribute("src"));
|
|
179
537
|
const explicitId = nonEmpty(element.getAttribute("module"));
|
|
180
|
-
const
|
|
181
|
-
|
|
182
|
-
|
|
538
|
+
const baseUrl = options.baseUrl
|
|
539
|
+
?? controller.baseUrl
|
|
540
|
+
?? element.baseURI
|
|
541
|
+
?? options.root?.baseURI
|
|
542
|
+
?? controller.root?.baseURI;
|
|
543
|
+
const sourceUrl = srcAttribute ? resolveSourceUrl(srcAttribute, baseUrl) : undefined;
|
|
183
544
|
|
|
184
545
|
const format = inferFormat({
|
|
185
546
|
explicitFormat: options.format,
|
|
@@ -190,7 +551,6 @@ export async function defineScript(runtime, element, options = {}) {
|
|
|
190
551
|
});
|
|
191
552
|
|
|
192
553
|
const id = explicitId ?? sourceUrl ?? (entry ? createAnonymousId(format) : undefined);
|
|
193
|
-
|
|
194
554
|
if (!id) {
|
|
195
555
|
throw new HTMLModuleError(
|
|
196
556
|
"Inline solid runtime module scripts require a `module` attribute unless they are marked `entry`.",
|
|
@@ -198,54 +558,152 @@ export async function defineScript(runtime, element, options = {}) {
|
|
|
198
558
|
);
|
|
199
559
|
}
|
|
200
560
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
source = await fetchSource(sourceUrl, options.fetch ?? globalThis.fetch, element, id);
|
|
204
|
-
} else {
|
|
205
|
-
source = element.textContent ?? "";
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
runtime.define(id, source, { format });
|
|
561
|
+
return { id, format, entry, sourceUrl };
|
|
562
|
+
}
|
|
209
563
|
|
|
564
|
+
function definitionFromRecord(record) {
|
|
210
565
|
return {
|
|
211
|
-
id:
|
|
212
|
-
format,
|
|
213
|
-
entry,
|
|
214
|
-
sourceUrl,
|
|
215
|
-
inline:
|
|
216
|
-
element,
|
|
566
|
+
id: record.moduleId,
|
|
567
|
+
format: record.format,
|
|
568
|
+
entry: record.entry,
|
|
569
|
+
sourceUrl: record.sourceUrl,
|
|
570
|
+
inline: record.inline,
|
|
571
|
+
element: record.element,
|
|
217
572
|
};
|
|
218
573
|
}
|
|
219
574
|
|
|
220
|
-
|
|
575
|
+
function definitionFromOwnedElement(controller, element) {
|
|
576
|
+
const record = elementOwners.get(element);
|
|
577
|
+
if (!record || record.owner !== controller) return undefined;
|
|
578
|
+
return record.definition ?? definitionFromRecord(record);
|
|
579
|
+
}
|
|
221
580
|
|
|
222
|
-
|
|
223
|
-
|
|
581
|
+
function rejectRecord(record, error) {
|
|
582
|
+
if (record.state === "error") return;
|
|
583
|
+
record.state = "error";
|
|
584
|
+
record.error = error;
|
|
585
|
+
record.reject(error);
|
|
586
|
+
}
|
|
224
587
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
588
|
+
function acceptsDiscoveredElement(controller, element, options = {}) {
|
|
589
|
+
const elementScope = nonEmpty(element?.getAttribute?.(RUNTIME_SCOPE_ATTRIBUTE));
|
|
590
|
+
const scope = normalizeScope(options.scope ?? controller.scope);
|
|
591
|
+
const acceptUnscoped = options.acceptUnscoped ?? controller.acceptUnscoped;
|
|
592
|
+
|
|
593
|
+
if (scope) {
|
|
594
|
+
if (elementScope) return elementScope === scope;
|
|
595
|
+
return acceptUnscoped !== false;
|
|
231
596
|
}
|
|
232
597
|
|
|
233
|
-
|
|
234
|
-
|
|
598
|
+
// An unscoped controller never silently steals a declaratively scoped script.
|
|
599
|
+
return !elementScope;
|
|
600
|
+
}
|
|
235
601
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
602
|
+
function assertExplicitScopeCompatibility(controller, element, options = {}) {
|
|
603
|
+
const requestedScope = normalizeScope(options.scope ?? controller.scope);
|
|
604
|
+
const elementScope = nonEmpty(element.getAttribute(RUNTIME_SCOPE_ATTRIBUTE));
|
|
605
|
+
|
|
606
|
+
if (elementScope && requestedScope !== elementScope) {
|
|
607
|
+
throw new HTMLModuleOwnershipError(
|
|
608
|
+
`Script is scoped to HTML runtime ${JSON.stringify(elementScope)} and cannot be claimed by ${JSON.stringify(requestedScope ?? "(unscoped)")}.`,
|
|
609
|
+
{
|
|
610
|
+
element,
|
|
611
|
+
ownerScope: elementScope,
|
|
612
|
+
requestedScope,
|
|
613
|
+
},
|
|
614
|
+
);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function stampScope(controller, element, options = {}) {
|
|
619
|
+
const scope = normalizeScope(options.scope ?? controller.scope);
|
|
620
|
+
if (!scope) return;
|
|
621
|
+
if (nonEmpty(element.getAttribute(RUNTIME_SCOPE_ATTRIBUTE))) return;
|
|
622
|
+
|
|
623
|
+
if (typeof element.setAttribute !== "function") {
|
|
624
|
+
throw new TypeError(
|
|
625
|
+
`Scoped HTML runtimes require script elements with setAttribute() so ${RUNTIME_SCOPE_ATTRIBUTE} can be recorded.`,
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
element.setAttribute(RUNTIME_SCOPE_ATTRIBUTE, scope);
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function ownershipError(controller, element, ownership) {
|
|
633
|
+
return new HTMLModuleOwnershipError(
|
|
634
|
+
`Runtime script element is already owned by HTML runtime ${JSON.stringify(ownership.owner.scope ?? ownership.owner.id)} and cannot be claimed by ${JSON.stringify(controller.scope ?? controller.id)}.`,
|
|
635
|
+
{
|
|
636
|
+
element,
|
|
637
|
+
moduleId: ownership.moduleId,
|
|
638
|
+
sourceUrl: ownership.sourceUrl,
|
|
639
|
+
ownerScope: ownership.owner.scope,
|
|
640
|
+
requestedScope: controller.scope,
|
|
641
|
+
},
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function getDefaultController(runtime, options = {}) {
|
|
646
|
+
assertRuntime(runtime);
|
|
647
|
+
let controller = defaultControllers.get(runtime);
|
|
648
|
+
if (!controller) {
|
|
649
|
+
controller = createHTMLRuntime(runtime, {
|
|
650
|
+
root: options.root,
|
|
651
|
+
selector: options.selector,
|
|
652
|
+
executeEntries: options.executeEntries,
|
|
653
|
+
fetch: options.fetch,
|
|
654
|
+
baseUrl: options.baseUrl,
|
|
655
|
+
MutationObserver: options.MutationObserver,
|
|
656
|
+
subtree: options.subtree,
|
|
657
|
+
createElement: options.createElement,
|
|
658
|
+
appendTo: options.appendTo,
|
|
659
|
+
scope: options.scope,
|
|
660
|
+
acceptUnscoped: options.acceptUnscoped,
|
|
661
|
+
});
|
|
662
|
+
defaultControllers.set(runtime, controller);
|
|
243
663
|
}
|
|
664
|
+
return controller;
|
|
665
|
+
}
|
|
244
666
|
|
|
667
|
+
function getAppendTarget(controller, options = {}) {
|
|
668
|
+
const explicit = options.appendTo ?? controller.appendTo;
|
|
669
|
+
if (explicit) return explicit;
|
|
670
|
+
|
|
671
|
+
const root = options.root ?? controller.root ?? globalThis.document;
|
|
672
|
+
if (root?.body) return root.body;
|
|
673
|
+
if (root?.documentElement && root.documentElement !== root) return root.documentElement;
|
|
674
|
+
return root;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
function appendElement(target, element) {
|
|
678
|
+
if (target && typeof target.append === "function") {
|
|
679
|
+
target.append(element);
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
if (target && typeof target.appendChild === "function") {
|
|
683
|
+
target.appendChild(element);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
throw new TypeError("append() requires an append target with append() or appendChild().");
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function getDocumentForCreation(controller, options = {}) {
|
|
690
|
+
return options.document
|
|
691
|
+
?? controller.root?.ownerDocument
|
|
692
|
+
?? controller.root
|
|
693
|
+
?? globalThis.document;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
function mergeOptions(controller, options) {
|
|
245
697
|
return {
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
698
|
+
root: options.root ?? controller.root,
|
|
699
|
+
selector: options.selector ?? controller.selector,
|
|
700
|
+
executeEntries: options.executeEntries ?? controller.executeEntries,
|
|
701
|
+
fetch: options.fetch ?? controller.fetch,
|
|
702
|
+
baseUrl: options.baseUrl ?? controller.baseUrl,
|
|
703
|
+
MutationObserver: options.MutationObserver ?? controller.MutationObserver,
|
|
704
|
+
subtree: options.subtree ?? controller.subtree,
|
|
705
|
+
scope: options.scope ?? controller.scope,
|
|
706
|
+
acceptUnscoped: options.acceptUnscoped ?? controller.acceptUnscoped,
|
|
249
707
|
};
|
|
250
708
|
}
|
|
251
709
|
|
|
@@ -260,7 +718,7 @@ function emptyRegistrationResult() {
|
|
|
260
718
|
function getRoot(options, caller) {
|
|
261
719
|
const root = options.root ?? globalThis.document;
|
|
262
720
|
if (!root || typeof root.querySelectorAll !== "function") {
|
|
263
|
-
throw new TypeError(`${caller}
|
|
721
|
+
throw new TypeError(`${caller} requires a DOM root with querySelectorAll().`);
|
|
264
722
|
}
|
|
265
723
|
return root;
|
|
266
724
|
}
|
|
@@ -271,6 +729,20 @@ function assertRuntime(runtime) {
|
|
|
271
729
|
}
|
|
272
730
|
}
|
|
273
731
|
|
|
732
|
+
function assertElement(element, caller) {
|
|
733
|
+
if (!element || typeof element.getAttribute !== "function") {
|
|
734
|
+
throw new TypeError(`${caller} expects a script element-like object.`);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
function normalizeScope(value) {
|
|
739
|
+
return nonEmpty(value);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
function normalizeModuleId(runtime, id) {
|
|
743
|
+
return runtime.resolve(String(id));
|
|
744
|
+
}
|
|
745
|
+
|
|
274
746
|
function normalizeType(value) {
|
|
275
747
|
return String(value ?? "").trim().toLowerCase();
|
|
276
748
|
}
|
|
@@ -336,8 +808,6 @@ function resolveSourceUrl(src, baseUrl) {
|
|
|
336
808
|
if (baseUrl) return new URL(src, baseUrl).href;
|
|
337
809
|
return new URL(src).href;
|
|
338
810
|
} catch {
|
|
339
|
-
// A DOM element normally supplies baseURI. Keeping the original string is
|
|
340
|
-
// still useful for custom DOM-like environments and injected fetchers.
|
|
341
811
|
return src;
|
|
342
812
|
}
|
|
343
813
|
}
|