@gooddata/sdk-ui-pluggable-host 11.40.0-alpha.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/LICENSE +19 -0
  2. package/README.md +20 -0
  3. package/esm/assets/logo-white.svg +3 -0
  4. package/esm/components/FullScreenLoader.d.ts +1 -0
  5. package/esm/components/FullScreenLoader.js +8 -0
  6. package/esm/components/HostUiContainer.d.ts +16 -0
  7. package/esm/components/HostUiContainer.js +141 -0
  8. package/esm/components/HostUiContainer.scss +5 -0
  9. package/esm/components/Root.d.ts +16 -0
  10. package/esm/components/Root.js +64 -0
  11. package/esm/components/Root.scss +14 -0
  12. package/esm/components/lib/translations.d.ts +7 -0
  13. package/esm/components/lib/translations.js +64 -0
  14. package/esm/components/useRedirectNavigation.d.ts +7 -0
  15. package/esm/components/useRedirectNavigation.js +23 -0
  16. package/esm/components/useRedirectTarget.d.ts +19 -0
  17. package/esm/components/useRedirectTarget.js +62 -0
  18. package/esm/debug.d.ts +9 -0
  19. package/esm/debug.js +18 -0
  20. package/esm/index.d.ts +11 -0
  21. package/esm/index.js +10 -0
  22. package/esm/lib/chunkReloadGuard.d.ts +89 -0
  23. package/esm/lib/chunkReloadGuard.js +203 -0
  24. package/esm/lib/hostNotifications.d.ts +20 -0
  25. package/esm/lib/hostNotifications.js +50 -0
  26. package/esm/lib/isProduction.d.ts +12 -0
  27. package/esm/lib/isProduction.js +13 -0
  28. package/esm/loader/lastVisitedApp.d.ts +11 -0
  29. package/esm/loader/lastVisitedApp.js +43 -0
  30. package/esm/loader/localLoader.d.ts +16 -0
  31. package/esm/loader/localLoader.js +38 -0
  32. package/esm/loader/pluggableApplicationsLoader.d.ts +13 -0
  33. package/esm/loader/pluggableApplicationsLoader.js +55 -0
  34. package/esm/loader/redirectLogic.d.ts +30 -0
  35. package/esm/loader/redirectLogic.js +143 -0
  36. package/esm/loader/remoteLoader.d.ts +5 -0
  37. package/esm/loader/remoteLoader.js +117 -0
  38. package/esm/loader/remoteUrlSecurity.d.ts +1 -0
  39. package/esm/loader/remoteUrlSecurity.js +26 -0
  40. package/esm/loader/routing.d.ts +22 -0
  41. package/esm/loader/routing.js +87 -0
  42. package/esm/platformContext/backend.d.ts +44 -0
  43. package/esm/platformContext/backend.js +131 -0
  44. package/esm/platformContext/bootstrap.d.ts +15 -0
  45. package/esm/platformContext/bootstrap.js +122 -0
  46. package/esm/platformContext/loadPlatformContext.d.ts +18 -0
  47. package/esm/platformContext/loadPlatformContext.js +50 -0
  48. package/esm/platformContext/tigerNotAuthenticatedHandler.d.ts +3 -0
  49. package/esm/platformContext/tigerNotAuthenticatedHandler.js +16 -0
  50. package/esm/platformContext/types.d.ts +17 -0
  51. package/esm/platformContext/types.js +2 -0
  52. package/esm/platformContext/useLoadPlatformContext.d.ts +35 -0
  53. package/esm/platformContext/useLoadPlatformContext.js +131 -0
  54. package/esm/platformContext/useWorkspacePermissions.d.ts +26 -0
  55. package/esm/platformContext/useWorkspacePermissions.js +52 -0
  56. package/esm/platformContext/useWorkspaceSettings.d.ts +25 -0
  57. package/esm/platformContext/useWorkspaceSettings.js +46 -0
  58. package/esm/registry/pluggableApplicationsRegistry.d.ts +55 -0
  59. package/esm/registry/pluggableApplicationsRegistry.js +203 -0
  60. package/esm/sdk-ui-pluggable-host.d.ts +262 -0
  61. package/esm/styles/global.css +16 -0
  62. package/esm/translations/de-DE.json +34 -0
  63. package/esm/translations/en-AU.json +34 -0
  64. package/esm/translations/en-GB.json +34 -0
  65. package/esm/translations/en-US.json +130 -0
  66. package/esm/translations/es-419.json +34 -0
  67. package/esm/translations/es-ES.json +34 -0
  68. package/esm/translations/fi-FI.json +34 -0
  69. package/esm/translations/fr-CA.json +34 -0
  70. package/esm/translations/fr-FR.json +34 -0
  71. package/esm/translations/id-ID.json +34 -0
  72. package/esm/translations/it-IT.json +34 -0
  73. package/esm/translations/ja-JP.json +34 -0
  74. package/esm/translations/ko-KR.json +34 -0
  75. package/esm/translations/nl-NL.json +34 -0
  76. package/esm/translations/pl-PL.json +34 -0
  77. package/esm/translations/pt-BR.json +34 -0
  78. package/esm/translations/pt-PT.json +34 -0
  79. package/esm/translations/ru-RU.json +34 -0
  80. package/esm/translations/sl-SI.json +34 -0
  81. package/esm/translations/th-TH.json +34 -0
  82. package/esm/translations/tr-TR.json +34 -0
  83. package/esm/translations/uk-UA.json +34 -0
  84. package/esm/translations/vi-VN.json +34 -0
  85. package/esm/translations/zh-HK.json +34 -0
  86. package/esm/translations/zh-Hans.json +34 -0
  87. package/esm/translations/zh-Hant.json +34 -0
  88. package/esm/tsdoc-metadata.json +11 -0
  89. package/esm/types/lifecycle.d.ts +18 -0
  90. package/esm/types/lifecycle.js +2 -0
  91. package/esm/ui/DefaultHostUi.d.ts +12 -0
  92. package/esm/ui/DefaultHostUi.js +101 -0
  93. package/esm/ui/DefaultHostUi.scss +8 -0
  94. package/esm/ui/GenAIChat.d.ts +43 -0
  95. package/esm/ui/GenAIChat.js +102 -0
  96. package/esm/ui/HostChrome.d.ts +19 -0
  97. package/esm/ui/HostChrome.js +115 -0
  98. package/esm/ui/HostChrome.scss +24 -0
  99. package/esm/ui/HostIntlProvider.d.ts +9 -0
  100. package/esm/ui/HostIntlProvider.js +13 -0
  101. package/esm/ui/HostNotificationDispatcher.d.ts +12 -0
  102. package/esm/ui/HostNotificationDispatcher.js +42 -0
  103. package/esm/ui/PluggableApplicationRenderer.d.ts +10 -0
  104. package/esm/ui/PluggableApplicationRenderer.js +100 -0
  105. package/esm/ui/PluggableApplicationRenderer.scss +29 -0
  106. package/esm/ui/SemanticSearch.d.ts +23 -0
  107. package/esm/ui/SemanticSearch.js +46 -0
  108. package/esm/ui/WorkspacePicker.d.ts +9 -0
  109. package/esm/ui/WorkspacePicker.js +29 -0
  110. package/esm/ui/appMenuItems.d.ts +17 -0
  111. package/esm/ui/appMenuItems.js +81 -0
  112. package/esm/ui/chromeHelpers.d.ts +17 -0
  113. package/esm/ui/chromeHelpers.js +29 -0
  114. package/esm/ui/hostChromeBem.d.ts +1 -0
  115. package/esm/ui/hostChromeBem.js +3 -0
  116. package/esm/ui/resolveHostUiModule.d.ts +8 -0
  117. package/esm/ui/resolveHostUiModule.js +22 -0
  118. package/esm/ui/useHostChromeChat.d.ts +29 -0
  119. package/esm/ui/useHostChromeChat.js +38 -0
  120. package/esm/ui/useHostChromePricing.d.ts +52 -0
  121. package/esm/ui/useHostChromePricing.js +37 -0
  122. package/esm/ui/useHostChromeSearch.d.ts +20 -0
  123. package/esm/ui/useHostChromeSearch.js +18 -0
  124. package/esm/ui/useHostChromeWorkspaceFeatures.d.ts +19 -0
  125. package/esm/ui/useHostChromeWorkspaceFeatures.js +36 -0
  126. package/package.json +114 -0
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Optional callback invoked synchronously immediately before a stale-chunk hard reload.
3
+ * Gives the host app a chance to record telemetry before navigation cancels in-flight requests.
4
+ *
5
+ * @alpha
6
+ */
7
+ export interface IStaleChunkReloadInfo {
8
+ /** Human-readable reason — e.g. the underlying preload error message. */
9
+ reason: string;
10
+ /** COMMITHASH of the build the tab was loaded with, or an empty string if unknown. */
11
+ commitHash: string;
12
+ }
13
+ /**
14
+ * @alpha
15
+ */
16
+ export type StaleChunkReloadListener = (info: IStaleChunkReloadInfo) => void;
17
+ /**
18
+ * Registers a listener invoked synchronously just before a stale-chunk hard reload,
19
+ * intended for telemetry. Called only when the reload actually happens — skipped
20
+ * reloads (loop-guard hits) do not fire the listener.
21
+ *
22
+ * @remarks
23
+ * Trackers should send the event via `navigator.sendBeacon` or `fetch` with `keepalive`
24
+ * because the page is navigating away immediately afterwards.
25
+ *
26
+ * @alpha
27
+ */
28
+ export declare function setStaleChunkReloadListener(listener: StaleChunkReloadListener | undefined): void;
29
+ /**
30
+ * Query-string parameter appended to the URL on a stale-chunk reload. The value
31
+ * is a timestamp; its only purpose is to make the navigation target a URL the
32
+ * browser has never seen before, defeating any cached HTML/remoteEntry that
33
+ * might otherwise replay the stale module graph.
34
+ *
35
+ * Exported so callers and tests can recognise (and strip) the marker if needed.
36
+ *
37
+ * @alpha
38
+ */
39
+ export declare const STALE_CHUNK_RELOAD_PARAM = "_gdcr";
40
+ /**
41
+ * Triggers a hard page reload once, guarded against loops by sessionStorage.
42
+ *
43
+ * If the same COMMITHASH already triggered a reload within the last 30 seconds,
44
+ * the call is a no-op so the user is not stuck reloading a broken build.
45
+ *
46
+ * @remarks
47
+ * Uses `location.replace` with a cache-busting query parameter rather than
48
+ * `location.reload()`. A soft reload honours the HTTP cache, which has bitten
49
+ * us when an intermediate chunk (max-age=30d) is still served from cache and
50
+ * keeps replaying a stale module graph after the deploy moved forward. Writing
51
+ * a unique URL forces the browser to treat it as a fresh navigation.
52
+ *
53
+ * @alpha
54
+ */
55
+ export declare function reloadForStaleChunks(reason: string): void;
56
+ /**
57
+ * Installs a window listener for `vite:preloadError`. When fired (e.g. a chunk has
58
+ * been removed by a redeploy), the page is hard-reloaded once to pick up new chunk
59
+ * hashes from a fresh index.html.
60
+ *
61
+ * Idempotent: calling more than once registers the listener only once.
62
+ *
63
+ * @alpha
64
+ */
65
+ export declare function installPreloadErrorHandler(): void;
66
+ /**
67
+ * Options for {@link installVersionWatcher}.
68
+ *
69
+ * @alpha
70
+ */
71
+ export interface IVersionWatcherOptions {
72
+ /** URL of the COMMITHASH file emitted by the host build. */
73
+ url: string;
74
+ /** Poll interval in ms. Default: 5 minutes. */
75
+ intervalMs?: number;
76
+ /** Called once when a new deployment is detected. */
77
+ onNewDeployment: (newHash: string) => void;
78
+ }
79
+ /**
80
+ * Periodically polls the COMMITHASH file and fires `onNewDeployment` once when the
81
+ * deployed commit differs from the one the tab was loaded with. Stops polling
82
+ * after the first detection — the caller decides what to do (typically: show a
83
+ * "please reload" banner).
84
+ *
85
+ * Idempotent: calling more than once is a no-op after the first invocation.
86
+ *
87
+ * @alpha
88
+ */
89
+ export declare function installVersionWatcher({ url, intervalMs, onNewDeployment }: IVersionWatcherOptions): void;
@@ -0,0 +1,203 @@
1
+ // (C) 2026 GoodData Corporation
2
+ const RELOAD_FLAG_KEY = "gd-chunk-reload-guard";
3
+ const RELOAD_LOOP_WINDOW_MS = 30_000;
4
+ let preloadErrorHandlerInstalled = false;
5
+ let versionWatcherInstalled = false;
6
+ let staleChunkReloadListener;
7
+ function readReloadFlag() {
8
+ try {
9
+ const raw = sessionStorage.getItem(RELOAD_FLAG_KEY);
10
+ if (!raw) {
11
+ return undefined;
12
+ }
13
+ const parsed = JSON.parse(raw);
14
+ if (typeof parsed.hash !== "string" || typeof parsed.at !== "number") {
15
+ return undefined;
16
+ }
17
+ return { hash: parsed.hash, at: parsed.at };
18
+ }
19
+ catch {
20
+ return undefined;
21
+ }
22
+ }
23
+ function writeReloadFlag(hash) {
24
+ try {
25
+ sessionStorage.setItem(RELOAD_FLAG_KEY, JSON.stringify({ hash, at: Date.now() }));
26
+ }
27
+ catch {
28
+ // sessionStorage unavailable (privacy mode, sandboxed iframe). Without the
29
+ // anti-loop guard we accept the risk of a reload loop on a broken redeploy
30
+ // — that is still better than a silent 404 white screen.
31
+ }
32
+ }
33
+ function getCurrentHash() {
34
+ return typeof window === "undefined" ? "" : (window.COMMITHASH ?? "");
35
+ }
36
+ /**
37
+ * Registers a listener invoked synchronously just before a stale-chunk hard reload,
38
+ * intended for telemetry. Called only when the reload actually happens — skipped
39
+ * reloads (loop-guard hits) do not fire the listener.
40
+ *
41
+ * @remarks
42
+ * Trackers should send the event via `navigator.sendBeacon` or `fetch` with `keepalive`
43
+ * because the page is navigating away immediately afterwards.
44
+ *
45
+ * @alpha
46
+ */
47
+ export function setStaleChunkReloadListener(listener) {
48
+ staleChunkReloadListener = listener;
49
+ }
50
+ /**
51
+ * Query-string parameter appended to the URL on a stale-chunk reload. The value
52
+ * is a timestamp; its only purpose is to make the navigation target a URL the
53
+ * browser has never seen before, defeating any cached HTML/remoteEntry that
54
+ * might otherwise replay the stale module graph.
55
+ *
56
+ * Exported so callers and tests can recognise (and strip) the marker if needed.
57
+ *
58
+ * @alpha
59
+ */
60
+ export const STALE_CHUNK_RELOAD_PARAM = "_gdcr";
61
+ /**
62
+ * Triggers a hard page reload once, guarded against loops by sessionStorage.
63
+ *
64
+ * If the same COMMITHASH already triggered a reload within the last 30 seconds,
65
+ * the call is a no-op so the user is not stuck reloading a broken build.
66
+ *
67
+ * @remarks
68
+ * Uses `location.replace` with a cache-busting query parameter rather than
69
+ * `location.reload()`. A soft reload honours the HTTP cache, which has bitten
70
+ * us when an intermediate chunk (max-age=30d) is still served from cache and
71
+ * keeps replaying a stale module graph after the deploy moved forward. Writing
72
+ * a unique URL forces the browser to treat it as a fresh navigation.
73
+ *
74
+ * @alpha
75
+ */
76
+ export function reloadForStaleChunks(reason) {
77
+ if (typeof window === "undefined") {
78
+ return;
79
+ }
80
+ const currentHash = getCurrentHash();
81
+ const previous = readReloadFlag();
82
+ if (previous?.hash === currentHash && Date.now() - previous.at < RELOAD_LOOP_WINDOW_MS) {
83
+ console.error(`[host-runtime/chunk-reload-guard] Skipping reload for "${reason}" — already reloaded for the same build (${currentHash}) within ${RELOAD_LOOP_WINDOW_MS}ms.`);
84
+ return;
85
+ }
86
+ writeReloadFlag(currentHash);
87
+ console.warn(`[host-runtime/chunk-reload-guard] Reloading page due to: ${reason}`);
88
+ try {
89
+ staleChunkReloadListener?.({ reason, commitHash: currentHash });
90
+ }
91
+ catch (error) {
92
+ // Telemetry failures must never block the recovery reload.
93
+ console.error("[host-runtime/chunk-reload-guard] Stale-chunk reload listener threw.", error);
94
+ }
95
+ const target = new URL(window.location.href);
96
+ target.searchParams.set(STALE_CHUNK_RELOAD_PARAM, String(Date.now()));
97
+ window.location.replace(target.toString());
98
+ }
99
+ /**
100
+ * Installs a window listener for `vite:preloadError`. When fired (e.g. a chunk has
101
+ * been removed by a redeploy), the page is hard-reloaded once to pick up new chunk
102
+ * hashes from a fresh index.html.
103
+ *
104
+ * Idempotent: calling more than once registers the listener only once.
105
+ *
106
+ * @alpha
107
+ */
108
+ export function installPreloadErrorHandler() {
109
+ if (preloadErrorHandlerInstalled || typeof window === "undefined") {
110
+ return;
111
+ }
112
+ preloadErrorHandlerInstalled = true;
113
+ window.addEventListener("vite:preloadError", (event) => {
114
+ const preloadEvent = event;
115
+ // Do NOT preventDefault. Vite's preload helper only rethrows when the event
116
+ // is left default-allowed; with preventDefault the helper returns undefined,
117
+ // and downstream wrappers (notably Module Federation's expose factory, which
118
+ // does `Object.assign({}, await import(...))`) silently produce an empty
119
+ // module — surfacing as a misleading "does not export a valid pluggable app"
120
+ // error from asPluggableApp instead of a preload failure. If the loop guard
121
+ // suppresses the reload, the original rejection still gives the caller a
122
+ // truthful failure to handle.
123
+ reloadForStaleChunks(`vite:preloadError (${preloadEvent.payload?.message ?? "unknown"})`);
124
+ });
125
+ }
126
+ /**
127
+ * Periodically polls the COMMITHASH file and fires `onNewDeployment` once when the
128
+ * deployed commit differs from the one the tab was loaded with. Stops polling
129
+ * after the first detection — the caller decides what to do (typically: show a
130
+ * "please reload" banner).
131
+ *
132
+ * Idempotent: calling more than once is a no-op after the first invocation.
133
+ *
134
+ * @alpha
135
+ */
136
+ export function installVersionWatcher({ url, intervalMs = 5 * 60_000, onNewDeployment, }) {
137
+ if (versionWatcherInstalled || typeof window === "undefined") {
138
+ return;
139
+ }
140
+ const initialHash = getCurrentHash();
141
+ if (!initialHash) {
142
+ // Without a baseline we cannot detect change — the build did not inject COMMITHASH.
143
+ return;
144
+ }
145
+ versionWatcherInstalled = true;
146
+ let stopped = false;
147
+ let pendingTimeoutId;
148
+ const check = async () => {
149
+ if (stopped) {
150
+ return;
151
+ }
152
+ try {
153
+ const response = await fetch(url, { cache: "no-store", credentials: "same-origin" });
154
+ if (!response.ok) {
155
+ return;
156
+ }
157
+ const remoteHash = (await response.text()).trim();
158
+ if (remoteHash && remoteHash !== initialHash && !remoteHash.startsWith("<")) {
159
+ stopped = true;
160
+ try {
161
+ onNewDeployment(remoteHash);
162
+ }
163
+ catch (error) {
164
+ // Don't let a broken consumer kill the watcher (although we already stopped).
165
+ console.error("[host-runtime/chunk-reload-guard] onNewDeployment threw.", error);
166
+ }
167
+ }
168
+ }
169
+ catch {
170
+ // Network blip; try again on the next tick.
171
+ }
172
+ };
173
+ const scheduleNextTick = () => {
174
+ if (stopped || pendingTimeoutId !== undefined) {
175
+ return;
176
+ }
177
+ // Skip ticks while the tab is hidden — the watcher resumes on visibilitychange.
178
+ // The first poll after install is gated the same way below.
179
+ if (document.hidden) {
180
+ return;
181
+ }
182
+ pendingTimeoutId = window.setTimeout(() => {
183
+ pendingTimeoutId = undefined;
184
+ void check().finally(scheduleNextTick);
185
+ }, intervalMs);
186
+ };
187
+ document.addEventListener("visibilitychange", () => {
188
+ if (stopped) {
189
+ return;
190
+ }
191
+ if (document.hidden) {
192
+ // Cancel a pending tick so we don't fire it the moment the tab returns —
193
+ // we'll schedule a fresh interval from the visibility-change instead.
194
+ if (pendingTimeoutId !== undefined) {
195
+ window.clearTimeout(pendingTimeoutId);
196
+ pendingTimeoutId = undefined;
197
+ }
198
+ return;
199
+ }
200
+ scheduleNextTick();
201
+ });
202
+ scheduleNextTick();
203
+ }
@@ -0,0 +1,20 @@
1
+ import { type IHostUiMountHandle, type IHostUiNotification } from "@gooddata/sdk-pluggable-application-model";
2
+ /**
3
+ * Dispatches a notification into the currently mounted host UI module.
4
+ *
5
+ * @remarks
6
+ * If no UI is mounted yet (e.g. the host bootstrap is still in progress) the notification
7
+ * is queued and replayed when the host UI handle registers. The queue is capped at a small
8
+ * bounded size; oldest entries are dropped on overflow.
9
+ *
10
+ * @alpha
11
+ */
12
+ export declare function dispatchHostNotification(notification: IHostUiNotification): void;
13
+ /**
14
+ * Registers (or clears) the host UI mount handle that should receive notifications.
15
+ *
16
+ * @remarks
17
+ * Called by {@link HostUiContainer} after a successful mount and again with `undefined`
18
+ * on unmount. On register, any queued notifications are flushed in arrival order.
19
+ */
20
+ export declare function setActiveHostHandle(handle: IHostUiMountHandle | undefined): void;
@@ -0,0 +1,50 @@
1
+ // (C) 2026 GoodData Corporation
2
+ //
3
+ // Routes runtime-detected notifications (e.g. a new deployment was published) to whichever
4
+ // host UI module is currently mounted. Notifications dispatched before any UI is mounted
5
+ // are queued and replayed once the UI registers itself, so the user is never silently dropped
6
+ // because of a timing race between the host's detection and the UI mount.
7
+ //
8
+ // Bounds the buffer for pre-mount dispatches. In practice we expect at most one
9
+ // notification before the UI mounts (the new-deployment toast), so this only matters
10
+ // as defence against a misbehaving caller that loops.
11
+ const MAX_QUEUE_SIZE = 16;
12
+ let activeHandle;
13
+ const queue = [];
14
+ /**
15
+ * Dispatches a notification into the currently mounted host UI module.
16
+ *
17
+ * @remarks
18
+ * If no UI is mounted yet (e.g. the host bootstrap is still in progress) the notification
19
+ * is queued and replayed when the host UI handle registers. The queue is capped at a small
20
+ * bounded size; oldest entries are dropped on overflow.
21
+ *
22
+ * @alpha
23
+ */
24
+ export function dispatchHostNotification(notification) {
25
+ if (activeHandle?.notify) {
26
+ activeHandle.notify(notification);
27
+ return;
28
+ }
29
+ if (queue.length >= MAX_QUEUE_SIZE) {
30
+ queue.shift();
31
+ }
32
+ queue.push(notification);
33
+ }
34
+ /**
35
+ * Registers (or clears) the host UI mount handle that should receive notifications.
36
+ *
37
+ * @remarks
38
+ * Called by {@link HostUiContainer} after a successful mount and again with `undefined`
39
+ * on unmount. On register, any queued notifications are flushed in arrival order.
40
+ */
41
+ export function setActiveHostHandle(handle) {
42
+ activeHandle = handle;
43
+ if (!handle?.notify || queue.length === 0) {
44
+ return;
45
+ }
46
+ const drained = queue.splice(0, queue.length);
47
+ for (const notification of drained) {
48
+ handle.notify(notification);
49
+ }
50
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Reads the compile-time PRODUCTION flag if the consumer's bundler defines it
3
+ * (e.g. via vite's `define: { PRODUCTION: ... }`); defaults to false (dev-mode
4
+ * semantics) when undefined.
5
+ *
6
+ * Reading `PRODUCTION` directly as a bare identifier throws ReferenceError in
7
+ * any consumer whose bundler doesn't substitute it — `typeof` is the only safe
8
+ * way to probe for an unbound global without crashing.
9
+ *
10
+ * @internal
11
+ */
12
+ export declare const isProduction: boolean;
@@ -0,0 +1,13 @@
1
+ // (C) 2026 GoodData Corporation
2
+ /**
3
+ * Reads the compile-time PRODUCTION flag if the consumer's bundler defines it
4
+ * (e.g. via vite's `define: { PRODUCTION: ... }`); defaults to false (dev-mode
5
+ * semantics) when undefined.
6
+ *
7
+ * Reading `PRODUCTION` directly as a bare identifier throws ReferenceError in
8
+ * any consumer whose bundler doesn't substitute it — `typeof` is the only safe
9
+ * way to probe for an unbound global without crashing.
10
+ *
11
+ * @internal
12
+ */
13
+ export const isProduction = typeof PRODUCTION !== "undefined" && PRODUCTION;
@@ -0,0 +1,11 @@
1
+ import { type ApplicationScope } from "@gooddata/sdk-model";
2
+ /**
3
+ * Returns the last visited app ID for the given scope, or `undefined` if none is stored
4
+ * or the stored value cannot be read.
5
+ */
6
+ export declare function getLastVisitedApp(scope: ApplicationScope): string | undefined;
7
+ /**
8
+ * Persists the app ID that the user last visited in the given scope.
9
+ * Silently swallows errors (e.g. storage full, private browsing).
10
+ */
11
+ export declare function setLastVisitedApp(scope: ApplicationScope, appId: string): void;
@@ -0,0 +1,43 @@
1
+ // (C) 2026 GoodData Corporation
2
+ const STORAGE_KEY = "gdc-host-lastVisitedApp";
3
+ /**
4
+ * Safely parse the stored JSON record, falling back to an empty record on
5
+ * missing or corrupt values.
6
+ */
7
+ function safeParse(raw) {
8
+ if (!raw) {
9
+ return {};
10
+ }
11
+ try {
12
+ return JSON.parse(raw);
13
+ }
14
+ catch {
15
+ return {};
16
+ }
17
+ }
18
+ /**
19
+ * Returns the last visited app ID for the given scope, or `undefined` if none is stored
20
+ * or the stored value cannot be read.
21
+ */
22
+ export function getLastVisitedApp(scope) {
23
+ try {
24
+ return safeParse(localStorage.getItem(STORAGE_KEY))[scope] ?? undefined;
25
+ }
26
+ catch {
27
+ return undefined;
28
+ }
29
+ }
30
+ /**
31
+ * Persists the app ID that the user last visited in the given scope.
32
+ * Silently swallows errors (e.g. storage full, private browsing).
33
+ */
34
+ export function setLastVisitedApp(scope, appId) {
35
+ try {
36
+ const record = safeParse(localStorage.getItem(STORAGE_KEY));
37
+ record[scope] = appId;
38
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(record));
39
+ }
40
+ catch {
41
+ // Intentionally ignored — localStorage may be unavailable or full
42
+ }
43
+ }
@@ -0,0 +1,16 @@
1
+ import { type IPluggableApp } from "@gooddata/sdk-pluggable-application-model";
2
+ /**
3
+ * @alpha
4
+ */
5
+ export type LocalPluggableApplicationLoader = () => Promise<{
6
+ default?: IPluggableApp | unknown;
7
+ pluggableApp?: IPluggableApp;
8
+ }>;
9
+ /**
10
+ * Registers the local application loaders map. Called by the host or harness
11
+ * before any app loading occurs.
12
+ *
13
+ * @alpha
14
+ */
15
+ export declare function registerLocalApplicationLoaders(loaders: Record<string, LocalPluggableApplicationLoader>): void;
16
+ export declare function loadLocalPluggableApplication(moduleId: string): Promise<IPluggableApp>;
@@ -0,0 +1,38 @@
1
+ // (C) 2026 GoodData Corporation
2
+ let registeredLoaders = {};
3
+ /**
4
+ * Registers the local application loaders map. Called by the host or harness
5
+ * before any app loading occurs.
6
+ *
7
+ * @alpha
8
+ */
9
+ export function registerLocalApplicationLoaders(loaders) {
10
+ registeredLoaders = loaders;
11
+ localAppPromises.clear();
12
+ }
13
+ const localAppPromises = new Map();
14
+ function asPluggableApp(moduleId, loaded) {
15
+ const app = loaded.pluggableApp ?? loaded.default;
16
+ if (!app || typeof app.mount !== "function") {
17
+ throw new Error(`[host-runtime/local-loader] Local module "${moduleId}" does not export a valid pluggable app.`);
18
+ }
19
+ return app;
20
+ }
21
+ export function loadLocalPluggableApplication(moduleId) {
22
+ const cached = localAppPromises.get(moduleId);
23
+ if (cached) {
24
+ return cached;
25
+ }
26
+ const loader = registeredLoaders[moduleId];
27
+ if (!loader) {
28
+ return Promise.reject(new Error(`[host-runtime/local-loader] Unknown local module "${moduleId}" in registry.`));
29
+ }
30
+ const loadingPromise = loader()
31
+ .then((loaded) => asPluggableApp(moduleId, loaded))
32
+ .catch((error) => {
33
+ localAppPromises.delete(moduleId);
34
+ throw error;
35
+ });
36
+ localAppPromises.set(moduleId, loadingPromise);
37
+ return loadingPromise;
38
+ }
@@ -0,0 +1,13 @@
1
+ import { type PluggableApplicationRegistryItem } from "@gooddata/sdk-model";
2
+ import { type IPluggableApp } from "@gooddata/sdk-pluggable-application-model";
3
+ import { type IAppLifecycleCallbacks } from "../types/lifecycle.js";
4
+ /**
5
+ * Registers app lifecycle callbacks used by the loader (e.g. for preload telemetry).
6
+ * Called by the host or harness at startup.
7
+ *
8
+ * @alpha
9
+ */
10
+ export declare function registerAppLifecycleCallbacks(callbacks: IAppLifecycleCallbacks): void;
11
+ export declare function getAppLifecycleCallbacks(): IAppLifecycleCallbacks | undefined;
12
+ export declare function preloadPluggableApplication(app: PluggableApplicationRegistryItem): void;
13
+ export declare function loadPluggableApplication(app: PluggableApplicationRegistryItem): Promise<IPluggableApp>;
@@ -0,0 +1,55 @@
1
+ // (C) 2026 GoodData Corporation
2
+ import { isExternalPluggableApplicationRegistryItem, isLocalPluggableApplicationRegistryItem, isRemotePluggableApplicationRegistryItem, } from "@gooddata/sdk-model";
3
+ import { now } from "../debug.js";
4
+ import { loadLocalPluggableApplication } from "./localLoader.js";
5
+ import { loadRemotePluggableApplication, preloadRemotePluggableApplication } from "./remoteLoader.js";
6
+ let registeredLifecycle;
7
+ /**
8
+ * Registers app lifecycle callbacks used by the loader (e.g. for preload telemetry).
9
+ * Called by the host or harness at startup.
10
+ *
11
+ * @alpha
12
+ */
13
+ export function registerAppLifecycleCallbacks(callbacks) {
14
+ registeredLifecycle = callbacks;
15
+ }
16
+ export function getAppLifecycleCallbacks() {
17
+ return registeredLifecycle;
18
+ }
19
+ export function preloadPluggableApplication(app) {
20
+ if (isLocalPluggableApplicationRegistryItem(app)) {
21
+ registeredLifecycle?.onPreloadStarted?.(app.id);
22
+ const start = now();
23
+ loadLocalPluggableApplication(app.id)
24
+ .then(() => {
25
+ registeredLifecycle?.onPreloadCompleted?.(app.id, now() - start);
26
+ })
27
+ .catch(() => {
28
+ // Load errors are logged by loadLocalPluggableApplication
29
+ });
30
+ return;
31
+ }
32
+ if (isRemotePluggableApplicationRegistryItem(app)) {
33
+ registeredLifecycle?.onPreloadStarted?.(app.id);
34
+ const start = now();
35
+ preloadRemotePluggableApplication(app.remote)
36
+ .then(() => {
37
+ registeredLifecycle?.onPreloadCompleted?.(app.id, now() - start);
38
+ })
39
+ .catch(() => {
40
+ // Load errors are logged by preloadRemotePluggableApplication
41
+ });
42
+ }
43
+ }
44
+ export async function loadPluggableApplication(app) {
45
+ if (isExternalPluggableApplicationRegistryItem(app)) {
46
+ throw new Error(`[host-runtime/loader] External application "${app.id}" cannot be mounted in PluggableApplicationRenderer.`);
47
+ }
48
+ if (isLocalPluggableApplicationRegistryItem(app)) {
49
+ return loadLocalPluggableApplication(app.id);
50
+ }
51
+ if (isRemotePluggableApplicationRegistryItem(app)) {
52
+ return loadRemotePluggableApplication(app.remote);
53
+ }
54
+ throw new Error(`[host-runtime/loader] Unknown application type for "${JSON.stringify(app)}".`);
55
+ }
@@ -0,0 +1,30 @@
1
+ import { type PluggableApplicationRegistryItem } from "@gooddata/sdk-model";
2
+ import { type IPlatformContext } from "@gooddata/sdk-pluggable-application-model";
3
+ /**
4
+ * Thrown when the current URL does not correspond to any accessible application.
5
+ */
6
+ export declare class AppNotFoundError extends Error {
7
+ constructor(message: string);
8
+ }
9
+ export interface IResolveRedirectTargetOptions {
10
+ /**
11
+ * Fully permission-filtered apps (including workspace permissions).
12
+ * Used for mount validation and as the redirect target when workspace permissions are available.
13
+ */
14
+ apps: PluggableApplicationRegistryItem[];
15
+ ctx: IPlatformContext;
16
+ pathname: string;
17
+ /** Fetches the first workspace ID for the current user. */
18
+ fetchFirstWorkspaceId: () => Promise<string | undefined>;
19
+ }
20
+ /**
21
+ * Resolves where the shell app should navigate given the current URL and permission context.
22
+ *
23
+ * @returns
24
+ * - A URL string → caller must navigate to this URL (e.g. via React Router)
25
+ * - `null` → the current URL is valid; render the active app
26
+ *
27
+ * @throws {AppNotFoundError} The current path maps to no accessible application (show 404)
28
+ * @throws {Error} Unexpected failure (show generic error screen)
29
+ */
30
+ export declare function resolveRedirectTarget({ apps, ctx, pathname, fetchFirstWorkspaceId }: IResolveRedirectTargetOptions): Promise<string | null>;