@farm.js/scripts 0.1.0-beta.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 +22 -0
- package/README.md +55 -0
- package/dist/client.d.ts +43 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +669 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +68 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/shared.d.ts +18 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +272 -0
- package/dist/types.d.ts +120 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +56 -0
package/dist/client.js
ADDED
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cloneScriptDefinition, markScriptHandle, normalizeScriptBasePath, normalizeScriptDefinition, sameScriptDefinition, withScriptBasePath, } from "./shared.js";
|
|
3
|
+
const SCRIPT_STORE = Symbol.for("@farm.js/scripts.browser-store");
|
|
4
|
+
export class ScriptEnvironmentError extends Error {
|
|
5
|
+
name = "ScriptEnvironmentError";
|
|
6
|
+
}
|
|
7
|
+
export class ScriptNotRegisteredError extends Error {
|
|
8
|
+
name = "ScriptNotRegisteredError";
|
|
9
|
+
}
|
|
10
|
+
export class ScriptConsentRequiredError extends Error {
|
|
11
|
+
scriptName;
|
|
12
|
+
category;
|
|
13
|
+
name = "ScriptConsentRequiredError";
|
|
14
|
+
constructor(scriptName, category) {
|
|
15
|
+
super(`Script ${JSON.stringify(scriptName)} requires granted consent for ${JSON.stringify(category)}`);
|
|
16
|
+
this.scriptName = scriptName;
|
|
17
|
+
this.category = category;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export class ScriptLoadError extends Error {
|
|
21
|
+
scriptName;
|
|
22
|
+
src;
|
|
23
|
+
name = "ScriptLoadError";
|
|
24
|
+
constructor(scriptName, src) {
|
|
25
|
+
super(`Script ${JSON.stringify(scriptName)} failed to load from ${JSON.stringify(src)}`);
|
|
26
|
+
this.scriptName = scriptName;
|
|
27
|
+
this.src = src;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
export class ScriptTimeoutError extends Error {
|
|
31
|
+
scriptName;
|
|
32
|
+
timeoutMs;
|
|
33
|
+
name = "ScriptTimeoutError";
|
|
34
|
+
constructor(scriptName, timeoutMs) {
|
|
35
|
+
super(`Script ${JSON.stringify(scriptName)} did not load within ${timeoutMs} ms`);
|
|
36
|
+
this.scriptName = scriptName;
|
|
37
|
+
this.timeoutMs = timeoutMs;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export class ScriptGlobalMissingError extends Error {
|
|
41
|
+
scriptName;
|
|
42
|
+
globalPath;
|
|
43
|
+
name = "ScriptGlobalMissingError";
|
|
44
|
+
constructor(scriptName, globalPath) {
|
|
45
|
+
super(`Script ${JSON.stringify(scriptName)} loaded but did not expose global ${JSON.stringify(globalPath)}`);
|
|
46
|
+
this.scriptName = scriptName;
|
|
47
|
+
this.globalPath = globalPath;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/** Define one typed external browser script. Register the returned handle with `scripts()`. */
|
|
51
|
+
export function defineScript(definition) {
|
|
52
|
+
const resolved = normalizeScriptDefinition(definition);
|
|
53
|
+
registerLocalDefinition(resolved);
|
|
54
|
+
const handle = {
|
|
55
|
+
name: resolved.name,
|
|
56
|
+
definition: resolved,
|
|
57
|
+
get status() {
|
|
58
|
+
const store = currentStore();
|
|
59
|
+
return store?.entries.get(resolved.name)?.status ?? "idle";
|
|
60
|
+
},
|
|
61
|
+
get error() {
|
|
62
|
+
const store = currentStore();
|
|
63
|
+
return store?.entries.get(resolved.name)?.error;
|
|
64
|
+
},
|
|
65
|
+
async load() {
|
|
66
|
+
const store = requireBrowserStore(resolved.name);
|
|
67
|
+
return (await loadRegisteredScript(store, resolved.name));
|
|
68
|
+
},
|
|
69
|
+
async use(callback) {
|
|
70
|
+
if (typeof callback !== "function") {
|
|
71
|
+
throw new TypeError("script use callback must be a function");
|
|
72
|
+
}
|
|
73
|
+
const sdk = await handle.load();
|
|
74
|
+
return (await callback(sdk));
|
|
75
|
+
},
|
|
76
|
+
subscribe(listener) {
|
|
77
|
+
if (typeof listener !== "function") {
|
|
78
|
+
throw new TypeError("script status listener must be a function");
|
|
79
|
+
}
|
|
80
|
+
const store = currentStore();
|
|
81
|
+
if (!store)
|
|
82
|
+
return () => { };
|
|
83
|
+
const entry = ensureEntry(store, resolved, false);
|
|
84
|
+
entry.listeners.add(listener);
|
|
85
|
+
callListener(store, listener, snapshot(entry));
|
|
86
|
+
return () => entry.listeners.delete(listener);
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
return Object.freeze(markScriptHandle(handle, resolved));
|
|
90
|
+
}
|
|
91
|
+
/** Set an application-owned consent category. Consent is intentionally not persisted by Farm. */
|
|
92
|
+
export function setScriptConsent(category, state) {
|
|
93
|
+
const normalized = normalizeConsentCategory(category);
|
|
94
|
+
if (state !== "unknown" && state !== "granted" && state !== "denied") {
|
|
95
|
+
throw new TypeError('script consent state must be "unknown", "granted", or "denied"');
|
|
96
|
+
}
|
|
97
|
+
const store = currentStore();
|
|
98
|
+
if (!store)
|
|
99
|
+
return;
|
|
100
|
+
if (state === "unknown")
|
|
101
|
+
store.consent.delete(normalized);
|
|
102
|
+
else
|
|
103
|
+
store.consent.set(normalized, state);
|
|
104
|
+
if (state === "granted") {
|
|
105
|
+
for (const entry of triggeredConsentDependents(store, normalized)) {
|
|
106
|
+
if (entry.status === "ready" || entry.status === "loading")
|
|
107
|
+
continue;
|
|
108
|
+
void loadRegisteredScript(store, entry.definition.name).catch((error) => {
|
|
109
|
+
if (!(error instanceof ScriptConsentRequiredError))
|
|
110
|
+
reportAutomaticError(store, error);
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
for (const entry of store.entries.values()) {
|
|
116
|
+
if (entry.definition.consent === normalized &&
|
|
117
|
+
entry.triggered &&
|
|
118
|
+
entry.status !== "ready" &&
|
|
119
|
+
entry.status !== "loading") {
|
|
120
|
+
setEntryState(store, entry, "blocked");
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
export function grantScriptConsent(category) {
|
|
126
|
+
setScriptConsent(category, "granted");
|
|
127
|
+
}
|
|
128
|
+
export function denyScriptConsent(category) {
|
|
129
|
+
setScriptConsent(category, "denied");
|
|
130
|
+
}
|
|
131
|
+
export function clearScriptConsent(category) {
|
|
132
|
+
setScriptConsent(category, "unknown");
|
|
133
|
+
}
|
|
134
|
+
export function getScriptConsent(category) {
|
|
135
|
+
const normalized = normalizeConsentCategory(category);
|
|
136
|
+
const store = currentStore();
|
|
137
|
+
if (!store)
|
|
138
|
+
return "unknown";
|
|
139
|
+
return store.consent.get(normalized) ?? "unknown";
|
|
140
|
+
}
|
|
141
|
+
/** @internal Start the browser lifecycle for definitions serialized by the Farm plugin. */
|
|
142
|
+
export function startScriptRuntime(definitions, runtimeWindow = window, basePath = "/") {
|
|
143
|
+
const store = getStore(runtimeWindow, basePath);
|
|
144
|
+
const entries = definitions.map((definition) => ensureEntry(store, cloneScriptDefinition(definition), true));
|
|
145
|
+
let closed = false;
|
|
146
|
+
let idleHandle;
|
|
147
|
+
let interactionHandler;
|
|
148
|
+
let mutationObserver;
|
|
149
|
+
let refreshQueued = false;
|
|
150
|
+
const visibleObservers = new Map();
|
|
151
|
+
for (const entry of entries) {
|
|
152
|
+
if (entry.definition.preconnect &&
|
|
153
|
+
(!entry.definition.consent || getConsent(store, entry.definition.consent) === "granted")) {
|
|
154
|
+
addPreconnect(store, entry.definition);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function unobserveScript(name) {
|
|
158
|
+
for (const { observer, targets } of visibleObservers.values()) {
|
|
159
|
+
for (const [element, names] of targets) {
|
|
160
|
+
names.delete(name);
|
|
161
|
+
if (names.size > 0)
|
|
162
|
+
continue;
|
|
163
|
+
observer.unobserve(element);
|
|
164
|
+
targets.delete(element);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function request(entry) {
|
|
169
|
+
if (closed)
|
|
170
|
+
return;
|
|
171
|
+
entry.triggered = true;
|
|
172
|
+
void loadRegisteredScript(store, entry.definition.name).catch((error) => {
|
|
173
|
+
if (!(error instanceof ScriptConsentRequiredError))
|
|
174
|
+
reportAutomaticError(store, error);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
function requestByStrategy(strategy) {
|
|
178
|
+
for (const entry of entries) {
|
|
179
|
+
if (entry.definition.load === strategy)
|
|
180
|
+
request(entry);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
function refreshVisible() {
|
|
184
|
+
if (closed)
|
|
185
|
+
return;
|
|
186
|
+
for (const { observer, targets } of visibleObservers.values()) {
|
|
187
|
+
for (const element of targets.keys()) {
|
|
188
|
+
if (element.isConnected)
|
|
189
|
+
continue;
|
|
190
|
+
observer.unobserve(element);
|
|
191
|
+
targets.delete(element);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
for (const entry of entries) {
|
|
195
|
+
const strategy = entry.definition.load;
|
|
196
|
+
if (typeof strategy !== "object" || strategy.when !== "visible" || entry.triggered)
|
|
197
|
+
continue;
|
|
198
|
+
let elements;
|
|
199
|
+
try {
|
|
200
|
+
elements = runtimeWindow.document.querySelectorAll(strategy.selector);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
const error = new TypeError(`Script ${JSON.stringify(entry.definition.name)} has invalid visible selector ${JSON.stringify(strategy.selector)}`);
|
|
204
|
+
entry.triggered = true;
|
|
205
|
+
setEntryState(store, entry, "error", error);
|
|
206
|
+
reportAutomaticError(store, error);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
for (const element of elements) {
|
|
210
|
+
const group = visibleObservers.get(strategy.rootMargin);
|
|
211
|
+
if (!group) {
|
|
212
|
+
request(entry);
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
const names = group.targets.get(element) ?? new Set();
|
|
216
|
+
if (names.has(entry.definition.name))
|
|
217
|
+
continue;
|
|
218
|
+
names.add(entry.definition.name);
|
|
219
|
+
group.targets.set(element, names);
|
|
220
|
+
group.observer.observe(element);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
function scheduleVisibleRefresh() {
|
|
225
|
+
if (closed || refreshQueued)
|
|
226
|
+
return;
|
|
227
|
+
refreshQueued = true;
|
|
228
|
+
runtimeWindow.queueMicrotask(() => {
|
|
229
|
+
refreshQueued = false;
|
|
230
|
+
refreshVisible();
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
const visibleEntries = entries.filter((entry) => typeof entry.definition.load === "object");
|
|
234
|
+
const visibleRequestListeners = new Map();
|
|
235
|
+
for (const entry of visibleEntries) {
|
|
236
|
+
const listener = () => unobserveScript(entry.definition.name);
|
|
237
|
+
entry.requestListeners.add(listener);
|
|
238
|
+
visibleRequestListeners.set(entry, listener);
|
|
239
|
+
}
|
|
240
|
+
if (visibleEntries.length > 0) {
|
|
241
|
+
const Observer = runtimeWindow.IntersectionObserver;
|
|
242
|
+
if (typeof Observer === "function") {
|
|
243
|
+
for (const entry of visibleEntries) {
|
|
244
|
+
const strategy = entry.definition.load;
|
|
245
|
+
if (typeof strategy !== "object" || visibleObservers.has(strategy.rootMargin))
|
|
246
|
+
continue;
|
|
247
|
+
const targets = new Map();
|
|
248
|
+
const observer = new Observer((observations) => {
|
|
249
|
+
for (const observation of observations) {
|
|
250
|
+
if (!observation.isIntersecting)
|
|
251
|
+
continue;
|
|
252
|
+
const names = targets.get(observation.target);
|
|
253
|
+
if (!names)
|
|
254
|
+
continue;
|
|
255
|
+
observer.unobserve(observation.target);
|
|
256
|
+
targets.delete(observation.target);
|
|
257
|
+
for (const name of names) {
|
|
258
|
+
const visibleEntry = store.entries.get(name);
|
|
259
|
+
if (visibleEntry)
|
|
260
|
+
request(visibleEntry);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}, { rootMargin: strategy.rootMargin });
|
|
264
|
+
visibleObservers.set(strategy.rootMargin, { observer, targets });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const Mutation = runtimeWindow.MutationObserver;
|
|
268
|
+
if (typeof Mutation === "function") {
|
|
269
|
+
mutationObserver = new Mutation(scheduleVisibleRefresh);
|
|
270
|
+
mutationObserver.observe(runtimeWindow.document.documentElement, {
|
|
271
|
+
childList: true,
|
|
272
|
+
subtree: true,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
scheduleVisibleRefresh();
|
|
276
|
+
}
|
|
277
|
+
requestByStrategy("immediate");
|
|
278
|
+
if (entries.some((entry) => entry.definition.load === "idle")) {
|
|
279
|
+
if (typeof runtimeWindow.requestIdleCallback === "function") {
|
|
280
|
+
idleHandle = runtimeWindow.requestIdleCallback(() => requestByStrategy("idle"), {
|
|
281
|
+
timeout: 2_000,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
else {
|
|
285
|
+
idleHandle = runtimeWindow.setTimeout(() => requestByStrategy("idle"), 1);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (entries.some((entry) => entry.definition.load === "interaction")) {
|
|
289
|
+
interactionHandler = () => {
|
|
290
|
+
removeInteractionListeners(runtimeWindow, interactionHandler);
|
|
291
|
+
interactionHandler = undefined;
|
|
292
|
+
requestByStrategy("interaction");
|
|
293
|
+
};
|
|
294
|
+
addInteractionListeners(runtimeWindow, interactionHandler);
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
afterHydration() {
|
|
298
|
+
requestByStrategy("after-hydration");
|
|
299
|
+
scheduleVisibleRefresh();
|
|
300
|
+
},
|
|
301
|
+
refresh: scheduleVisibleRefresh,
|
|
302
|
+
close() {
|
|
303
|
+
if (closed)
|
|
304
|
+
return;
|
|
305
|
+
closed = true;
|
|
306
|
+
if (idleHandle !== undefined) {
|
|
307
|
+
if (typeof runtimeWindow.cancelIdleCallback === "function") {
|
|
308
|
+
runtimeWindow.cancelIdleCallback(idleHandle);
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
runtimeWindow.clearTimeout(idleHandle);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
if (interactionHandler)
|
|
315
|
+
removeInteractionListeners(runtimeWindow, interactionHandler);
|
|
316
|
+
for (const { observer, targets } of visibleObservers.values()) {
|
|
317
|
+
observer.disconnect();
|
|
318
|
+
targets.clear();
|
|
319
|
+
}
|
|
320
|
+
for (const [entry, listener] of visibleRequestListeners) {
|
|
321
|
+
entry.requestListeners.delete(listener);
|
|
322
|
+
}
|
|
323
|
+
mutationObserver?.disconnect();
|
|
324
|
+
visibleObservers.clear();
|
|
325
|
+
},
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
function currentStore() {
|
|
329
|
+
if (typeof window === "undefined")
|
|
330
|
+
return undefined;
|
|
331
|
+
return getStore(window);
|
|
332
|
+
}
|
|
333
|
+
function requireBrowserStore(name) {
|
|
334
|
+
const store = currentStore();
|
|
335
|
+
if (!store) {
|
|
336
|
+
throw new ScriptEnvironmentError(`Script ${JSON.stringify(name)} can only be loaded in a browser environment`);
|
|
337
|
+
}
|
|
338
|
+
return store;
|
|
339
|
+
}
|
|
340
|
+
function getStore(runtimeWindow, basePath) {
|
|
341
|
+
const host = runtimeWindow;
|
|
342
|
+
if (host[SCRIPT_STORE]) {
|
|
343
|
+
if (basePath !== undefined)
|
|
344
|
+
host[SCRIPT_STORE].basePath = normalizeScriptBasePath(basePath);
|
|
345
|
+
return host[SCRIPT_STORE];
|
|
346
|
+
}
|
|
347
|
+
const store = {
|
|
348
|
+
window: runtimeWindow,
|
|
349
|
+
basePath: normalizeScriptBasePath(basePath),
|
|
350
|
+
entries: new Map(),
|
|
351
|
+
consent: new Map(),
|
|
352
|
+
};
|
|
353
|
+
Object.defineProperty(host, SCRIPT_STORE, {
|
|
354
|
+
configurable: true,
|
|
355
|
+
enumerable: false,
|
|
356
|
+
writable: false,
|
|
357
|
+
value: store,
|
|
358
|
+
});
|
|
359
|
+
return store;
|
|
360
|
+
}
|
|
361
|
+
function registerLocalDefinition(definition) {
|
|
362
|
+
const store = currentStore();
|
|
363
|
+
if (store)
|
|
364
|
+
ensureEntry(store, definition, false);
|
|
365
|
+
}
|
|
366
|
+
function ensureEntry(store, definition, configured) {
|
|
367
|
+
const existing = store.entries.get(definition.name);
|
|
368
|
+
if (existing) {
|
|
369
|
+
if (!sameScriptDefinition(existing.definition, definition)) {
|
|
370
|
+
throw new TypeError(`Script ${JSON.stringify(definition.name)} was defined with conflicting options`);
|
|
371
|
+
}
|
|
372
|
+
if (configured)
|
|
373
|
+
existing.configured = true;
|
|
374
|
+
return existing;
|
|
375
|
+
}
|
|
376
|
+
const entry = {
|
|
377
|
+
definition,
|
|
378
|
+
configured,
|
|
379
|
+
status: "idle",
|
|
380
|
+
attempt: 0,
|
|
381
|
+
triggered: false,
|
|
382
|
+
listeners: new Set(),
|
|
383
|
+
requestListeners: new Set(),
|
|
384
|
+
};
|
|
385
|
+
store.entries.set(definition.name, entry);
|
|
386
|
+
return entry;
|
|
387
|
+
}
|
|
388
|
+
async function loadRegisteredScript(store, name, stack = []) {
|
|
389
|
+
const entry = store.entries.get(name);
|
|
390
|
+
if (!entry?.configured) {
|
|
391
|
+
throw new ScriptNotRegisteredError(`Script ${JSON.stringify(name)} is not registered. Pass its defineScript() handle to scripts({ scripts: [...] })`);
|
|
392
|
+
}
|
|
393
|
+
entry.triggered = true;
|
|
394
|
+
for (const listener of entry.requestListeners)
|
|
395
|
+
listener();
|
|
396
|
+
if (entry.status === "ready") {
|
|
397
|
+
try {
|
|
398
|
+
return requireLoadedValue(store, entry.definition);
|
|
399
|
+
}
|
|
400
|
+
catch (error) {
|
|
401
|
+
setEntryState(store, entry, "error", error);
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (entry.promise)
|
|
405
|
+
return entry.promise;
|
|
406
|
+
if (entry.definition.consent && getConsent(store, entry.definition.consent) !== "granted") {
|
|
407
|
+
setEntryState(store, entry, "blocked");
|
|
408
|
+
throw new ScriptConsentRequiredError(name, entry.definition.consent);
|
|
409
|
+
}
|
|
410
|
+
if (entry.definition.preconnect)
|
|
411
|
+
addPreconnect(store, entry.definition);
|
|
412
|
+
if (stack.includes(name)) {
|
|
413
|
+
throw new Error(`Script dependency cycle reached in the browser: ${[...stack, name].join(" -> ")}`);
|
|
414
|
+
}
|
|
415
|
+
const promise = (async () => {
|
|
416
|
+
for (const dependency of entry.definition.dependsOn) {
|
|
417
|
+
try {
|
|
418
|
+
await loadRegisteredScript(store, dependency, [...stack, name]);
|
|
419
|
+
}
|
|
420
|
+
catch (error) {
|
|
421
|
+
if (error instanceof ScriptConsentRequiredError)
|
|
422
|
+
setEntryState(store, entry, "blocked");
|
|
423
|
+
else
|
|
424
|
+
setEntryState(store, entry, "error", error);
|
|
425
|
+
throw error;
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
setEntryState(store, entry, "loading");
|
|
429
|
+
const maximumAttempts = entry.definition.retries + 1;
|
|
430
|
+
let lastError;
|
|
431
|
+
for (let attempt = 1; attempt <= maximumAttempts; attempt += 1) {
|
|
432
|
+
entry.attempt = attempt;
|
|
433
|
+
notify(store, entry);
|
|
434
|
+
try {
|
|
435
|
+
const value = await loadElement(store, entry.definition);
|
|
436
|
+
setEntryState(store, entry, "ready");
|
|
437
|
+
return value;
|
|
438
|
+
}
|
|
439
|
+
catch (error) {
|
|
440
|
+
lastError = error;
|
|
441
|
+
if (attempt < maximumAttempts) {
|
|
442
|
+
await delay(store.window, entry.definition.retryDelayMs);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
setEntryState(store, entry, "error", lastError);
|
|
447
|
+
throw lastError;
|
|
448
|
+
})();
|
|
449
|
+
entry.promise = promise;
|
|
450
|
+
try {
|
|
451
|
+
return await promise;
|
|
452
|
+
}
|
|
453
|
+
finally {
|
|
454
|
+
if (entry.promise === promise && entry.status !== "loading")
|
|
455
|
+
entry.promise = undefined;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
async function loadElement(store, definition) {
|
|
459
|
+
const existingValue = findLoadedValue(store, definition);
|
|
460
|
+
if (existingValue.found)
|
|
461
|
+
return existingValue.value;
|
|
462
|
+
removeStaleElements(store.window.document, definition.name);
|
|
463
|
+
const script = store.window.document.createElement("script");
|
|
464
|
+
script.dataset.farmScript = definition.name;
|
|
465
|
+
script.src = withScriptBasePath(definition.src, store.basePath);
|
|
466
|
+
script.async = definition.async;
|
|
467
|
+
if (definition.type === "module")
|
|
468
|
+
script.type = "module";
|
|
469
|
+
if (definition.id)
|
|
470
|
+
script.id = definition.id;
|
|
471
|
+
if (definition.integrity)
|
|
472
|
+
script.integrity = definition.integrity;
|
|
473
|
+
if (definition.crossOrigin)
|
|
474
|
+
script.crossOrigin = definition.crossOrigin;
|
|
475
|
+
if (definition.referrerPolicy)
|
|
476
|
+
script.referrerPolicy = definition.referrerPolicy;
|
|
477
|
+
if (definition.fetchPriority)
|
|
478
|
+
script.setAttribute("fetchpriority", definition.fetchPriority);
|
|
479
|
+
for (const [key, value] of Object.entries(definition.attributes)) {
|
|
480
|
+
script.setAttribute(key, value);
|
|
481
|
+
}
|
|
482
|
+
const parent = definition.placement === "body"
|
|
483
|
+
? (store.window.document.body ?? store.window.document.head)
|
|
484
|
+
: (store.window.document.head ?? store.window.document.body);
|
|
485
|
+
if (!parent) {
|
|
486
|
+
throw new ScriptLoadError(definition.name, withScriptBasePath(definition.src, store.basePath));
|
|
487
|
+
}
|
|
488
|
+
const loaded = new Promise((resolve, reject) => {
|
|
489
|
+
let settled = false;
|
|
490
|
+
const finish = (error) => {
|
|
491
|
+
if (settled)
|
|
492
|
+
return;
|
|
493
|
+
settled = true;
|
|
494
|
+
store.window.clearTimeout(timeout);
|
|
495
|
+
script.removeEventListener("load", onLoad);
|
|
496
|
+
script.removeEventListener("error", onError);
|
|
497
|
+
if (error) {
|
|
498
|
+
script.dataset.farmScriptState = "error";
|
|
499
|
+
script.remove();
|
|
500
|
+
reject(error);
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
script.dataset.farmScriptState = "ready";
|
|
504
|
+
resolve();
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
const onLoad = () => finish();
|
|
508
|
+
const onError = () => finish(new ScriptLoadError(definition.name, script.src));
|
|
509
|
+
const timeout = store.window.setTimeout(() => finish(new ScriptTimeoutError(definition.name, definition.timeoutMs)), definition.timeoutMs);
|
|
510
|
+
script.addEventListener("load", onLoad, { once: true });
|
|
511
|
+
script.addEventListener("error", onError, { once: true });
|
|
512
|
+
});
|
|
513
|
+
parent.append(script);
|
|
514
|
+
try {
|
|
515
|
+
await loaded;
|
|
516
|
+
if (!definition.global)
|
|
517
|
+
return undefined;
|
|
518
|
+
return await waitForGlobal(store, definition);
|
|
519
|
+
}
|
|
520
|
+
catch (error) {
|
|
521
|
+
script.dataset.farmScriptState = "error";
|
|
522
|
+
script.remove();
|
|
523
|
+
throw error;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
async function waitForGlobal(store, definition) {
|
|
527
|
+
const immediate = findLoadedValue(store, definition);
|
|
528
|
+
if (immediate.found)
|
|
529
|
+
return immediate.value;
|
|
530
|
+
if (definition.readyTimeoutMs === 0) {
|
|
531
|
+
throw new ScriptGlobalMissingError(definition.name, definition.global);
|
|
532
|
+
}
|
|
533
|
+
const deadline = Date.now() + definition.readyTimeoutMs;
|
|
534
|
+
while (Date.now() < deadline) {
|
|
535
|
+
await delay(store.window, Math.min(25, Math.max(1, deadline - Date.now())));
|
|
536
|
+
const result = findLoadedValue(store, definition);
|
|
537
|
+
if (result.found)
|
|
538
|
+
return result.value;
|
|
539
|
+
}
|
|
540
|
+
throw new ScriptGlobalMissingError(definition.name, definition.global);
|
|
541
|
+
}
|
|
542
|
+
function findLoadedValue(store, definition) {
|
|
543
|
+
if (!definition.global) {
|
|
544
|
+
const element = findManagedElement(store.window.document, definition.name);
|
|
545
|
+
return { found: element?.dataset.farmScriptState === "ready", value: undefined };
|
|
546
|
+
}
|
|
547
|
+
let owner = store.window;
|
|
548
|
+
for (const segment of definition.global.split(".")) {
|
|
549
|
+
if ((typeof owner !== "object" && typeof owner !== "function") || owner === null) {
|
|
550
|
+
return { found: false };
|
|
551
|
+
}
|
|
552
|
+
owner = Reflect.get(owner, segment);
|
|
553
|
+
}
|
|
554
|
+
const found = owner !== undefined && owner !== null;
|
|
555
|
+
return { found, ...(found ? { value: owner } : {}) };
|
|
556
|
+
}
|
|
557
|
+
function requireLoadedValue(store, definition) {
|
|
558
|
+
const result = findLoadedValue(store, definition);
|
|
559
|
+
if (result.found)
|
|
560
|
+
return result.value;
|
|
561
|
+
throw new ScriptGlobalMissingError(definition.name, definition.global);
|
|
562
|
+
}
|
|
563
|
+
function findManagedElement(document, name) {
|
|
564
|
+
for (const element of document.querySelectorAll("script[data-farm-script]")) {
|
|
565
|
+
if (element.dataset.farmScript === name)
|
|
566
|
+
return element;
|
|
567
|
+
}
|
|
568
|
+
return undefined;
|
|
569
|
+
}
|
|
570
|
+
function removeStaleElements(document, name) {
|
|
571
|
+
for (const element of document.querySelectorAll("script[data-farm-script]")) {
|
|
572
|
+
if (element.dataset.farmScript === name)
|
|
573
|
+
element.remove();
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
function addPreconnect(store, definition) {
|
|
577
|
+
let source;
|
|
578
|
+
try {
|
|
579
|
+
source = new URL(definition.src, store.window.location.href);
|
|
580
|
+
}
|
|
581
|
+
catch {
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
if (source.origin === store.window.location.origin)
|
|
585
|
+
return;
|
|
586
|
+
for (const link of store.window.document.querySelectorAll('link[rel="preconnect"][data-farm-script-preconnect]')) {
|
|
587
|
+
if (link.href === source.origin + "/")
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
const link = store.window.document.createElement("link");
|
|
591
|
+
link.rel = "preconnect";
|
|
592
|
+
link.href = source.origin;
|
|
593
|
+
link.dataset.farmScriptPreconnect = definition.name;
|
|
594
|
+
if (definition.crossOrigin)
|
|
595
|
+
link.crossOrigin = definition.crossOrigin;
|
|
596
|
+
store.window.document.head?.append(link);
|
|
597
|
+
}
|
|
598
|
+
function addInteractionListeners(runtimeWindow, listener) {
|
|
599
|
+
const options = { capture: true, passive: true, once: true };
|
|
600
|
+
runtimeWindow.document.addEventListener("pointerdown", listener, options);
|
|
601
|
+
runtimeWindow.document.addEventListener("keydown", listener, options);
|
|
602
|
+
runtimeWindow.document.addEventListener("touchstart", listener, options);
|
|
603
|
+
}
|
|
604
|
+
function removeInteractionListeners(runtimeWindow, listener) {
|
|
605
|
+
runtimeWindow.document.removeEventListener("pointerdown", listener, true);
|
|
606
|
+
runtimeWindow.document.removeEventListener("keydown", listener, true);
|
|
607
|
+
runtimeWindow.document.removeEventListener("touchstart", listener, true);
|
|
608
|
+
}
|
|
609
|
+
function getConsent(store, category) {
|
|
610
|
+
return store.consent.get(category) ?? "unknown";
|
|
611
|
+
}
|
|
612
|
+
function triggeredConsentDependents(store, category) {
|
|
613
|
+
const names = new Set([...store.entries.values()]
|
|
614
|
+
.filter((entry) => entry.triggered && entry.definition.consent === category)
|
|
615
|
+
.map((entry) => entry.definition.name));
|
|
616
|
+
let changed = true;
|
|
617
|
+
while (changed) {
|
|
618
|
+
changed = false;
|
|
619
|
+
for (const entry of store.entries.values()) {
|
|
620
|
+
if (entry.triggered &&
|
|
621
|
+
!names.has(entry.definition.name) &&
|
|
622
|
+
entry.definition.dependsOn.some((dependency) => names.has(dependency))) {
|
|
623
|
+
names.add(entry.definition.name);
|
|
624
|
+
changed = true;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
return [...names].map((name) => store.entries.get(name));
|
|
629
|
+
}
|
|
630
|
+
function normalizeConsentCategory(category) {
|
|
631
|
+
if (typeof category !== "string" || !category.trim()) {
|
|
632
|
+
throw new TypeError("script consent category must be a non-empty string");
|
|
633
|
+
}
|
|
634
|
+
return category.trim();
|
|
635
|
+
}
|
|
636
|
+
function setEntryState(store, entry, status, error) {
|
|
637
|
+
entry.status = status;
|
|
638
|
+
entry.error = error;
|
|
639
|
+
notify(store, entry);
|
|
640
|
+
}
|
|
641
|
+
function notify(store, entry) {
|
|
642
|
+
const value = snapshot(entry);
|
|
643
|
+
for (const listener of entry.listeners)
|
|
644
|
+
callListener(store, listener, value);
|
|
645
|
+
}
|
|
646
|
+
function snapshot(entry) {
|
|
647
|
+
return Object.freeze({
|
|
648
|
+
name: entry.definition.name,
|
|
649
|
+
status: entry.status,
|
|
650
|
+
attempt: entry.attempt,
|
|
651
|
+
...(entry.error === undefined ? {} : { error: entry.error }),
|
|
652
|
+
});
|
|
653
|
+
}
|
|
654
|
+
function callListener(store, listener, value) {
|
|
655
|
+
try {
|
|
656
|
+
listener(value);
|
|
657
|
+
}
|
|
658
|
+
catch (error) {
|
|
659
|
+
store.window.console.error("[farm:scripts] A status listener failed", error);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
function reportAutomaticError(store, error) {
|
|
663
|
+
store.window.console.warn("[farm:scripts] An automatically loaded script failed", error);
|
|
664
|
+
}
|
|
665
|
+
function delay(runtimeWindow, durationMs) {
|
|
666
|
+
if (durationMs === 0)
|
|
667
|
+
return Promise.resolve();
|
|
668
|
+
return new Promise((resolve) => runtimeWindow.setTimeout(resolve, durationMs));
|
|
669
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAA4B,sBAAsB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAEnG,wBAAgB,qBAAqB,CAAC,OAAO,EAAE,cAAc,GAAG,sBAAsB,CAyDrF"}
|