@omega.js/client 0.1.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.
Files changed (72) hide show
  1. package/LICENSE +98 -0
  2. package/README.md +874 -0
  3. package/dist/index.js +999 -0
  4. package/dist/modules/analytics.js +584 -0
  5. package/dist/modules/auth.js +469 -0
  6. package/dist/modules/bindings.js +319 -0
  7. package/dist/modules/device.js +282 -0
  8. package/dist/modules/dom.js +96 -0
  9. package/dist/modules/features.js +30 -0
  10. package/dist/modules/firestore.js +313 -0
  11. package/dist/modules/form-manager.js +1577 -0
  12. package/dist/modules/icon-core.js +226 -0
  13. package/dist/modules/icon-renderer.js +149 -0
  14. package/dist/modules/live-page.js +235 -0
  15. package/dist/modules/logger.js +36 -0
  16. package/dist/modules/motion.js +853 -0
  17. package/dist/modules/notifications.js +433 -0
  18. package/dist/modules/path-prefix.js +22 -0
  19. package/dist/modules/request.js +223 -0
  20. package/dist/modules/sentry.js +108 -0
  21. package/dist/modules/service-worker.js +237 -0
  22. package/dist/modules/storage.js +133 -0
  23. package/dist/modules/triggers.js +117 -0
  24. package/dist/modules/utilities.js +479 -0
  25. package/dist/modules/vert-document.js +354 -0
  26. package/dist/modules/verts.js +1133 -0
  27. package/dist/vendor/account/engine.js +182 -0
  28. package/dist/vendor/account/features.js +220 -0
  29. package/dist/vendor/account/index.js +53 -0
  30. package/dist/vendor/account/schema.js +272 -0
  31. package/dist/vendor/account/subscription.js +38 -0
  32. package/dist/vendor/analytics/adapters/ga4.js +26 -0
  33. package/dist/vendor/analytics/adapters/meta.js +26 -0
  34. package/dist/vendor/analytics/adapters/resolve.js +130 -0
  35. package/dist/vendor/analytics/adapters/tiktok.js +27 -0
  36. package/dist/vendor/analytics/catalog.js +908 -0
  37. package/dist/vendor/analytics/consent.js +49 -0
  38. package/dist/vendor/analytics/core.js +141 -0
  39. package/dist/vendor/analytics/identity.js +136 -0
  40. package/dist/vendor/analytics/index.js +170 -0
  41. package/dist/vendor/analytics/logger.js +40 -0
  42. package/dist/vendor/analytics/transports/browser.js +110 -0
  43. package/dist/vendor/monitoring/browser.js +207 -0
  44. package/dist/vendor/monitoring/core.js +180 -0
  45. package/dist/vendor/monitoring/logger.js +39 -0
  46. package/docs/architecture.md +59 -0
  47. package/docs/bindings.md +235 -0
  48. package/docs/build-system.md +32 -0
  49. package/docs/cdp-debugging.md +29 -0
  50. package/docs/code-patterns.md +96 -0
  51. package/docs/common-tasks.md +36 -0
  52. package/docs/dependencies.md +19 -0
  53. package/docs/index.md +159 -0
  54. package/docs/modules.md +180 -0
  55. package/docs/shared/agent-docs.md +89 -0
  56. package/docs/shared/analytics.md +612 -0
  57. package/docs/shared/brands.md +51 -0
  58. package/docs/shared/breaking-changes.md +497 -0
  59. package/docs/shared/config.md +1387 -0
  60. package/docs/shared/deploys.md +215 -0
  61. package/docs/shared/icons.md +201 -0
  62. package/docs/shared/local-dev.md +147 -0
  63. package/docs/shared/logging.md +202 -0
  64. package/docs/shared/monitoring.md +153 -0
  65. package/docs/shared/publishing.md +183 -0
  66. package/docs/shared/rulings.md +34 -0
  67. package/docs/shared/testing.md +147 -0
  68. package/docs/shared/theming.md +604 -0
  69. package/docs/shared/translation.md +291 -0
  70. package/docs/shared/updates.md +61 -0
  71. package/docs/testing.md +9 -0
  72. package/package.json +65 -0
@@ -0,0 +1,319 @@
1
+ import { createLogger } from './logger.js';
2
+
3
+ const logger = createLogger('bindings');
4
+
5
+ class Bindings {
6
+ constructor(manager) {
7
+ this.manager = manager;
8
+ this._context = {};
9
+ }
10
+
11
+ // Update bindings with new data
12
+ update(data = {}) {
13
+ // Merge new data with existing context
14
+ // Whatever keys are provided will overwrite existing values
15
+ this._context = {
16
+ ...this._context,
17
+ ...data
18
+ };
19
+
20
+ // Get the top-level keys that were updating
21
+ const updatedKeys = Object.keys(data);
22
+
23
+ this._updateBindings(this._context, updatedKeys);
24
+ }
25
+
26
+ // Get current context
27
+ getContext() {
28
+ return this._context;
29
+ }
30
+
31
+ // Clear all context
32
+ clear() {
33
+ this._context = {};
34
+ this._updateBindings(this._context, null); // null = update all bindings
35
+ }
36
+
37
+ // Main binding update system
38
+ _updateBindings(context, updatedKeys = null) {
39
+ // Find all elements with data-omega-bind attribute
40
+ const bindElements = document.querySelectorAll('[data-omega-bind]');
41
+
42
+ /* @dev-only:start */
43
+ {
44
+ logger.log('Updating bindings', context, updatedKeys);
45
+ }
46
+ /* @dev-only:end */
47
+
48
+ bindElements.forEach(element => {
49
+ const bindValue = element.getAttribute('data-omega-bind');
50
+
51
+ // Split by comma to support multiple actions
52
+ const bindings = this._parseBindings(bindValue);
53
+
54
+ // Execute each action, track if any were actually processed
55
+ let anyProcessed = false;
56
+ bindings.forEach(({ action, expression }) => {
57
+ if (this._executeAction(element, action, expression, context, updatedKeys)) {
58
+ anyProcessed = true;
59
+ }
60
+ });
61
+
62
+ // Only remove skeleton if at least one binding was actually processed
63
+ if (!anyProcessed) return;
64
+
65
+ // Add bound class to trigger fade out
66
+ element.classList.add('omega-bound');
67
+
68
+ // Remove skeleton class after fade completes
69
+ setTimeout(() => {
70
+ element.classList.remove('omega-binding-skeleton');
71
+ }, 300);
72
+ });
73
+ }
74
+
75
+ // Parse binding string into separate actions
76
+ _parseBindings(bindValue) {
77
+ const bindings = [];
78
+
79
+ // Split by comma, but be smart about it
80
+ // We need to handle cases where commas might be inside expressions
81
+ const parts = bindValue.split(',').map(p => p.trim());
82
+
83
+ parts.forEach(part => {
84
+ let action = '@text'; // Default action
85
+ let expression = part;
86
+
87
+ // Check if it starts with an action keyword
88
+ if (part.startsWith('@')) {
89
+ const spaceIndex = part.indexOf(' ');
90
+ if (spaceIndex > -1) {
91
+ action = part.slice(0, spaceIndex);
92
+ expression = part.slice(spaceIndex + 1).trim();
93
+ } else {
94
+ // No space means it's just an action with no expression (like @hide)
95
+ action = part;
96
+ expression = '';
97
+ }
98
+ }
99
+
100
+ bindings.push({ action, expression });
101
+ });
102
+
103
+ return bindings;
104
+ }
105
+
106
+ // Execute a single action on an element
107
+ // Returns true if the action was processed, false if skipped
108
+ _executeAction(element, action, expression, context, updatedKeys = null) {
109
+ switch (action) {
110
+ case '@show':
111
+ // Show element if condition is true (or always if no condition)
112
+
113
+ // Check if this path should be updated
114
+ if (!this._shouldUpdatePath(expression, updatedKeys)) {
115
+ return false;
116
+ }
117
+
118
+ const shouldShow = expression ? this._evaluateCondition(expression, context) : true;
119
+ if (shouldShow) {
120
+ element.removeAttribute('hidden');
121
+ } else {
122
+ element.setAttribute('hidden', '');
123
+ }
124
+ return true;
125
+
126
+ case '@hide':
127
+ // Hide element if condition is true (or always if no condition)
128
+
129
+ // Check if this path should be updated
130
+ if (!this._shouldUpdatePath(expression, updatedKeys)) {
131
+ return false;
132
+ }
133
+
134
+ const shouldHide = expression ? this._evaluateCondition(expression, context) : true;
135
+ if (shouldHide) {
136
+ element.setAttribute('hidden', '');
137
+ } else {
138
+ element.removeAttribute('hidden');
139
+ }
140
+ return true;
141
+
142
+ case '@attr':
143
+ // Set attribute value
144
+ // Format: @attr attributeName expression
145
+ const attrParts = expression.split(' ');
146
+ const attrName = attrParts[0];
147
+ const attrExpression = attrParts.slice(1).join(' ');
148
+
149
+ // Check if this path should be updated
150
+ if (!this._shouldUpdatePath(attrExpression, updatedKeys)) {
151
+ return false;
152
+ }
153
+
154
+ const attrValue = this._resolvePath(context, attrExpression) || '';
155
+
156
+ if (attrValue) {
157
+ // Block javascript: protocol on URL attributes to prevent XSS
158
+ const URL_ATTRS = ['href', 'src', 'action', 'formaction'];
159
+ if (URL_ATTRS.includes(attrName.toLowerCase())
160
+ && /^\s*javascript\s*:/i.test(String(attrValue))) {
161
+ logger.warn(`Blocked javascript: URL in @attr ${attrName}`);
162
+ return true;
163
+ }
164
+
165
+ element.setAttribute(attrName, attrValue);
166
+ } else {
167
+ element.removeAttribute(attrName);
168
+ }
169
+ return true;
170
+
171
+ case '@style':
172
+ // Set CSS custom property or style
173
+ // Format: @style propertyName expression
174
+ const styleParts = expression.split(' ');
175
+ const styleName = styleParts[0];
176
+ const styleExpression = styleParts.slice(1).join(' ');
177
+
178
+ // Check if this path should be updated
179
+ if (!this._shouldUpdatePath(styleExpression, updatedKeys)) {
180
+ return false;
181
+ }
182
+
183
+ const styleValue = this._resolvePath(context, styleExpression);
184
+
185
+ if (styleValue !== null && styleValue !== undefined && styleValue !== '') {
186
+ // If it starts with --, it's a CSS custom property
187
+ if (styleName.startsWith('--')) {
188
+ element.style.setProperty(styleName, styleValue);
189
+ } else {
190
+ // Regular style property
191
+ element.style[styleName] = styleValue;
192
+ }
193
+ } else {
194
+ // Remove the style if value is empty
195
+ if (styleName.startsWith('--')) {
196
+ element.style.removeProperty(styleName);
197
+ } else {
198
+ element.style[styleName] = '';
199
+ }
200
+ }
201
+ return true;
202
+
203
+ case '@value':
204
+ // Set input/textarea value explicitly
205
+ // Check if this path should be updated
206
+ if (!this._shouldUpdatePath(expression, updatedKeys)) {
207
+ return false;
208
+ }
209
+
210
+ const inputValue = this._resolvePath(context, expression) ?? '';
211
+ element.value = inputValue;
212
+ return true;
213
+
214
+ case '@text':
215
+ default:
216
+ // Set text content (default behavior)
217
+
218
+ // Check if this path should be updated
219
+ if (!this._shouldUpdatePath(expression, updatedKeys)) {
220
+ return false;
221
+ }
222
+
223
+ const textValue = this._resolvePath(context, expression) ?? '';
224
+ element.textContent = textValue;
225
+ return true;
226
+ }
227
+ }
228
+
229
+ // Check if a path should be updated based on updatedKeys
230
+ _shouldUpdatePath(path, updatedKeys) {
231
+ // If no updatedKeys filter, always update
232
+ if (updatedKeys === null || !path) {
233
+ return true;
234
+ }
235
+
236
+ // Strip negation operator if present before extracting root key
237
+ const cleanPath = path.trim().replace(/^!/, '');
238
+
239
+ // Extract the root key from the path
240
+ const rootKey = cleanPath.split('.')[0];
241
+
242
+ // Only update if the root key is in updatedKeys
243
+ return updatedKeys.includes(rootKey);
244
+ }
245
+
246
+ // Resolve nested object path
247
+ _resolvePath(obj, path) {
248
+ if (!obj || !path) return null;
249
+
250
+ return path.split('.').reduce((current, key) => {
251
+ return current?.[key];
252
+ }, obj);
253
+ }
254
+
255
+ // Safely evaluate simple conditions
256
+ _evaluateCondition(condition, context) {
257
+ try {
258
+ // Replace context references with actual values
259
+ // Support: auth.user.field, auth.account.field, simple comparisons
260
+
261
+ // Check for negation operator at the start
262
+ if (condition.trim().startsWith('!')) {
263
+ const expression = condition.trim().slice(1).trim();
264
+ const value = this._resolvePath(context, expression);
265
+ return !value;
266
+ }
267
+
268
+ // Parse the condition to extract left side, operator, and right side
269
+ // Longest alternatives first — `>` before `>=` would shadow `>=` forever
270
+ const comparisonMatch = condition.match(/^(.+?)\s*(===|!==|==|!=|>=|<=|>|<)\s*(.+)$/);
271
+
272
+ if (comparisonMatch) {
273
+ const [, leftPath, operator, rightValue] = comparisonMatch;
274
+
275
+ // Get the left side value
276
+ const leftValue = this._resolvePath(context, leftPath.trim());
277
+
278
+ // Parse the right side (could be string, number, boolean)
279
+ let right = rightValue.trim();
280
+
281
+ // Remove quotes if it's a string
282
+ if ((right.startsWith("'") && right.endsWith("'")) ||
283
+ (right.startsWith('"') && right.endsWith('"'))) {
284
+ right = right.slice(1, -1);
285
+ } else if (right === 'true') {
286
+ right = true;
287
+ } else if (right === 'false') {
288
+ right = false;
289
+ } else if (right === 'null') {
290
+ right = null;
291
+ } else if (!isNaN(right)) {
292
+ right = Number(right);
293
+ }
294
+
295
+ // Evaluate based on operator
296
+ switch (operator) {
297
+ case '===': return leftValue === right;
298
+ case '!==': return leftValue !== right;
299
+ case '==': return leftValue == right;
300
+ case '!=': return leftValue != right;
301
+ case '>': return leftValue > right;
302
+ case '<': return leftValue < right;
303
+ case '>=': return leftValue >= right;
304
+ case '<=': return leftValue <= right;
305
+ default: return false;
306
+ }
307
+ } else {
308
+ // Simple truthy check (e.g., "auth.user.emailVerified" or "auth.account")
309
+ const value = this._resolvePath(context, condition.trim());
310
+ return !!value;
311
+ }
312
+ } catch (error) {
313
+ console.warn('Failed to evaluate condition:', condition, error);
314
+ return false;
315
+ }
316
+ }
317
+ }
318
+
319
+ export default Bindings;
@@ -0,0 +1,282 @@
1
+ import { createLogger } from './logger.js';
2
+
3
+ const logger = createLogger('device');
4
+
5
+ // Unit multipliers
6
+ const UNITS = {
7
+ milliseconds: 1,
8
+ seconds: 1000,
9
+ minutes: 1000 * 60,
10
+ hours: 1000 * 60 * 60,
11
+ days: 1000 * 60 * 60 * 24,
12
+ };
13
+
14
+ // Storage key
15
+ const STORAGE_KEY = 'omega_device';
16
+
17
+ // Session timeout (30 minutes of inactivity = new session)
18
+ const SESSION_TIMEOUT = 30 * 60 * 1000;
19
+
20
+ class Device {
21
+ constructor(manager) {
22
+ this.manager = manager;
23
+ this.data = null;
24
+ this.initialized = false;
25
+ this.isNewVersion = false;
26
+ }
27
+
28
+ // Check if we're in a browser extension context
29
+ _isExtension() {
30
+ return this.manager.utilities().getRuntime() === 'browser-extension';
31
+ }
32
+
33
+ // Get extension storage API
34
+ _getExtensionStorage() {
35
+ if (typeof chrome !== 'undefined' && chrome.storage?.local) {
36
+ return chrome.storage.local;
37
+ }
38
+ if (typeof browser !== 'undefined' && browser.storage?.local) {
39
+ return browser.storage.local;
40
+ }
41
+ return null;
42
+ }
43
+
44
+ // Initialize - loads or creates usage data (async for extensions)
45
+ async initialize() {
46
+ // Skip if already initialized
47
+ if (this.initialized) {
48
+ return this.data;
49
+ }
50
+
51
+ // Load existing data based on runtime
52
+ let existing = null;
53
+
54
+ if (this._isExtension()) {
55
+ existing = await this._loadFromExtensionStorage();
56
+ } else {
57
+ existing = this._loadFromLocalStorage();
58
+ }
59
+
60
+ const now = Date.now();
61
+ const currentVersion = this.manager.config?.version || null;
62
+
63
+ // Stored data is raw JSON.parse output — only a plain object is usable;
64
+ // a primitive or array entry falls through to the first-time payload
65
+ if (existing && typeof existing !== 'object') {
66
+ existing = null;
67
+ }
68
+ if (Array.isArray(existing)) {
69
+ existing = null;
70
+ }
71
+
72
+ if (existing) {
73
+ this.data = existing;
74
+
75
+ // Check if this is a new session (last activity was more than SESSION_TIMEOUT ago)
76
+ const timeSinceLastActive = now - (this.data.lastActive || 0);
77
+ if (timeSinceLastActive > SESSION_TIMEOUT) {
78
+ // A malformed entry missing `session` (or carrying a non-object
79
+ // there) must not break the whole manager boot
80
+ if (!this.data.session || typeof this.data.session !== 'object') {
81
+ this.data.session = {};
82
+ }
83
+ this.data.session.count = (this.data.session.count || 0) + 1;
84
+ this.data.session.started = now;
85
+ }
86
+
87
+ // Update lastActive
88
+ this.data.lastActive = now;
89
+
90
+ // Check for version change
91
+ if (currentVersion && this.data.version?.current !== currentVersion) {
92
+ this.data.version = this.data.version || {};
93
+ this.data.version.previous = this.data.version.current;
94
+ this.data.version.current = currentVersion;
95
+ this.isNewVersion = true;
96
+ }
97
+
98
+ await this._save();
99
+ } else {
100
+ // First time usage
101
+ this.data = {
102
+ installed: now,
103
+ lastActive: now,
104
+ session: {
105
+ started: now,
106
+ count: 1,
107
+ },
108
+ version: {
109
+ initial: currentVersion,
110
+ current: currentVersion,
111
+ previous: null,
112
+ },
113
+ };
114
+ await this._save();
115
+ }
116
+
117
+ this.initialized = true;
118
+ return this.data;
119
+ }
120
+
121
+ // Load from extension storage (async)
122
+ async _loadFromExtensionStorage() {
123
+ const storage = this._getExtensionStorage();
124
+ if (!storage) {
125
+ return null;
126
+ }
127
+
128
+ try {
129
+ const result = await storage.get(STORAGE_KEY);
130
+ return result[STORAGE_KEY] || null;
131
+ } catch (e) {
132
+ logger.warn('Failed to load from extension storage:', e);
133
+ return null;
134
+ }
135
+ }
136
+
137
+ // Load from localStorage (sync)
138
+ _loadFromLocalStorage() {
139
+ try {
140
+ const data = localStorage.getItem(STORAGE_KEY);
141
+ return data ? JSON.parse(data) : null;
142
+ } catch (e) {
143
+ return null;
144
+ }
145
+ }
146
+
147
+ // Save data to storage
148
+ async _save() {
149
+ if (this._isExtension()) {
150
+ await this._saveToExtensionStorage();
151
+ } else {
152
+ this._saveToLocalStorage();
153
+ }
154
+
155
+ return this.data;
156
+ }
157
+
158
+ // Save to extension storage (async)
159
+ async _saveToExtensionStorage() {
160
+ const storage = this._getExtensionStorage();
161
+ if (!storage) {
162
+ return;
163
+ }
164
+
165
+ try {
166
+ await storage.set({ [STORAGE_KEY]: this.data });
167
+ } catch (e) {
168
+ logger.warn('Failed to save to extension storage:', e);
169
+ }
170
+ }
171
+
172
+ // Save to localStorage (sync)
173
+ _saveToLocalStorage() {
174
+ try {
175
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(this.data));
176
+ } catch (e) {
177
+ // localStorage not available
178
+ }
179
+ }
180
+
181
+ // Calculate duration from a timestamp in specified units
182
+ _calculateDuration(timestamp, unit) {
183
+ if (!timestamp) {
184
+ return 0;
185
+ }
186
+
187
+ // Default to milliseconds
188
+ unit = unit || 'milliseconds';
189
+
190
+ // Get multiplier
191
+ const multiplier = UNITS[unit];
192
+ if (!multiplier) {
193
+ throw new Error(`Invalid unit: ${unit}. Valid units: ${Object.keys(UNITS).join(', ')}`);
194
+ }
195
+
196
+ return (Date.now() - timestamp) / multiplier;
197
+ }
198
+
199
+ // Get total usage duration in specified units (since installed)
200
+ getUsageDuration(unit) {
201
+ return this._calculateDuration(this.data?.installed, unit);
202
+ }
203
+
204
+ // Get current session duration in specified units
205
+ getSessionDuration(unit) {
206
+ return this._calculateDuration(this.data?.session?.started, unit);
207
+ }
208
+
209
+ // Get installed date
210
+ getInstalledDate() {
211
+ if (!this.data?.installed) {
212
+ return null;
213
+ }
214
+
215
+ return new Date(this.data.installed);
216
+ }
217
+
218
+ // Get session count
219
+ getSessionCount() {
220
+ return this.data?.session?.count || 0;
221
+ }
222
+
223
+ // Reset usage data (for testing or user request)
224
+ async reset() {
225
+ const now = Date.now();
226
+ const currentVersion = this.manager.config?.version || null;
227
+
228
+ this.data = {
229
+ installed: now,
230
+ lastActive: now,
231
+ session: {
232
+ started: now,
233
+ count: 1,
234
+ },
235
+ version: {
236
+ initial: currentVersion,
237
+ current: currentVersion,
238
+ previous: null,
239
+ },
240
+ };
241
+
242
+ this.isNewVersion = false;
243
+ await this._save();
244
+ return this.data;
245
+ }
246
+
247
+ // Get binding-friendly data object for bindings system
248
+ getBindingData() {
249
+ return {
250
+ installed: this.data?.installed || null,
251
+ lastActive: this.data?.lastActive || null,
252
+ session: {
253
+ started: this.data?.session?.started || null,
254
+ count: this.getSessionCount(),
255
+ },
256
+ version: {
257
+ initial: this.data?.version?.initial || null,
258
+ current: this.data?.version?.current || null,
259
+ previous: this.data?.version?.previous || null,
260
+ isNew: this.isNewVersion,
261
+ },
262
+ duration: {
263
+ total: {
264
+ milliseconds: this.getUsageDuration('milliseconds'),
265
+ seconds: this.getUsageDuration('seconds'),
266
+ minutes: this.getUsageDuration('minutes'),
267
+ hours: this.getUsageDuration('hours'),
268
+ days: this.getUsageDuration('days'),
269
+ },
270
+ session: {
271
+ milliseconds: this.getSessionDuration('milliseconds'),
272
+ seconds: this.getSessionDuration('seconds'),
273
+ minutes: this.getSessionDuration('minutes'),
274
+ hours: this.getSessionDuration('hours'),
275
+ days: this.getSessionDuration('days'),
276
+ },
277
+ },
278
+ };
279
+ }
280
+ }
281
+
282
+ export default Device;
@@ -0,0 +1,96 @@
1
+ // Load external script dynamically
2
+ export function loadScript(options) {
3
+ return new Promise((resolve, reject) => {
4
+ // Handle simple string parameter
5
+ if (typeof options === 'string') {
6
+ options = { src: options };
7
+ }
8
+
9
+ const {
10
+ src,
11
+ async = true,
12
+ defer = false,
13
+ crossorigin = false,
14
+ integrity = null,
15
+ attributes = {},
16
+ timeout = 60000,
17
+ retries = 0,
18
+ parent = null
19
+ } = options;
20
+
21
+ if (!src) {
22
+ return reject(new Error('Script source is required'));
23
+ }
24
+
25
+ let timeoutId;
26
+ let retryCount = 0;
27
+
28
+ function createAndLoadScript() {
29
+ const script = document.createElement('script');
30
+ script.src = src;
31
+ script.async = async;
32
+ script.defer = defer;
33
+
34
+ if (crossorigin) {
35
+ script.crossOrigin = typeof crossorigin === 'string' ? crossorigin : 'anonymous';
36
+ }
37
+
38
+ if (integrity) {
39
+ script.integrity = integrity;
40
+ }
41
+
42
+ // Add custom attributes
43
+ Object.keys(attributes).forEach(name => {
44
+ script.setAttribute(name, attributes[name]);
45
+ });
46
+
47
+ // Set up timeout
48
+ if (timeout > 0) {
49
+ timeoutId = setTimeout(() => {
50
+ script.remove();
51
+ handleError(new Error(`Script load timeout: ${src}`));
52
+ }, timeout);
53
+ }
54
+
55
+ // Event handlers
56
+ script.onload = () => {
57
+ clearTimeout(timeoutId);
58
+ resolve({ script, cached: false });
59
+ };
60
+
61
+ script.onerror = (error) => {
62
+ clearTimeout(timeoutId);
63
+ script.remove();
64
+ handleError(new Error(`Failed to load script ${src}`, { cause: error }));
65
+ };
66
+
67
+ // Append to document
68
+ const $targetParent = parent || document.head || document.documentElement;
69
+ $targetParent.appendChild(script);
70
+ }
71
+
72
+ function handleError(error) {
73
+ if (retryCount < retries) {
74
+ retryCount++;
75
+ setTimeout(createAndLoadScript, 1000 * retryCount);
76
+ } else {
77
+ reject(error);
78
+ }
79
+ }
80
+
81
+ createAndLoadScript();
82
+ });
83
+ }
84
+
85
+ // Return promise that resolves when DOM is ready
86
+ export function ready() {
87
+ return new Promise((resolve) => {
88
+ if (document.readyState === 'loading') {
89
+ // Wait for DOM if still loading
90
+ document.addEventListener('DOMContentLoaded', resolve);
91
+ } else {
92
+ // DOM is already ready
93
+ resolve();
94
+ }
95
+ });
96
+ }