@salesforce/lightning-out 2.2.0-rc.2

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.
@@ -0,0 +1,1724 @@
1
+ /*! @salesforce/lightning-out v2.2.0-rc.2 (2026-03-18) */
2
+ var LO2 = (function (exports) {
3
+ 'use strict';
4
+
5
+ /**
6
+ * EmbeddingResizer - Handles dynamic iframe/container resizing
7
+ * Uses ResizeObserver to monitor element size changes and notify the host
8
+ */
9
+ /**
10
+ * Generates a pseudo-random alphanumeric identifier string for unique
11
+ * element IDs or temporary identifiers (not cryptographically secure).
12
+ *
13
+ * @returns Random alphanumeric string
14
+ *
15
+ * @example
16
+ * getUUID(); // 'k2j8f5l9m'
17
+ */
18
+ function getUUID() {
19
+ return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER).toString(36);
20
+ }
21
+
22
+ /**
23
+ * Events fired by dispatchEvent
24
+ */
25
+ const events = {
26
+ lo: {
27
+ // public
28
+ application: {
29
+ ready: "lo.application.ready",
30
+ error: "lo.application.error",
31
+ logout: "lo.application.logout",
32
+ auth: {
33
+ redirect: "lo.application.auth.redirect",
34
+ },
35
+ },
36
+ // public
37
+ component: {
38
+ ready: "lo.component.ready",
39
+ error: "lo.component.error",
40
+ },
41
+ // internal
42
+ iframe: {
43
+ load: "lo.iframe.load",
44
+ error: "lo.iframe.error",
45
+ logout: "lo.iframe.logout",
46
+ auth: {
47
+ redirect: "lo.iframe.auth.redirect",
48
+ },
49
+ },
50
+ },
51
+ };
52
+ /**
53
+ * Messages sent by postMessage (all internal)
54
+ */
55
+ const messages = {
56
+ lo: {
57
+ addEventListener: "lo.addEventListener",
58
+ dispatchEvent: "lo.dispatchEvent",
59
+ error: "lo.error",
60
+ getComponentData: "lo.getComponentData",
61
+ loaded: "lo.loaded",
62
+ logout: "lo.logout",
63
+ ready: "lo.ready",
64
+ redirect: "lo.redirect",
65
+ removeEventListener: "lo.removeEventListener",
66
+ setComponentData: "lo.setComponentData",
67
+ setComponentProps: "lo.setComponentProps",
68
+ },
69
+ };
70
+
71
+ const logLevels = {
72
+ error: 0,
73
+ warn: 1,
74
+ info: 2,
75
+ debug: 3,
76
+ trace: 4,
77
+ };
78
+ class Logger {
79
+ static #prefix = "LO2";
80
+ static #level = "error";
81
+ #branding;
82
+ static set level(level) {
83
+ this.#level = level;
84
+ }
85
+ static set prefix(prefix) {
86
+ this.#prefix = prefix;
87
+ }
88
+ // This string appears first in the console, it can be used for filtering messages
89
+ get brand() {
90
+ return `${Logger.#prefix}:${this.#branding}:`;
91
+ }
92
+ constructor(branding) {
93
+ if (typeof branding === "string") {
94
+ this.#branding = branding;
95
+ }
96
+ else {
97
+ this.#branding = branding.constructor?.name;
98
+ }
99
+ }
100
+ error(...args) {
101
+ if (logLevels.error <= logLevels[Logger.#level]) {
102
+ console.error(this.brand, ...args);
103
+ }
104
+ }
105
+ warn(...args) {
106
+ if (logLevels.warn <= logLevels[Logger.#level]) {
107
+ console.warn(this.brand, ...args);
108
+ }
109
+ }
110
+ info(...args) {
111
+ if (logLevels.info <= logLevels[Logger.#level]) {
112
+ console.info(this.brand, ...args);
113
+ }
114
+ }
115
+ debug(...args) {
116
+ if (logLevels.debug <= logLevels[Logger.#level]) {
117
+ console.debug(this.brand, ...args);
118
+ }
119
+ }
120
+ trace(...args) {
121
+ if (logLevels.trace <= logLevels[Logger.#level]) {
122
+ console.trace(this.brand, ...args);
123
+ }
124
+ }
125
+ }
126
+
127
+ /**
128
+ * Error class for Lightning Out
129
+ */
130
+ const logger$4 = new Logger("LightningOutError");
131
+ class LightningOutError {
132
+ #eventTarget;
133
+ #branding;
134
+ constructor(branding) {
135
+ if (typeof branding === "string") {
136
+ this.#branding = branding;
137
+ }
138
+ else {
139
+ this.#branding = branding.constructor?.name;
140
+ }
141
+ if (typeof branding.dispatchEvent === "function") {
142
+ this.#eventTarget = branding;
143
+ }
144
+ }
145
+ #branded(message) {
146
+ return `${this.#branding}: ${message}`;
147
+ }
148
+ create(error) {
149
+ const message = typeof error === "string" ? error : error.message;
150
+ return new Error(this.#branded(message));
151
+ }
152
+ dispatch(type, error) {
153
+ const message = typeof error === "string" ? error : error.message || error.detail?.message;
154
+ if (this.#eventTarget) {
155
+ const detail = error.detail || {
156
+ message: this.#branded(message),
157
+ originalError: error,
158
+ };
159
+ const loErrorEvent = new CustomEvent(type, { detail });
160
+ this.#eventTarget.dispatchEvent(loErrorEvent);
161
+ logger$4.error(`${this.#branded("dispatched error")} -> ${type}: ${message}`);
162
+ }
163
+ else {
164
+ logger$4.error(`${this.#branded("unable to dispatch error on a non-EventTarget object")} -> ${type}: ${message}`);
165
+ }
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Utility functions for Lightning Out
171
+ * @fileoverview Collection of utility functions used throughout the Lightning Out framework
172
+ * @author Salesforce ECS Team
173
+ * @version 2.1.0
174
+ */
175
+ const loError = new LightningOutError("LightningOutUtils");
176
+ /**
177
+ * Converts HTML attribute/element names from kebab-case format to JavaScript property/component names in camelCase
178
+ * format.
179
+ *
180
+ * @param attrName - The kebab-case string to convert (should be all lowercase)
181
+ * @returns The camelCase string
182
+ *
183
+ * @example
184
+ * kebabToCamel('data-attribute-name'); // 'dataAttributeName'
185
+ */
186
+ function kebabToCamel(attrName) {
187
+ // Validate the input is all lowercase
188
+ if (/[A-Z]/.test(attrName)) {
189
+ throw loError.create(`kebabToCamel: "${attrName}" is not valid kebab-case - must be all lowercase.`);
190
+ }
191
+ // Standard kebab-case to camelCase conversion
192
+ return attrName.replace(/-([a-z])/g, (_, char) => char.toUpperCase());
193
+ }
194
+ /**
195
+ * Converts JavaScript property/component names from camelCase format to HTML attribute/element names in kebab-case
196
+ * format. This function performs pure transformation without validation.
197
+ *
198
+ * @param propName - The camelCase string to convert
199
+ * @returns The kebab-case string
200
+ *
201
+ * @example
202
+ * camelToKebab('dataAttributeName'); // 'data-attribute-name'
203
+ */
204
+ function camelToKebab(propName) {
205
+ // Standard camelCase to kebab-case conversion
206
+ return propName.replace(/([A-Z])/g, "-$1").toLowerCase();
207
+ }
208
+ /**
209
+ * Converts JavaScript property names from camelCase format to snake_case format. Preserves existing underscores by
210
+ * escaping them as double underscores.
211
+ *
212
+ * @param propName - The camelCase string to convert
213
+ * @returns The snake_case string
214
+ *
215
+ * @example
216
+ * camelToSnake('cwcMessaging'); // 'cwc_messaging'
217
+ * camelToSnake('my_variableName'); // 'my__variable_name'
218
+ */
219
+ function camelToSnake(propName) {
220
+ return propName.replace(/([A-Z_])/g, "_$1").toLowerCase();
221
+ }
222
+ /**
223
+ * Converts snake_case strings to camelCase format. Handles escaped double underscores by converting them back to single
224
+ * underscores.
225
+ *
226
+ * @param snakeName - The snake_case string to convert (should be all lowercase)
227
+ * @returns The camelCase string
228
+ *
229
+ * @example
230
+ * snakeToCamel('cwc_messaging'); // 'cwcMessaging'
231
+ * snakeToCamel('my__variable_name'); // 'my_variableName'
232
+ */
233
+ function snakeToCamel(snakeName) {
234
+ if (/[A-Z]/.test(snakeName)) {
235
+ throw loError.create(`snakeToCamel: "${snakeName}" is not valid snake_case - must be all lowercase.`);
236
+ }
237
+ return snakeName.replace(/_([a-z_])/g, (_, char) => char.toUpperCase());
238
+ }
239
+ /**
240
+ * Transforms custom element names in kebab-name format (i.e. foo-bar) into standard Salesforce Lightning Web component
241
+ * names (i.e. foo/bar) for internal component resolution.
242
+ *
243
+ * @param elementName - The custom element tag name (must contain a hyphen and be all lowercase)
244
+ * @param isAura - Whether this is an Aura component
245
+ * @returns Lightning component name in namespace/componentName format (LWC) or namespace:componentName format (Aura)
246
+ * @throws LightningOutError if the element name input doesn't contain a hyphen or contains uppercase letters
247
+ *
248
+ * @example
249
+ * elementNameToStandardName('c-my-component'); // 'c/myComponent'
250
+ * elementNameToStandardName('c-my-component', true); // 'c:myComponent'
251
+ */
252
+ function elementNameToStandardName(elementName, isAura = false) {
253
+ // Validate the element name input is all lowercase
254
+ if (/[A-Z]/.test(elementName)) {
255
+ throw loError.create(`elementNameToStandardName: "${elementName}" is not a valid custom element name - must be all lowercase.`);
256
+ }
257
+ const nsSepIdx = elementName.indexOf("-");
258
+ if (nsSepIdx === -1) {
259
+ throw loError.create(`elementNameToStandardName: "${elementName}" is not a valid custom element name - missing hyphen character.`);
260
+ }
261
+ // Namespace part (before first hyphen) may contain underscores - convert to camelCase
262
+ const namespace = snakeToCamel(elementName.slice(0, nsSepIdx));
263
+ const name = kebabToCamel(elementName.slice(nsSepIdx + 1));
264
+ // Use colon separator for Aura, slash for LWC
265
+ const separator = isAura ? ":" : "/";
266
+ return `${namespace}${separator}${name}`;
267
+ }
268
+ /**
269
+ * Transforms standard Salesforce Lightning component names (i.e. foo/bar for LWC, foo:bar for Aura) into custom element
270
+ * names (i.e. foo-bar) for HTML usage.
271
+ *
272
+ * @param standardName - The Lightning component name
273
+ * @returns Custom element tag name in kebab-case format
274
+ * @throws LightningOutError if standard name doesn't contain a separator (/ or :), or if namespace contains hyphens
275
+ *
276
+ * @example
277
+ * standardNameToElementName('c/myComponent'); // 'c-my-component'
278
+ * standardNameToElementName('c:myComponent'); // 'c-my-component'
279
+ */
280
+ function standardNameToElementName(standardName) {
281
+ // Detect namespace separator - slash for LWC, colon for Aura
282
+ const separator = standardName.includes("/") ? "/" : ":";
283
+ const sepIdx = standardName.indexOf(separator);
284
+ if (sepIdx === -1) {
285
+ throw loError.create(`standardNameToElementName: "${standardName}" is not a valid component name - missing namespace separator.`);
286
+ }
287
+ // Validate entire component name input must not contain hyphens
288
+ if (/-/.test(standardName)) {
289
+ throw loError.create(`standardNameToElementName: "${standardName}" is not a valid component name - must not contain hyphens.`);
290
+ }
291
+ // Validate first character is not uppercase
292
+ if (/^[A-Z]/.test(standardName)) {
293
+ throw loError.create(`standardNameToElementName: "${standardName}" is not a valid component name - first character must not be uppercase.`);
294
+ }
295
+ // Separate the namespace and component name inputs
296
+ const namespaceInput = standardName.slice(0, sepIdx);
297
+ const nameInput = standardName.slice(sepIdx + 1);
298
+ // Convert namespace input from camelCase to snake_case
299
+ const namespace = camelToSnake(namespaceInput);
300
+ // Convert component name input from camelCase to kebab-case
301
+ const name = camelToKebab(nameInput);
302
+ return `${namespace}-${name}`;
303
+ }
304
+ /**
305
+ * Checks if a name is in standard Lightning component format (contains '/' for LWC or ':' for Aura).
306
+ *
307
+ * @param name - The name to check
308
+ * @returns True if the name contains a forward slash or colon, false otherwise
309
+ *
310
+ * @example
311
+ * isStandardName('c/myComponent'); // true
312
+ * isStandardName('c:myComponent'); // true
313
+ * isStandardName('my-component'); // false
314
+ */
315
+ function isStandardName(name) {
316
+ return name.includes("/") || name.includes(":");
317
+ }
318
+ /**
319
+ * Applies property transformations and validation callbacks to component
320
+ * property changes before they are sent to Lightning components.
321
+ * Handles 'dataMirror' prefixes and invokes element-specific property callbacks.
322
+ *
323
+ * @param element - Element that may contain property change callbacks
324
+ * @param changes - Property changes to process
325
+ * @returns Processed property changes
326
+ *
327
+ * @example
328
+ * processPropertyChanges(element, { dataMirrorHeight: '100px' }); // { height: '100px' }
329
+ */
330
+ function processPropertyChanges(element, changes) {
331
+ const processed = Object.entries(changes).map((entry) => {
332
+ // Extract the key name (already in camelCase) and it's value
333
+ let [key, value] = entry;
334
+ // Check if the key name has the special 'dataMirror' prefix, and if it does, use the suffix
335
+ // as the actual key name, i.e. 'dataMirrorHeight' becomes 'height'
336
+ const mirror = key.split("dataMirror");
337
+ if (mirror.length === 2 && mirror[0] === "") {
338
+ key = mirror[1].charAt(0).toLowerCase() + mirror[1].slice(1);
339
+ }
340
+ // Check if there is a _propertyChanged_ callback for the key, and if there is, call it to
341
+ // transform the value
342
+ const callbackName = `_propertyChanged_${key}`;
343
+ if (typeof element[callbackName] === "function") {
344
+ const callback = element[callbackName];
345
+ value = callback(value);
346
+ }
347
+ // Return the (possibly) transformed key name and value
348
+ return [key, value];
349
+ });
350
+ // This will remove duplicate key names from the object, if any, the last one prevails.
351
+ return Object.fromEntries(processed);
352
+ }
353
+ /**
354
+ * Parses and structures URL information for debugging and logging, with
355
+ * specialized parsing for Salesforce authentication frontdoor URLs.
356
+ *
357
+ * @param url - URL object to analyze
358
+ * @returns Structured URL data with parsed parameters
359
+ *
360
+ * @example
361
+ * getUrlData(new URL('https://example.com/path?param=value'));
362
+ */
363
+ function getUrlData(url) {
364
+ // This function is useful for debugging URLs, it understands frontdoor.jsp URLs
365
+ const urlData = {};
366
+ const paramsData = (params) => {
367
+ const paramsData = {};
368
+ for (const [key, value] of params.entries()) {
369
+ paramsData[key] = value;
370
+ }
371
+ return paramsData;
372
+ };
373
+ urlData.url = url.origin + url.pathname;
374
+ urlData.urlParams = paramsData(url.searchParams);
375
+ if (url.pathname === "/secur/frontdoor.jsp") {
376
+ const paramName = urlData.urlParams["otp"] ? "startURL" : "retURL";
377
+ const startURL = new URL(urlData.urlParams[paramName], "http://dummy.com");
378
+ urlData.urlParams[paramName] = {
379
+ url: startURL.pathname,
380
+ urlParams: paramsData(startURL.searchParams),
381
+ };
382
+ }
383
+ return urlData;
384
+ }
385
+
386
+ /**
387
+ * IFrame manager for Lightning Out
388
+ */
389
+ const logger$3 = new Logger("LightningOutIFrame");
390
+ class LightningOutIFrame {
391
+ #parentElement;
392
+ #isVisible;
393
+ #parentError;
394
+ #hiddenStyle = "display:none";
395
+ #visibleStyle = "border:0px; width:100%; height:100%; overflow:auto;";
396
+ #shadowRoot;
397
+ #element;
398
+ #window;
399
+ #origin;
400
+ #endpoint;
401
+ #timeoutID;
402
+ constructor(iframeConfig) {
403
+ this.#parentElement = iframeConfig.parentElement;
404
+ this.#isVisible = iframeConfig.isVisible;
405
+ // Dispatch errors to the iframe's parentElement
406
+ this.#parentError = new LightningOutError(iframeConfig.parentElement);
407
+ }
408
+ get iframeReady() {
409
+ return !!this.#window && !!this.#origin;
410
+ }
411
+ get iframeElement() {
412
+ return this.#element;
413
+ }
414
+ #config(window, origin) {
415
+ this.#window = window;
416
+ this.#origin = origin;
417
+ }
418
+ #messageListener = (event) => {
419
+ if (event.data.id !== this.#parentElement._uuid) {
420
+ // Ignore messages that are not meant to be received by the parent Custom Element
421
+ return;
422
+ }
423
+ logger$3.debug("#messageListener:", `parentElement._uuid: ${this.#parentElement._uuid}`, `parentElement.localName: ${this.#parentElement.localName}`, JSON.stringify(event.data));
424
+ switch (event.data.type) {
425
+ case messages.lo.loaded: {
426
+ // Clear the timeout, cache its window and origin
427
+ this.#timeoutID = clearTimeout(this.#timeoutID);
428
+ this.#config(event.source, event.origin);
429
+ // Notify the parentElement that the iframe has successfully loaded. The origin where the iframe landed
430
+ // might be different than the origin used to load the frontdoor url, so notify that back.
431
+ this.#parentElement.dispatchEvent(new CustomEvent(events.lo.iframe.load, {
432
+ detail: event.origin,
433
+ }));
434
+ break;
435
+ }
436
+ case messages.lo.logout: {
437
+ // Clear the timeout
438
+ this.#timeoutID = clearTimeout(this.#timeoutID);
439
+ // Notify the parentElement that the iframe loaded the logout page
440
+ this.#parentElement.dispatchEvent(new CustomEvent(events.lo.iframe.logout));
441
+ break;
442
+ }
443
+ case messages.lo.redirect: {
444
+ // Clear the timeout
445
+ this.#timeoutID = clearTimeout(this.#timeoutID);
446
+ // Notify the parentElement that the iframe loaded with a redirection
447
+ this.#parentElement.dispatchEvent(new CustomEvent(events.lo.iframe.auth.redirect, {
448
+ detail: {
449
+ redirectUrl: event.data.redirectUrl,
450
+ redirectOrigin: event.origin,
451
+ },
452
+ }));
453
+ break;
454
+ }
455
+ }
456
+ };
457
+ #init() {
458
+ if (!this.#element) {
459
+ const iframe = window.document.createElement("iframe");
460
+ iframe.name = "lightning_af";
461
+ // allow-top-navigation should only be enabled for UMA External Forms as Forms are first party and trusted.
462
+ // No custom code is enabled in External Forms scenario. When this assumption changed, this code needs to
463
+ // be reviewed and updated.
464
+ iframe.setAttribute("sandbox", [
465
+ "allow-downloads",
466
+ "allow-forms",
467
+ "allow-popups",
468
+ "allow-same-origin",
469
+ "allow-scripts",
470
+ "allow-top-navigation-by-user-activation",
471
+ ].join(" "));
472
+ iframe.style.cssText = this.#isVisible ? this.#visibleStyle : this.#hiddenStyle;
473
+ this.#element = iframe;
474
+ this.#shadowRoot = this.#parentElement.attachShadow({
475
+ mode: "closed",
476
+ });
477
+ this.#shadowRoot.appendChild(this.#element);
478
+ // Note: In WebKit browsers (Safari/Chrome), the iframe's `onload` event
479
+ // will be triggered twice if the `onload` handler is attached **before**
480
+ // the iframe is appended to the DOM.
481
+ iframe.addEventListener("load", this.#loaded);
482
+ window.addEventListener("message", this.#messageListener);
483
+ }
484
+ return this.#element;
485
+ }
486
+ load(endpoint) {
487
+ const iframe = this.#init();
488
+ this.#endpoint = new URL(endpoint);
489
+ logger$3.debug(`#loadIframe: endpoint =`, getUrlData(this.#endpoint));
490
+ this.#config(undefined, undefined);
491
+ if (this.#isVisible) {
492
+ iframe.src = endpoint;
493
+ }
494
+ else {
495
+ // This is to help us debug issues with hidden iframes. The target page should call postMessage like this:
496
+ // (window.opener ?? window.parent).postMessage();
497
+ if (localStorage.getItem("LightningOutIFrame:load:window.open")) {
498
+ window.open(endpoint, `LO2 Hidden ${this.#parentElement._uuid}`, "left=200,top=200,width=800,height=800");
499
+ }
500
+ else {
501
+ iframe.src = endpoint;
502
+ }
503
+ }
504
+ }
505
+ #loaded = () => {
506
+ // Browsers will call the onload event even when there is an error, so if the iframe
507
+ // is not ready when the timeout expires, there was an error but we don't know
508
+ // exactly what.
509
+ const timeout = 60 * 1000; // It should take less than this to receive the "lo.loaded" message
510
+ if (this.#timeoutID) {
511
+ clearTimeout(this.#timeoutID);
512
+ }
513
+ this.#timeoutID = setTimeout(() => {
514
+ if (!this.iframeReady) {
515
+ const message = "Error: Unknown error, unable to load the iframe.";
516
+ this.#parentError.dispatch(events.lo.iframe.error, message);
517
+ this.#showErrorMessage(message);
518
+ }
519
+ }, timeout);
520
+ };
521
+ destroy() {
522
+ if (this.#shadowRoot) {
523
+ this.#shadowRoot.innerHTML = "";
524
+ }
525
+ if (this.#element) {
526
+ this.#element.remove();
527
+ }
528
+ this.#shadowRoot = undefined;
529
+ this.#element = undefined;
530
+ this.#config(undefined, undefined);
531
+ }
532
+ #showErrorMessage(message) {
533
+ if (this.#isVisible && this.#endpoint) {
534
+ const url = new URL("/lightning/lightning.out.message.html", this.#endpoint.origin);
535
+ url.search = new URLSearchParams({
536
+ loAppOrigin: window.location.origin,
537
+ parentElementId: this.#parentElement._uuid,
538
+ message: message,
539
+ }).toString();
540
+ this.load(url.href);
541
+ }
542
+ }
543
+ postMessage(message) {
544
+ if (this.#window && this.#origin) {
545
+ logger$3.debug("postMessage:", `parentElement: ${this.#parentElement._uuid}`, JSON.stringify(message));
546
+ try {
547
+ this.#window.postMessage(message, this.#origin);
548
+ }
549
+ catch (err) {
550
+ const message = `postMessage error: ${err}`;
551
+ this.#parentError.dispatch(events.lo.iframe.error, message);
552
+ throw this.#parentError.create(message);
553
+ }
554
+ }
555
+ else {
556
+ throw this.#parentError.create("Error attempting to postMessage on an iframe that is not ready.");
557
+ }
558
+ }
559
+ }
560
+
561
+ /**
562
+ * @file property-observer.ts
563
+ * @author Caridy Patiño (2025)
564
+ * @license MIT
565
+ * @description Provides the PropertyObserver class, a utility to observe property and attribute
566
+ * changes on any DOM element, with automatic getter/setter interception and batched notifications.
567
+ */
568
+ const logger$2 = new Logger("PropertyObserver");
569
+ /**
570
+ * The PropertyObserver class allows developers to monitor property and attribute changes
571
+ * on any DOM element. It focuses on non-standard properties and attributes, automatically
572
+ * installing getter/setter interceptors for property changes and using MutationObserver
573
+ * for attribute changes.
574
+ *
575
+ * Key features:
576
+ * 1. Observes both element properties (el.foo = 1) and HTML attributes
577
+ * 2. Focuses on non-standard attributes/properties only
578
+ * 3. Converts between attribute names (kebab-case) and property names (camelCase)
579
+ * 4. Installs getter/setter interceptors for real-time property change detection
580
+ * 5. Uses MutationObserver for attribute change detection
581
+ * 6. Batches multiple changes using microtasks (except initial synchronous call)
582
+ *
583
+ * @example
584
+ * ```typescript
585
+ * const myElement = document.getElementById('my-element');
586
+ *
587
+ * function handleChanges(changes: Record<string, any>) {
588
+ * logger.log('Properties/attributes changed:', changes);
589
+ * // Example: changes might be { myProp: 'newValue', anotherProp: 42 }
590
+ * }
591
+ *
592
+ * const observer = new PropertyObserver(myElement!, handleChanges);
593
+ *
594
+ * // Later...
595
+ * myElement!.myProp = 'test'; // Will trigger callback
596
+ * myElement!.setAttribute('my-prop', 'test2'); // Will also trigger callback
597
+ *
598
+ * // To stop observing:
599
+ * observer.disconnect();
600
+ * ```
601
+ */
602
+ class PropertyObserver {
603
+ /**
604
+ * The HTML element whose properties and attributes are being observed.
605
+ */
606
+ _el;
607
+ /**
608
+ * The callback function to invoke when properties/attributes change.
609
+ */
610
+ _cb;
611
+ /**
612
+ * A cache of the last known values of observed properties.
613
+ * Keys are property names (camelCase), values are their current values.
614
+ */
615
+ _cache;
616
+ /**
617
+ * The callback to determine if a property should be observed.
618
+ */
619
+ _shouldObserve;
620
+ /**
621
+ * Set of property names that we've installed getter/setter interceptors for.
622
+ */
623
+ _interceptedProps;
624
+ /**
625
+ * Map of original property descriptors that we've replaced.
626
+ */
627
+ _originalDescriptors;
628
+ /**
629
+ * The MutationObserver instance watching for attribute changes.
630
+ */
631
+ _observer;
632
+ /**
633
+ * Flag to track if we have a pending microtask for batched changes.
634
+ */
635
+ _changesPending;
636
+ /**
637
+ * Accumulated changes waiting to be reported in the next microtask.
638
+ */
639
+ _pendingChanges;
640
+ /**
641
+ * Map of attribute names that don't follow standard kebab-case to camelCase conversion.
642
+ * Maps attribute name -> property name for special cases.
643
+ */
644
+ _attributeExceptions = new Map([
645
+ ["for", "htmlFor"],
646
+ ["class", "className"],
647
+ ["formnovalidate", "formNoValidate"],
648
+ ["readonly", "readOnly"],
649
+ ["maxlength", "maxLength"],
650
+ ["minlength", "minLength"],
651
+ ["contenteditable", "contentEditable"],
652
+ ["spellcheck", "spellcheck"], // This one actually matches, but explicitly included for clarity
653
+ ["novalidate", "noValidate"],
654
+ ["autofocus", "autofocus"], // This one actually matches too
655
+ ["autocomplete", "autocomplete"], // And this one
656
+ ["crossorigin", "crossOrigin"],
657
+ ]);
658
+ /**
659
+ * Creates an instance of PropertyObserver.
660
+ *
661
+ * @param target The DOM element whose properties and attributes you want to observe.
662
+ * @param callback
663
+ * The function to be invoked when one or more properties/attributes change.
664
+ * This function receives a single argument: an object where keys are property names
665
+ * (in camelCase) and values are their new values.
666
+ * This callback is called synchronously once upon initialization with all initial values,
667
+ * then asynchronously (batched via microtask) for subsequent changes.
668
+ * @throws {TypeError} If `target` is not a DOM Element or `callback` is not a function.
669
+ */
670
+ constructor(target, callback, shouldObserve) {
671
+ if (!(target instanceof Element)) {
672
+ throw new TypeError("Target must be a DOM Element");
673
+ }
674
+ if (typeof callback !== "function") {
675
+ throw new TypeError("Callback must be a function");
676
+ }
677
+ if (shouldObserve && typeof shouldObserve !== "function") {
678
+ throw new TypeError("shouldObserve callback must be a function");
679
+ }
680
+ const shouldObserveDefault = (propName, isStandard) => {
681
+ // By default, observe only non-standard (i.e. custom) properties and attributes.
682
+ return isStandard === false;
683
+ };
684
+ this._el = target;
685
+ this._cb = callback;
686
+ this._cache = new Map();
687
+ this._shouldObserve = shouldObserve || shouldObserveDefault;
688
+ this._interceptedProps = new Set();
689
+ this._originalDescriptors = new Map();
690
+ this._changesPending = false;
691
+ this._pendingChanges = {};
692
+ // Perform initial scan and setup
693
+ this._initialScan();
694
+ this._setupMutationObserver();
695
+ }
696
+ /**
697
+ * Disconnects the observer and restores original property descriptors.
698
+ * This stops observing changes and cleans up any modifications made to the element.
699
+ */
700
+ disconnect() {
701
+ // Disconnect mutation observer
702
+ if (this._observer) {
703
+ this._observer.disconnect();
704
+ }
705
+ // Restore original property descriptors
706
+ for (const [propName, descriptor] of this._originalDescriptors) {
707
+ Object.defineProperty(this._el, propName, descriptor);
708
+ }
709
+ // Clear all internal state
710
+ this._cache.clear();
711
+ this._interceptedProps.clear();
712
+ this._originalDescriptors.clear();
713
+ this._pendingChanges = {};
714
+ this._changesPending = false;
715
+ }
716
+ /**
717
+ * Performs the initial scan of the element to detect non-standard attributes
718
+ * and properties, then calls the callback synchronously with initial values.
719
+ */
720
+ _initialScan() {
721
+ const initialValues = {};
722
+ // Scan attributes
723
+ for (const attr of Array.from(this._el.attributes)) {
724
+ const attrName = attr.name;
725
+ // Check if the attribute itself is standard
726
+ const isStandard = this._isStandardAttribute(attrName);
727
+ // Skip the attribute if the user says it should not be observed
728
+ if (!this._shouldObserve(attrName, isStandard)) {
729
+ continue;
730
+ }
731
+ const propName = this._attributeNameToPropName(attrName);
732
+ const value = attr.value;
733
+ initialValues[propName] = value;
734
+ this._cache.set(propName, value);
735
+ // Install property interceptor
736
+ this._installPropertyInterceptor(propName);
737
+ }
738
+ // Also scan for existing custom properties on the element
739
+ for (const propName of Object.getOwnPropertyNames(this._el)) {
740
+ // Check if the property itself is standard
741
+ const isStandard = this._isStandardProperty(propName);
742
+ // Skip the property if it is already processed as an attribute (check this first), or
743
+ // if it the user says it should not be observed.
744
+ if (this._cache.has(propName) || !this._shouldObserve(propName, isStandard)) {
745
+ continue;
746
+ }
747
+ // If it's an own property, it's likely custom and worth observing
748
+ const value = this._el[propName];
749
+ initialValues[propName] = value;
750
+ this._cache.set(propName, value);
751
+ // Install property interceptor
752
+ this._installPropertyInterceptor(propName);
753
+ }
754
+ // Call callback synchronously with initial values
755
+ if (Object.keys(initialValues).length > 0) {
756
+ try {
757
+ this._cb(initialValues);
758
+ }
759
+ catch (e) {
760
+ logger$2.error("Error in initial PropertyObserver callback:", e);
761
+ }
762
+ }
763
+ }
764
+ /**
765
+ * Sets up the MutationObserver to watch for attribute changes.
766
+ */
767
+ _setupMutationObserver() {
768
+ this._observer = new MutationObserver((mutations) => {
769
+ const changes = {};
770
+ for (const mutation of mutations) {
771
+ if (mutation.type === "attributes" && mutation.attributeName) {
772
+ const attrName = mutation.attributeName;
773
+ // Check if the attribute itself is standard
774
+ const isStandard = this._isStandardAttribute(attrName);
775
+ // Skip the attribute if the user says it should not be observed
776
+ if (!this._shouldObserve(attrName, isStandard)) {
777
+ continue;
778
+ }
779
+ const propName = this._attributeNameToPropName(attrName);
780
+ const newValue = this._el.getAttribute(attrName);
781
+ const cachedValue = this._cache.get(propName);
782
+ if (newValue !== cachedValue) {
783
+ changes[propName] = newValue;
784
+ this._cache.set(propName, newValue);
785
+ // Install interceptor for new properties
786
+ if (!this._interceptedProps.has(propName)) {
787
+ this._installPropertyInterceptor(propName);
788
+ }
789
+ }
790
+ }
791
+ }
792
+ if (Object.keys(changes).length > 0) {
793
+ this._batchChanges(changes);
794
+ }
795
+ });
796
+ this._observer.observe(this._el, {
797
+ attributes: true,
798
+ attributeOldValue: false, // We track old values ourselves
799
+ });
800
+ }
801
+ /**
802
+ * Installs a getter/setter interceptor for a property to detect changes.
803
+ * @param propName The property name to intercept
804
+ */
805
+ _installPropertyInterceptor(propName) {
806
+ if (this._interceptedProps.has(propName)) {
807
+ return; // Already intercepted
808
+ }
809
+ // Get current descriptor
810
+ const currentDescriptor = Object.getOwnPropertyDescriptor(this._el, propName) || {
811
+ value: this._el[propName],
812
+ writable: true,
813
+ enumerable: true,
814
+ configurable: true,
815
+ };
816
+ // Store original descriptor
817
+ this._originalDescriptors.set(propName, currentDescriptor);
818
+ // Create new descriptor with getter/setter
819
+ const newDescriptor = {
820
+ enumerable: currentDescriptor.enumerable,
821
+ configurable: currentDescriptor.configurable,
822
+ get: currentDescriptor.get || (() => currentDescriptor.value),
823
+ set: (newValue) => {
824
+ const oldValue = this._cache.get(propName);
825
+ if (newValue !== oldValue) {
826
+ // Update the actual property
827
+ if (currentDescriptor.set) {
828
+ currentDescriptor.set.call(this._el, newValue);
829
+ }
830
+ else {
831
+ currentDescriptor.value = newValue;
832
+ }
833
+ // Update cache and batch the change
834
+ this._cache.set(propName, newValue);
835
+ this._batchChanges({ [propName]: newValue });
836
+ }
837
+ },
838
+ };
839
+ // Install the new descriptor
840
+ Object.defineProperty(this._el, propName, newDescriptor);
841
+ this._interceptedProps.add(propName);
842
+ }
843
+ /**
844
+ * Batches changes to be reported in the next microtask.
845
+ * @param changes Changes to batch
846
+ */
847
+ _batchChanges(changes) {
848
+ // Merge with pending changes
849
+ Object.assign(this._pendingChanges, changes);
850
+ if (!this._changesPending) {
851
+ this._changesPending = true;
852
+ queueMicrotask(() => {
853
+ this._changesPending = false;
854
+ const changesToReport = { ...this._pendingChanges };
855
+ this._pendingChanges = {};
856
+ try {
857
+ this._cb(changesToReport);
858
+ }
859
+ catch (e) {
860
+ logger$2.error("Error in PropertyObserver callback:", e);
861
+ }
862
+ });
863
+ }
864
+ }
865
+ /**
866
+ * Converts an attribute name (kebab-case) to a property name (camelCase).
867
+ * @param attrName The attribute name
868
+ * @returns The property name
869
+ */
870
+ _attributeNameToPropName(attrName) {
871
+ // Check for special cases that don't follow standard conversion
872
+ if (this._attributeExceptions.has(attrName)) {
873
+ return this._attributeExceptions.get(attrName);
874
+ }
875
+ // Standard kebab-case to camelCase conversion
876
+ return attrName.replace(/-([a-z])/g, (match, letter) => letter.toUpperCase());
877
+ }
878
+ /**
879
+ * Checks if an attribute name is a standard HTML attribute that should be ignored.
880
+ * @param attrName The attribute name (already lowercase per HTML spec)
881
+ * @returns True if it's a standard attribute
882
+ */
883
+ _isStandardAttribute(attrName) {
884
+ // Skip known prefixes that are always standard
885
+ if (attrName.startsWith("data-") || attrName.startsWith("aria-") || attrName.startsWith("on")) {
886
+ return true;
887
+ }
888
+ // Convert attribute name to property name and check if it exists in prototypes
889
+ const propName = this._attributeNameToPropName(attrName);
890
+ return this._isStandardProperty(propName);
891
+ }
892
+ /**
893
+ * Checks if a property name corresponds to a standard property that should be ignored.
894
+ * @param propName The property name
895
+ * @returns True if it's a standard property
896
+ */
897
+ _isStandardProperty(propName) {
898
+ // Check if the property exists in the standard prototype chain
899
+ // HTMLElement.prototype inherits from Element → Node → EventTarget
900
+ return propName in HTMLElement.prototype;
901
+ }
902
+ }
903
+
904
+ /**
905
+ * A registry for managing the relationship between parent `App` instances
906
+ * and child `Comp` instances.
907
+ */
908
+ class Registry {
909
+ #loError = new LightningOutError("LightningOutRegistry");
910
+ appToComps = new WeakMap();
911
+ compToApp = new WeakMap();
912
+ compNameToApp = new Map();
913
+ /**
914
+ * Registers a new App instance in the registry.
915
+ * @param app The App instance to register.
916
+ * registered app.
917
+ */
918
+ registerApplication(app) {
919
+ if (!this.appToComps.has(app)) {
920
+ this.appToComps.set(app, new Set());
921
+ }
922
+ }
923
+ /**
924
+ * Registers a component name which is owned by a specific App
925
+ * @param name The component name
926
+ * @param app The app that owns the name
927
+ * @throws {LightningOutError} if the name is already registered
928
+ */
929
+ registerComponentName(name, app) {
930
+ if (this.compNameToApp.has(name)) {
931
+ throw this.#loError.create(`"${name}" is already registered to another App.`);
932
+ }
933
+ this.compNameToApp.set(name, app);
934
+ }
935
+ /**
936
+ * Registers a Comp instance and associates it with a parent App.
937
+ * @param comp The Comp instance to register.
938
+ * @param app The parent App instance. (Optional)
939
+ * @returns The parent App
940
+ * @throws {LightningOutError} If the Comp is already registered or if no parent App is found.
941
+ */
942
+ registerComponent(comp, app) {
943
+ if (this.compToApp.has(comp)) {
944
+ throw this.#loError.create("This Comp is already registered to another App.");
945
+ }
946
+ let parentApp = app;
947
+ // If no app was provided, find it by component name
948
+ if (!parentApp) {
949
+ const compName = comp.localName; // inherited from Element
950
+ parentApp = this.compNameToApp.get(compName);
951
+ if (!parentApp) {
952
+ throw this.#loError.create(`Could not find a parent App for component "${comp.localName}"`);
953
+ }
954
+ }
955
+ // Associate the component with its parent app
956
+ this.appToComps.get(parentApp).add(comp);
957
+ this.compToApp.set(comp, parentApp);
958
+ return parentApp;
959
+ }
960
+ /**
961
+ * Unregister a Comp instance from the registry.
962
+ * @param comp The Comp instance to unregister.
963
+ * @returns `true` if the component was found and unregistered, otherwise `false`.
964
+ */
965
+ unregisterComponent(comp) {
966
+ const app = this.compToApp.get(comp);
967
+ if (!app) {
968
+ return false;
969
+ }
970
+ const comps = this.appToComps.get(app);
971
+ comps?.delete(comp);
972
+ this.compToApp.delete(comp);
973
+ return true;
974
+ }
975
+ /**
976
+ * Retrieves the set of Comps associated with a given App.
977
+ * @param app The App instance for which to retrieve components.
978
+ * @returns A Set of Comp instances associated with the given App.
979
+ * @throws {LightningOutError} If the set of components for the given app cannot be found.
980
+ */
981
+ getComps(app) {
982
+ const comps = this.appToComps.get(app);
983
+ if (!comps) {
984
+ throw this.#loError.create("Unable to find set of LightningOutComponents");
985
+ }
986
+ return comps;
987
+ }
988
+ }
989
+ /**
990
+ * Singleton instance of the Registry class.
991
+ */
992
+ const registry = new Registry();
993
+
994
+ /**
995
+ * LightningOutComponent class for Lightning Out
996
+ */
997
+ const logger$1 = new Logger("LightningOutComponent");
998
+ // These attributes are mirrored into the iframe
999
+ const MIRROR = new Set([
1000
+ "autocapitalize",
1001
+ "autocorrect",
1002
+ "dir",
1003
+ "enterkeyhint",
1004
+ "inputmode",
1005
+ "lang",
1006
+ "spellcheck",
1007
+ "style", // this is mirrored but only the css variables
1008
+ "title",
1009
+ "translate",
1010
+ ]);
1011
+ // These aria attributes are mirrored into the iframe (specially for extensions of lightning-input)
1012
+ const ARIA_MIRROR = new Set([
1013
+ "aria-disabled",
1014
+ "aria-hidden",
1015
+ "aria-label",
1016
+ "aria-live",
1017
+ "aria-modal",
1018
+ "aria-pressed",
1019
+ "aria-valuemax",
1020
+ "aria-valuemin",
1021
+ "aria-valuenow",
1022
+ ]);
1023
+ // These attributes are not mirrored into the iframe
1024
+ const BLOCK = new Set([
1025
+ "accesskey",
1026
+ "autofocus",
1027
+ "draggable",
1028
+ "exportparts",
1029
+ "hidden",
1030
+ "inert",
1031
+ "nonce", // block for security reasons!
1032
+ "part",
1033
+ "slot",
1034
+ "tabindex",
1035
+ ]);
1036
+ // These aria attributes are not mirrored into the iframe (they are ID refs)
1037
+ const ARIA_BLOCK = new Set([
1038
+ "aria-activedescendant",
1039
+ "aria-controls",
1040
+ "aria-describedby",
1041
+ "aria-details",
1042
+ "aria-errormessage",
1043
+ "aria-flowto",
1044
+ "aria-labelledby",
1045
+ "aria-owns",
1046
+ ]);
1047
+ class LightningOutComponent extends HTMLElement {
1048
+ _uuid = getUUID();
1049
+ _ready = false;
1050
+ _standardName = elementNameToStandardName(this.localName); // May change during registration
1051
+ #parentApp; // The parent LightningOutApplication
1052
+ #loError = new LightningOutError(this);
1053
+ #loIFrame = new LightningOutIFrame({
1054
+ parentElement: this,
1055
+ isVisible: true,
1056
+ });
1057
+ #propObserver;
1058
+ #initialRender = true;
1059
+ #eventQueue = [];
1060
+ #listenerKeyMap = new WeakMap();
1061
+ // Seed for generating unique listener keys.
1062
+ #listenerKeySeed = 0;
1063
+ constructor() {
1064
+ super();
1065
+ logger$1.trace("constructor: called", `_uuid: ${this._uuid}`);
1066
+ }
1067
+ _getComponentURL() {
1068
+ const parentApp = this.#parentApp;
1069
+ if (!parentApp) {
1070
+ throw this.#loError.create("Undefined parent App!");
1071
+ }
1072
+ const compURL = parentApp._getComponentURL(this._standardName, this._uuid);
1073
+ return compURL;
1074
+ }
1075
+ _init() {
1076
+ if (!this._ready) {
1077
+ // We don't care about events.lo.iframe.ready, we care about messages.lo.ready
1078
+ this.#loIFrame.load(this._getComponentURL().href);
1079
+ }
1080
+ }
1081
+ #messageListener = (event) => {
1082
+ if (event.data.id !== this._uuid) {
1083
+ // Ignore messages that are not meant to be received by this instance.
1084
+ return;
1085
+ }
1086
+ logger$1.debug("#messageListener:", `this._uuid: ${this._uuid}`, `this._standardName: ${this._standardName}`, `event.data: ${JSON.stringify(event.data)}`);
1087
+ switch (event.data.type) {
1088
+ case messages.lo.ready: {
1089
+ while (this.#eventQueue.length) {
1090
+ const item = this.#eventQueue.shift();
1091
+ if (!item)
1092
+ continue;
1093
+ if (item.type === "add") {
1094
+ this.addEventListener(...item.args);
1095
+ }
1096
+ else if (item.type === "remove") {
1097
+ this.removeEventListener(...item.args);
1098
+ }
1099
+ else if (item.type === "dispatch") {
1100
+ this.dispatchEvent(item.event);
1101
+ }
1102
+ }
1103
+ this._ready = true;
1104
+ super.dispatchEvent(new CustomEvent(events.lo.component.ready));
1105
+ break;
1106
+ }
1107
+ case messages.lo.getComponentData: {
1108
+ this.#propObserver = new PropertyObserver(this, this.#propObserverCallback, this.#shouldObserveCallback);
1109
+ break;
1110
+ }
1111
+ case messages.lo.dispatchEvent: {
1112
+ const customEvent = new CustomEvent(event.data.name, {
1113
+ detail: event.data.detail,
1114
+ });
1115
+ super.dispatchEvent(customEvent);
1116
+ break;
1117
+ }
1118
+ case messages.lo.error: {
1119
+ this.#loError.dispatch(events.lo.component.error, event.data.error);
1120
+ break;
1121
+ }
1122
+ default: {
1123
+ logger$1.info(`#messageListener:`, `Unknown message received:`, {
1124
+ "event.data": event.data,
1125
+ });
1126
+ }
1127
+ }
1128
+ };
1129
+ _propertyChanged_style = (_style) => {
1130
+ // Iterate over the CSSStyleDeclaration object to filter the CSS variables. (Notice we don't
1131
+ // use the style argument, we take it from this.style).
1132
+ const cssDeclaration = this.style;
1133
+ const cssVars = [];
1134
+ for (let i = 0; i < cssDeclaration.length; i += 1) {
1135
+ const prop = cssDeclaration.item(i);
1136
+ if (prop.startsWith("--")) {
1137
+ cssVars.push(`${prop}:${cssDeclaration.getPropertyValue(prop)}`);
1138
+ }
1139
+ }
1140
+ return cssVars.join(";");
1141
+ };
1142
+ #propObserverCallback = (changes) => {
1143
+ const propsToSend = processPropertyChanges(this, changes);
1144
+ logger$1.debug("#propObserverCallback:", { changes, propsToSend });
1145
+ if (this.#initialRender) {
1146
+ this.#initialRender = false;
1147
+ this.#loIFrame.postMessage({
1148
+ type: messages.lo.setComponentData,
1149
+ componentData: {
1150
+ id: this._uuid,
1151
+ name: this._standardName,
1152
+ props: propsToSend,
1153
+ },
1154
+ });
1155
+ }
1156
+ else {
1157
+ this.#loIFrame.postMessage({
1158
+ type: messages.lo.setComponentProps,
1159
+ componentProps: propsToSend,
1160
+ });
1161
+ }
1162
+ };
1163
+ #shouldObserveCallback = (attrOrPropName, isStandard) => {
1164
+ const attrName = camelToKebab(attrOrPropName); // do this becaouse our config Sets are in kebab-case
1165
+ logger$1.debug("#shouldObserveCallback:", { attrOrPropName, attrName, isStandard });
1166
+ if (isStandard) {
1167
+ if (MIRROR.has(attrName) || ARIA_MIRROR.has(attrName)) {
1168
+ return true;
1169
+ }
1170
+ if (BLOCK.has(attrName) || ARIA_BLOCK.has(attrName) || attrName.startsWith("on")) {
1171
+ logger$1.warn(`"${attrName}" will not be mirrored.`);
1172
+ return false;
1173
+ }
1174
+ if (attrName.startsWith("data-mirror-")) {
1175
+ return true;
1176
+ }
1177
+ // Everything else
1178
+ logger$1.warn(`"${attrName}" will not be mirrored.`);
1179
+ return false;
1180
+ }
1181
+ else {
1182
+ return !attrName.startsWith("_");
1183
+ }
1184
+ };
1185
+ addEventListener(eventName, listener, options) {
1186
+ let key = this.#listenerKeyMap.get(listener);
1187
+ if (!key) {
1188
+ key = `${eventName}_${this.#listenerKeySeed++}`;
1189
+ this.#listenerKeyMap.set(listener, key);
1190
+ }
1191
+ if (this.#loIFrame.iframeReady) {
1192
+ // @ts-ignore: Spread arguments works at runtime even though TypeScript doesn't like it
1193
+ super.addEventListener(...arguments);
1194
+ this.#loIFrame.postMessage({
1195
+ name: eventName,
1196
+ options: options,
1197
+ listenerKey: key,
1198
+ type: messages.lo.addEventListener,
1199
+ });
1200
+ }
1201
+ else {
1202
+ this.#eventQueue.push({ type: "add", args: [eventName, listener, options] });
1203
+ logger$1.debug(`addEventListener:`, `#eventQueue pushed add args:`, [eventName, listener, options]);
1204
+ }
1205
+ }
1206
+ dispatchEvent(event) {
1207
+ if (event.type.startsWith("lo.")) {
1208
+ logger$1.debug(`dispatchEvent: dispatching event "${event.type}" to this Element only`);
1209
+ return super.dispatchEvent(event);
1210
+ }
1211
+ else {
1212
+ if (this.#loIFrame.iframeReady) {
1213
+ logger$1.debug(`dispatchEvent: dispatching event "${event.type}" to this Element and embedded Element inside the iframe`);
1214
+ const result = super.dispatchEvent(event);
1215
+ this.#loIFrame.postMessage({
1216
+ name: event.type,
1217
+ detail: event.detail || {},
1218
+ type: messages.lo.dispatchEvent,
1219
+ });
1220
+ return result;
1221
+ }
1222
+ else {
1223
+ logger$1.debug(`dispatchEvent: iframe not reade, queueing event "${event.type}"`);
1224
+ this.#eventQueue.push({ type: "dispatch", event });
1225
+ return true; // Assuming success since we're queueing it
1226
+ }
1227
+ }
1228
+ }
1229
+ removeEventListener(eventName, listener, options) {
1230
+ const key = this.#listenerKeyMap.get(listener);
1231
+ if (this.#loIFrame.iframeReady) {
1232
+ // @ts-ignore: Spread arguments works at runtime even though TypeScript doesn't like it
1233
+ super.removeEventListener(...arguments);
1234
+ this.#loIFrame.postMessage({
1235
+ name: eventName,
1236
+ options: options,
1237
+ listenerKey: key,
1238
+ type: messages.lo.removeEventListener,
1239
+ });
1240
+ }
1241
+ else {
1242
+ this.#eventQueue.push({ type: "remove", args: [eventName, listener, options] });
1243
+ logger$1.debug(`removeEventListener:`, `#eventQueue pushed remove args:`, [eventName, listener, options]);
1244
+ }
1245
+ if (key) {
1246
+ this.#listenerKeyMap.delete(listener);
1247
+ }
1248
+ }
1249
+ adoptedCallback() {
1250
+ this.remove(); // will call disconnectedCallback
1251
+ throw this.#loError.create("This component cannot be rerendered for security reasons.");
1252
+ }
1253
+ connectedCallback() {
1254
+ logger$1.trace("connectedCallback: called", `_uuid: ${this._uuid}`);
1255
+ window.addEventListener("message", this.#messageListener);
1256
+ if (this.hasChildNodes()) {
1257
+ throw this.#loError.create("Should not have child nodes");
1258
+ }
1259
+ // Apply default styles
1260
+ this.style.display ||= "block";
1261
+ this.style.width ||= "100%";
1262
+ this.style.height ||= "100%";
1263
+ this.#parentApp = registry.registerComponent(this);
1264
+ this._standardName = this.#parentApp._getComponentStandardName(this);
1265
+ if (this.#parentApp._ready) {
1266
+ this._init();
1267
+ }
1268
+ }
1269
+ disconnectedCallback() {
1270
+ logger$1.trace("disconnectedCallback: called", `_uuid: ${this._uuid}`);
1271
+ // Since disconnectedCallback is called after the DOM has been removed, destroying the iframe here may be a moot
1272
+ // point, but doing it for completeness.
1273
+ this.#loIFrame.destroy();
1274
+ window.removeEventListener("message", this.#messageListener);
1275
+ registry.unregisterComponent(this);
1276
+ // Stop PropertyObserver
1277
+ this.#propObserver?.disconnect();
1278
+ }
1279
+ connectedMoveCallback() {
1280
+ // Define this callback but do nothing
1281
+ }
1282
+ }
1283
+
1284
+ /**
1285
+ * Router for handling Lightning Out component URLs
1286
+ */
1287
+ class LightningOutRouter {
1288
+ config;
1289
+ errorHandler;
1290
+ constructor(config, errorHandler) {
1291
+ this.config = config;
1292
+ this.errorHandler = errorHandler;
1293
+ if (!this.config.origin) {
1294
+ throw this.errorHandler(`Missing "frontdoor-url" or "org-url" attribute`);
1295
+ }
1296
+ }
1297
+ getComponentURL(componentName, parentElementId) {
1298
+ let appURL;
1299
+ if (this.config.sitePrefix === undefined) {
1300
+ // Route to LWR
1301
+ let lwrApp = componentName.includes("/")
1302
+ ? this.config.lwrAppComp
1303
+ : componentName.includes(":")
1304
+ ? this.config.lwrAppAura
1305
+ : undefined;
1306
+ if (lwrApp === undefined) {
1307
+ throw this.errorHandler(`Invalid componentName: ${componentName}`);
1308
+ }
1309
+ lwrApp = lwrApp.replace("/", "%2F"); // ¯\_(ツ)_/¯
1310
+ const lang = this.config.lang ? `l/${this.config.lang}/` : "";
1311
+ appURL = new URL(`lwr/application/amd/0/${lang}ai/${lwrApp}`, this.config.origin);
1312
+ }
1313
+ else {
1314
+ // Route to CLWR or TEST environment. Note: sitePrefix may be blank and that's okay.
1315
+ const pathname = `${this.config.sitePrefix}/lightning-out` ;
1316
+ appURL = new URL(pathname, this.config.origin);
1317
+ }
1318
+ appURL.searchParams.set("componentName", componentName);
1319
+ return this.#addCommonParams(appURL, parentElementId);
1320
+ }
1321
+ getAuthURL(parentElementId) {
1322
+ let appURL;
1323
+ if (this.config.sitePrefix === undefined) {
1324
+ // Route to LWR
1325
+ const lwrApp = this.config.lwrAppAuth.replace("/", "%2F"); // ¯\_(ツ)_/¯
1326
+ appURL = new URL(`lwr/application/amd/0/ai/${lwrApp}`, this.config.origin);
1327
+ }
1328
+ else {
1329
+ // Route to CLWR or TEST environment. Note: sitePrefix may be blank and that's okay.
1330
+ appURL = new URL(this.config.lwrPageAuth, this.config.origin);
1331
+ }
1332
+ return this.#addCommonParams(appURL, parentElementId);
1333
+ }
1334
+ getPageURL(pagePathname, parentElementId) {
1335
+ const pageURL = new URL(pagePathname, this.config.origin);
1336
+ return this.#addCommonParams(pageURL, parentElementId);
1337
+ }
1338
+ #addCommonParams(url, parentElementId) {
1339
+ url.searchParams.set("parentElementId", parentElementId);
1340
+ url.searchParams.set("loAppOrigin", this.config.loAppOrigin);
1341
+ // This helps in general but also for cache busting
1342
+ url.searchParams.set("loVersion", "2.2.0-rc.2");
1343
+ if (this.config.appId) {
1344
+ url.searchParams.set("appId", this.config.appId);
1345
+ }
1346
+ if (this.config.testMode) {
1347
+ url.searchParams.set("testMode", "true");
1348
+ }
1349
+ if (this.config.designSystem) {
1350
+ url.searchParams.set("designSystem", this.config.designSystem);
1351
+ }
1352
+ if (this.config.globalStyle) {
1353
+ url.searchParams.set("globalStyle", this.config.globalStyle);
1354
+ }
1355
+ return url;
1356
+ }
1357
+ }
1358
+
1359
+ /**
1360
+ * LightningOutApplication class for Lightning Out
1361
+ */
1362
+ const logger = new Logger("LightningOutApplication");
1363
+ /** Valid values for the optional design-system attribute. */
1364
+ const DESIGN_SYSTEM_VALUES = new Set(["slds1", "slds2", "none"]);
1365
+ // These props trigger LO app initialization
1366
+ const INIT_TRIGGERING_PROPS = new Set(["frontdoorUrl", "orgUrl"]);
1367
+ class LightningOutApplication extends HTMLElement {
1368
+ _uuid = getUUID();
1369
+ _ready = false;
1370
+ #loError = new LightningOutError(this);
1371
+ #loIFrame = new LightningOutIFrame({
1372
+ parentElement: this,
1373
+ isVisible: false,
1374
+ });
1375
+ #loRouter;
1376
+ #propObserver;
1377
+ #lwrAppOrigin = "";
1378
+ #lwrAppAuth = "lightningout/auth";
1379
+ #lwrAppComp = "lightningout/container";
1380
+ #lwrAppAura = "lightningout/auraContainer";
1381
+ #lwrPageAuth = "lightning/lightning.out.auth.html";
1382
+ #lwrPageLogout = "lightning/lightning.out.logout.html";
1383
+ #lwrPageAuthError = "lightning/lightning.out.auth.error.html";
1384
+ #logoutUrl = "/secur/logout.jsp";
1385
+ lwrApplication;
1386
+ orgUrl;
1387
+ #orgUrl;
1388
+ frontdoorUrl;
1389
+ #frontdoorUrl;
1390
+ appId;
1391
+ #appId;
1392
+ components; // CSV of component names
1393
+ #compNames = new Map(); // a Map of of component name->alias
1394
+ sitePrefix;
1395
+ #sitePrefix;
1396
+ designSystem;
1397
+ #designSystem;
1398
+ globalStyle;
1399
+ #globalStyle;
1400
+ #lang = document.documentElement.lang ?? "";
1401
+ constructor() {
1402
+ super();
1403
+ logger.trace("constructor: called", `_uuid: ${this._uuid}`);
1404
+ // We have to register this instance as early as possible
1405
+ registry.registerApplication(this);
1406
+ }
1407
+ #access(orgUrl) {
1408
+ try {
1409
+ this.#orgUrl = new URL(orgUrl);
1410
+ }
1411
+ catch {
1412
+ throw this.#loError.create(`Invalid org-url: ${orgUrl}`);
1413
+ }
1414
+ // Skip auth by simulating a successful lo.iframe.load event to continue normally
1415
+ this.dispatchEvent(new CustomEvent(events.lo.iframe.load, {
1416
+ detail: this.#orgUrl.origin,
1417
+ }));
1418
+ }
1419
+ #login(frontdoorUrl) {
1420
+ try {
1421
+ this.#frontdoorUrl = new URL(frontdoorUrl);
1422
+ this.#lwrAppOrigin = this.#frontdoorUrl.origin;
1423
+ // Append/override the `startURL` param in frontdoor.jsp
1424
+ const startURL = this.#getAuthURL();
1425
+ // (It is important to use the right paramName!)
1426
+ const paramName = this.#frontdoorUrl.searchParams.has("otp") ? "startURL" : "retURL";
1427
+ this.#frontdoorUrl.searchParams.set(paramName, startURL.pathname + startURL.search);
1428
+ // Append the `error-redirect-uri` param to frontdoor.jsp
1429
+ const authErrorPage = this.#getPageURL(this.#lwrPageAuthError);
1430
+ this.#frontdoorUrl.searchParams.set("error-redirect-uri", authErrorPage.pathname + authErrorPage.search);
1431
+ }
1432
+ catch {
1433
+ throw this.#loError.create(`Invalid frontdoor-url: ${frontdoorUrl}`);
1434
+ }
1435
+ this.#loIFrame.load(this.#frontdoorUrl.href);
1436
+ }
1437
+ #logout() {
1438
+ const logoutUrl = new URL(this.#logoutUrl, this.#lwrAppOrigin);
1439
+ const logoutPage = this.#getPageURL(this.#lwrPageLogout);
1440
+ // Append the `redirect-uri` param to logout.jsp (using the same auth page just to get the lo.loaded message)
1441
+ logoutUrl.searchParams.set("redirect-uri", logoutPage.pathname + logoutPage.search);
1442
+ // (This next piece of code should be removed after making sure `redirect-uri` works well)
1443
+ logoutUrl.searchParams.set("retUrl", logoutPage.pathname + logoutPage.search);
1444
+ this.#loIFrame.load(logoutUrl.href);
1445
+ // (This next piece of code should be removed after making sure `redirect-uri` works well)
1446
+ setTimeout(() => {
1447
+ this.dispatchEvent(new CustomEvent(events.lo.application.logout));
1448
+ }, 3000);
1449
+ }
1450
+ getRouter() {
1451
+ if (this.#loRouter === undefined) {
1452
+ const config = {
1453
+ origin: this.#lwrAppOrigin,
1454
+ lwrPageAuth: this.#lwrPageAuth,
1455
+ lwrAppAuth: this.#lwrAppAuth,
1456
+ lwrAppComp: this.#lwrAppComp,
1457
+ lwrAppAura: this.#lwrAppAura,
1458
+ sitePrefix: this.#sitePrefix,
1459
+ lang: this.#lang,
1460
+ appId: this.#appId,
1461
+ testMode: this.__testMode || "debug" === "test",
1462
+ loAppOrigin: window.location.origin,
1463
+ designSystem: this.#designSystem,
1464
+ globalStyle: this.#globalStyle,
1465
+ };
1466
+ this.#loRouter = new LightningOutRouter(config, (msg) => this.#loError.create(msg));
1467
+ }
1468
+ return this.#loRouter;
1469
+ }
1470
+ _getComponentURL(componentName, parentElementId) {
1471
+ return this.getRouter().getComponentURL(componentName, parentElementId);
1472
+ }
1473
+ #getAuthURL() {
1474
+ return this.getRouter().getAuthURL(this._uuid);
1475
+ }
1476
+ #getPageURL(pagePathname) {
1477
+ return this.getRouter().getPageURL(pagePathname, this._uuid);
1478
+ }
1479
+ #iframeLoaded = (event) => {
1480
+ // Set our internal flag
1481
+ this._ready = true;
1482
+ // The origin where the iframe landed might be different than the origin used to load the
1483
+ // frontdoor url, so update its value.
1484
+ this.#lwrAppOrigin = event.detail;
1485
+ // Initiate registered components
1486
+ this.#initComponents();
1487
+ // Notify the user that the application session is ready
1488
+ this.dispatchEvent(new CustomEvent(events.lo.application.ready));
1489
+ };
1490
+ #iframeError = (event) => {
1491
+ // Notify the user that the application session has failed
1492
+ this.#loError.dispatch(events.lo.application.error, event);
1493
+ };
1494
+ #iframeLogout = (event) => {
1495
+ // Notify the user that the application session is terminated
1496
+ this.#loError.dispatch(events.lo.application.logout, event);
1497
+ };
1498
+ #iframeAuthRedirect = (event) => {
1499
+ // Notify the user that an authorization redirect was issued, and pass the redirectUrl and redirectOrigin
1500
+ this.dispatchEvent(new CustomEvent(events.lo.application.auth.redirect, { detail: event.detail }));
1501
+ };
1502
+ #initComponents() {
1503
+ const myComps = registry.getComps(this);
1504
+ myComps.forEach((comp) => {
1505
+ comp._init();
1506
+ });
1507
+ }
1508
+ #propObserverCallback = (changes) => {
1509
+ // If multiple attrs/props are being changed at the same time, process the ones that don't trigger
1510
+ // initialization first, and the ones that trigger initialization last.
1511
+ const changesFirst = {};
1512
+ const changesLast = {};
1513
+ Object.keys(changes).forEach((prop) => {
1514
+ if (INIT_TRIGGERING_PROPS.has(prop)) {
1515
+ changesLast[prop] = changes[prop];
1516
+ }
1517
+ else {
1518
+ changesFirst[prop] = changes[prop];
1519
+ }
1520
+ });
1521
+ processPropertyChanges(this, changesFirst);
1522
+ processPropertyChanges(this, changesLast);
1523
+ };
1524
+ #shouldObserveCallback = (propName, isStandard) => {
1525
+ if (isStandard) {
1526
+ if (propName === "lang") {
1527
+ return true;
1528
+ }
1529
+ return false;
1530
+ }
1531
+ else {
1532
+ return !propName.startsWith("_");
1533
+ }
1534
+ };
1535
+ _propertyChanged_lwrApplication = (lwrApplication) => {
1536
+ if (lwrApplication !== undefined) {
1537
+ const parts = lwrApplication.split("/");
1538
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
1539
+ throw this.#loError.create(`"${lwrApplication}" is not a valid lwr-application name, must be of the form 'namespace/name'`);
1540
+ }
1541
+ this.#lwrAppComp = lwrApplication;
1542
+ }
1543
+ };
1544
+ _propertyChanged_lang = (lang) => {
1545
+ this.#lang = lang ?? "";
1546
+ };
1547
+ _propertyChanged_orgUrl = (orgUrl) => {
1548
+ if (orgUrl !== undefined) {
1549
+ if (this.#frontdoorUrl !== undefined) {
1550
+ throw this.#loError.create(`Can't set "org-url" because "frontdoor-url" is already set`);
1551
+ }
1552
+ if (orgUrl === "") {
1553
+ this.#logout();
1554
+ }
1555
+ else {
1556
+ this.#access(orgUrl);
1557
+ }
1558
+ }
1559
+ };
1560
+ _propertyChanged_frontdoorUrl = (frontdoorUrl) => {
1561
+ if (frontdoorUrl !== undefined) {
1562
+ if (this.#orgUrl !== undefined) {
1563
+ throw this.#loError.create(`Can't set "frontdoor-url" because "org-url" is already set`);
1564
+ }
1565
+ if (frontdoorUrl === "") {
1566
+ this.#logout();
1567
+ }
1568
+ else {
1569
+ this.#login(frontdoorUrl);
1570
+ }
1571
+ }
1572
+ };
1573
+ _propertyChanged_sitePrefix = (sitePrefix) => {
1574
+ if (sitePrefix !== undefined) {
1575
+ this.#sitePrefix = sitePrefix;
1576
+ }
1577
+ };
1578
+ _propertyChanged_appId = (appId) => {
1579
+ if (appId !== undefined) {
1580
+ this.#appId = appId;
1581
+ }
1582
+ };
1583
+ _propertyChanged_globalStyle = (globalStyle) => {
1584
+ if (globalStyle !== undefined) {
1585
+ // Validate that all properties are CSS custom properties (start with --)
1586
+ // and normalize by removing unnecessary whitespace
1587
+ const properties = globalStyle
1588
+ .split(";")
1589
+ .map((prop) => prop.trim())
1590
+ .filter((prop) => prop.length > 0);
1591
+ const normalizedProperties = [];
1592
+ for (const property of properties) {
1593
+ const [key, ...valueParts] = property.split(":");
1594
+ const trimmedKey = key.trim();
1595
+ // Validate CSS custom property
1596
+ if (!trimmedKey.startsWith("--")) {
1597
+ throw this.#loError.create(`Invalid global-style: "${trimmedKey}" is not a CSS custom property. Only CSS custom properties (starting with --) are allowed.`);
1598
+ }
1599
+ // Normalize: remove extra whitespace
1600
+ const trimmedValue = valueParts.join(":").trim();
1601
+ if (trimmedValue) {
1602
+ normalizedProperties.push(`${trimmedKey}:${trimmedValue}`);
1603
+ }
1604
+ }
1605
+ // Store normalized version (removes unnecessary whitespace and keeps trailing semicolon)
1606
+ this.#globalStyle = normalizedProperties.join(";") + ";";
1607
+ }
1608
+ };
1609
+ /**
1610
+ * components is a comma separated list of names with optional aliases. The names can be standard (i.e. foo/bar) or
1611
+ * kebab (i.e. foo-bar). It is recommended to use standard names because that's what the backend runtime requires,
1612
+ * and converting from kebab name to standard name can be ambiguous. (W-19562914)
1613
+ *
1614
+ * Example: components = "foo/bar, baz-qux, c/myComp as alias-for-my-comp, other-comp as other-comp-alias"
1615
+ */
1616
+ _propertyChanged_components = (components) => {
1617
+ // Ignore undefined initialization value
1618
+ if (components === undefined) {
1619
+ return;
1620
+ }
1621
+ components.split(",").forEach((comp) => {
1622
+ const [name, alias] = comp.split(" as ").map((n) => n.trim());
1623
+ // Register the component name and its optional alias. This callback may be called
1624
+ // multiple times, so if a component name is already registered by this application
1625
+ // instance, just skip it, so only register new names.
1626
+ const isStandard = isStandardName(name);
1627
+ // Disallow Aura-style names (namespace:name) in components; Aura is indicated via the "aura"
1628
+ // attribute on the component element instead.
1629
+ if (isStandard && name.includes(":")) {
1630
+ throw this.#loError.create(`"${name}" is not supported in components. Use kebab "namespace-name" in components and the "aura" attribute on the component element instead.`);
1631
+ }
1632
+ const standardName = isStandard ? name : elementNameToStandardName(name);
1633
+ const localName = alias || (isStandard ? standardNameToElementName(standardName) : name);
1634
+ if (localName && !this.#compNames.has(localName)) {
1635
+ this.#compNames.set(localName, standardName);
1636
+ try {
1637
+ registry.registerComponentName(localName, this);
1638
+ // Use a new anonymous class for each definition, extending our base class
1639
+ customElements.define(localName, class extends LightningOutComponent {
1640
+ });
1641
+ }
1642
+ catch (err) {
1643
+ // This name is already registered by another application.
1644
+ throw this.#loError.create(`"${localName}" is already registered. ${err}`);
1645
+ }
1646
+ }
1647
+ });
1648
+ };
1649
+ _propertyChanged_designSystem = (designSystem) => {
1650
+ if (designSystem !== undefined && !DESIGN_SYSTEM_VALUES.has(designSystem)) {
1651
+ throw this.#loError.create(`Invalid design-system: ${designSystem}`);
1652
+ }
1653
+ this.#designSystem = designSystem;
1654
+ };
1655
+ _getComponentStandardName(component) {
1656
+ const localName = component.localName;
1657
+ const isAura = component.hasAttribute("aura");
1658
+ const standardName = this.#compNames.get(localName);
1659
+ if (!standardName) {
1660
+ throw this.#loError.create(`"${localName}" is not registered.`);
1661
+ }
1662
+ if (isAura) {
1663
+ // If this is an Aura component (has aura attribute) and the stored name is in LWC format (c/name),
1664
+ // convert it to Aura format (c:name)
1665
+ if (standardName.includes("/")) {
1666
+ return standardName.replace("/", ":");
1667
+ }
1668
+ }
1669
+ return standardName;
1670
+ }
1671
+ get _compNames() {
1672
+ }
1673
+ connectedCallback() {
1674
+ logger.trace("connectedCallback: called", `_uuid: ${this._uuid}`);
1675
+ if (this.hasChildNodes()) {
1676
+ throw this.#loError.create("Should not have child nodes");
1677
+ }
1678
+ // Apply default styles
1679
+ this.style.display ||= "none";
1680
+ // Add event listeners
1681
+ this.addEventListener(events.lo.iframe.load, this.#iframeLoaded);
1682
+ this.addEventListener(events.lo.iframe.error, this.#iframeError);
1683
+ this.addEventListener(events.lo.iframe.logout, this.#iframeLogout);
1684
+ this.addEventListener(events.lo.iframe.auth.redirect, this.#iframeAuthRedirect);
1685
+ // Start PropertyObserver
1686
+ this.#propObserver = new PropertyObserver(this, this.#propObserverCallback, this.#shouldObserveCallback);
1687
+ }
1688
+ disconnectedCallback() {
1689
+ logger.trace("disconnectedCallback: called", `_uuid: ${this._uuid}`);
1690
+ // Since disconnectedCallback is called after the DOM has been removed, calling logout and destroying the iframe
1691
+ // here may be a moot point, but doing it for completeness.
1692
+ this.#logout();
1693
+ this.#loIFrame.destroy();
1694
+ // Remove event listeners
1695
+ this.removeEventListener(events.lo.iframe.load, this.#iframeLoaded);
1696
+ this.removeEventListener(events.lo.iframe.error, this.#iframeError);
1697
+ this.removeEventListener(events.lo.iframe.logout, this.#iframeLogout);
1698
+ this.removeEventListener(events.lo.iframe.auth.redirect, this.#iframeAuthRedirect);
1699
+ // Stop PropertyObserver
1700
+ this.#propObserver?.disconnect();
1701
+ }
1702
+ connectedMoveCallback() {
1703
+ // Define this callback but do nothing
1704
+ }
1705
+ }
1706
+
1707
+ /**
1708
+ * Core functionality for Lightning Out 2.0
1709
+ */
1710
+ Logger.level = "debug";
1711
+
1712
+ /**
1713
+ * @salesforce/lightning-out
1714
+ *
1715
+ * Main entry point for Lightning Out 2.0
1716
+ */
1717
+ window.customElements.define("lightning-out-application", LightningOutApplication);
1718
+
1719
+ exports.LightningOutApplication = LightningOutApplication;
1720
+
1721
+ return exports;
1722
+
1723
+ })({});
1724
+ //# sourceMappingURL=index.iife.debug.js.map