@gooddata/sdk-ui-pluggable-host 11.54.0-alpha.4 → 11.54.0-alpha.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm/lib/stylesheetRegistry.d.ts +40 -0
- package/esm/lib/stylesheetRegistry.d.ts.map +1 -0
- package/esm/lib/stylesheetRegistry.js +207 -0
- package/esm/loader/failingStorage.d.ts.map +1 -0
- package/esm/loader/pluggableApplicationsLoader.d.ts.map +1 -1
- package/esm/loader/pluggableApplicationsLoader.js +2 -0
- package/esm/ui/PluggableApplicationRenderer.d.ts.map +1 -1
- package/esm/ui/PluggableApplicationRenderer.js +6 -0
- package/esm/ui/resolveHostUiModule.d.ts.map +1 -1
- package/esm/ui/resolveHostUiModule.js +2 -0
- package/package.json +17 -17
- package/esm/loader/test/failingStorage.d.ts.map +0 -1
- /package/esm/loader/{test/failingStorage.d.ts → failingStorage.d.ts} +0 -0
- /package/esm/loader/{test/failingStorage.js → failingStorage.js} +0 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Registers the URL scope of a remote app's stylesheets: every stylesheet link whose
|
|
3
|
+
* href starts with the directory of `remoteEntryUrl` belongs to `appId`.
|
|
4
|
+
*
|
|
5
|
+
* Idempotent. Registering a different URL for a known app replaces its scope and retires the
|
|
6
|
+
* previous build's links, so the old CSS never comes back with the new build. Must be called
|
|
7
|
+
* before the app's module is loaded so late-injected CSS of deactivated apps is caught by the
|
|
8
|
+
* observer.
|
|
9
|
+
*
|
|
10
|
+
* Refused with a warning when the prefix would swallow host stylesheets: a remote entry at the
|
|
11
|
+
* host page's own origin root, or a prefix covering a scope reserved by
|
|
12
|
+
* `reserveHostStylesheetScope`. A refused app is simply not tracked, so its CSS stays applied
|
|
13
|
+
* across navigation. A root on another origin cannot match a host link and is kept.
|
|
14
|
+
*
|
|
15
|
+
* If one remote ever served two pluggable apps from the same base URL, attribution would
|
|
16
|
+
* over-match; the fix would be the per-expose CSS lists in mf-manifest.json
|
|
17
|
+
* (`exposes[].assets.css`).
|
|
18
|
+
*/
|
|
19
|
+
export declare function registerAppStylesheetScope(appId: string, remoteEntryUrl: string): void;
|
|
20
|
+
/**
|
|
21
|
+
* Reserves the URL scope of the host's own remote UI module so no app can claim stylesheets
|
|
22
|
+
* served from the same directory. Must be called before any app registers a scope; the host UI
|
|
23
|
+
* is resolved before an app renders inside it.
|
|
24
|
+
*/
|
|
25
|
+
export declare function reserveHostStylesheetScope(remoteEntryUrl: string): void;
|
|
26
|
+
/**
|
|
27
|
+
* Re-enables all stylesheets attributed to the app. Safe to call before any of the
|
|
28
|
+
* app's CSS exists and repeatedly.
|
|
29
|
+
*/
|
|
30
|
+
export declare function activateAppStylesheets(appId: string): void;
|
|
31
|
+
/**
|
|
32
|
+
* Disables all stylesheets attributed to the app. Idempotent; a no-op scan for an
|
|
33
|
+
* unknown appId.
|
|
34
|
+
*/
|
|
35
|
+
export declare function deactivateAppStylesheets(appId: string): void;
|
|
36
|
+
/**
|
|
37
|
+
* Test-only: clears all registry state and disconnects the head observer.
|
|
38
|
+
*/
|
|
39
|
+
export declare function resetStylesheetRegistry(): void;
|
|
40
|
+
//# sourceMappingURL=stylesheetRegistry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stylesheetRegistry.d.ts","sourceRoot":"","sources":["../../src/lib/stylesheetRegistry.ts"],"names":[],"mappings":"AAkJA;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,0BAA0B,CAAC,KAAK,EAAE,MAAM,EAAE,cAAc,EAAE,MAAM,GAAG,IAAI,CAiCtF;AAED;;;;GAIG;AACH,wBAAgB,0BAA0B,CAAC,cAAc,EAAE,MAAM,GAAG,IAAI,CAQvE;AAED;;;GAGG;AACH,wBAAgB,sBAAsB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAM1D;AAED;;;GAGG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI,CAM5D;AAED;;GAEG;AACH,wBAAgB,uBAAuB,IAAI,IAAI,CAQ9C"}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// (C) 2026 GoodData Corporation
|
|
2
|
+
const scopePrefixes = new Map();
|
|
3
|
+
const reservedHostPrefixes = new Set();
|
|
4
|
+
const deactivatedApps = new Set();
|
|
5
|
+
let linkLoadStates = new WeakMap();
|
|
6
|
+
let retiredLinks = new WeakSet();
|
|
7
|
+
let observer;
|
|
8
|
+
function deriveScopePrefix(remoteEntryUrl) {
|
|
9
|
+
try {
|
|
10
|
+
const url = new URL(remoteEntryUrl, document.baseURI);
|
|
11
|
+
return url.origin + url.pathname.slice(0, url.pathname.lastIndexOf("/") + 1);
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
console.error(`[host-runtime/stylesheet-registry] Cannot derive stylesheet scope from remote URL "${remoteEntryUrl}".`);
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
// The longest match wins, so nested directories cannot make ownership depend on registration order.
|
|
19
|
+
function findOwnerOfHref(href) {
|
|
20
|
+
let owner;
|
|
21
|
+
let longestPrefixLength = -1;
|
|
22
|
+
for (const [appId, prefixes] of scopePrefixes) {
|
|
23
|
+
for (const prefix of prefixes) {
|
|
24
|
+
if (href.startsWith(prefix) && prefix.length > longestPrefixLength) {
|
|
25
|
+
owner = appId;
|
|
26
|
+
longestPrefixLength = prefix.length;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return owner;
|
|
31
|
+
}
|
|
32
|
+
// An app prefix above a reserved host prefix would claim the host's own links.
|
|
33
|
+
function coversReservedHostScope(prefix) {
|
|
34
|
+
for (const reserved of reservedHostPrefixes) {
|
|
35
|
+
if (reserved.startsWith(prefix)) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
function isAppStylesheetLink(node) {
|
|
42
|
+
return node instanceof HTMLLinkElement && node.relList.contains("stylesheet");
|
|
43
|
+
}
|
|
44
|
+
// `link.sheet` says whether a sheet is attached right now, not whether one ever loaded:
|
|
45
|
+
// disabling a link detaches it and re-enabling reattaches it asynchronously. So once this
|
|
46
|
+
// registry has disabled a link, only a remembered flag can still be trusted.
|
|
47
|
+
function hasLoaded(link) {
|
|
48
|
+
if (link.sheet) {
|
|
49
|
+
linkLoadStates.set(link, "loaded");
|
|
50
|
+
}
|
|
51
|
+
return linkLoadStates.get(link) === "loaded";
|
|
52
|
+
}
|
|
53
|
+
function setLinkDisabled(link, disabled) {
|
|
54
|
+
if (!disabled) {
|
|
55
|
+
link.disabled = false;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (hasLoaded(link)) {
|
|
59
|
+
link.disabled = true;
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
if (linkLoadStates.get(link) === "awaiting-load") {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
linkLoadStates.set(link, "awaiting-load");
|
|
66
|
+
// Disabling a link that has never finished loading can suppress its load event, which
|
|
67
|
+
// Vite's preload helper awaits — the pending chunk import would hang forever.
|
|
68
|
+
link.addEventListener("load", () => {
|
|
69
|
+
linkLoadStates.set(link, "loaded");
|
|
70
|
+
// A retired link outlives the scope that owned it, so its owner can no longer be found.
|
|
71
|
+
if (retiredLinks.has(link)) {
|
|
72
|
+
link.disabled = true;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
const owner = findOwnerOfHref(link.href);
|
|
76
|
+
if (owner && deactivatedApps.has(owner)) {
|
|
77
|
+
link.disabled = true;
|
|
78
|
+
}
|
|
79
|
+
}, { once: true });
|
|
80
|
+
}
|
|
81
|
+
function forEachOwnedLink(appId, callback) {
|
|
82
|
+
const prefixes = scopePrefixes.get(appId);
|
|
83
|
+
if (!prefixes?.size) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
document.head.querySelectorAll("link").forEach((link) => {
|
|
87
|
+
if (isAppStylesheetLink(link) && findOwnerOfHref(link.href) === appId) {
|
|
88
|
+
callback(link);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
function handleHeadMutations(mutations) {
|
|
93
|
+
for (const mutation of mutations) {
|
|
94
|
+
mutation.addedNodes.forEach((node) => {
|
|
95
|
+
if (!isAppStylesheetLink(node)) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const owner = findOwnerOfHref(node.href);
|
|
99
|
+
if (owner && deactivatedApps.has(owner)) {
|
|
100
|
+
setLinkDisabled(node, true);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function ensureObserver() {
|
|
106
|
+
if (observer) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
observer = new MutationObserver(handleHeadMutations);
|
|
110
|
+
observer.observe(document.head, { childList: true });
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Registers the URL scope of a remote app's stylesheets: every stylesheet link whose
|
|
114
|
+
* href starts with the directory of `remoteEntryUrl` belongs to `appId`.
|
|
115
|
+
*
|
|
116
|
+
* Idempotent. Registering a different URL for a known app replaces its scope and retires the
|
|
117
|
+
* previous build's links, so the old CSS never comes back with the new build. Must be called
|
|
118
|
+
* before the app's module is loaded so late-injected CSS of deactivated apps is caught by the
|
|
119
|
+
* observer.
|
|
120
|
+
*
|
|
121
|
+
* Refused with a warning when the prefix would swallow host stylesheets: a remote entry at the
|
|
122
|
+
* host page's own origin root, or a prefix covering a scope reserved by
|
|
123
|
+
* `reserveHostStylesheetScope`. A refused app is simply not tracked, so its CSS stays applied
|
|
124
|
+
* across navigation. A root on another origin cannot match a host link and is kept.
|
|
125
|
+
*
|
|
126
|
+
* If one remote ever served two pluggable apps from the same base URL, attribution would
|
|
127
|
+
* over-match; the fix would be the per-expose CSS lists in mf-manifest.json
|
|
128
|
+
* (`exposes[].assets.css`).
|
|
129
|
+
*/
|
|
130
|
+
export function registerAppStylesheetScope(appId, remoteEntryUrl) {
|
|
131
|
+
if (typeof document === "undefined") {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const prefix = deriveScopePrefix(remoteEntryUrl);
|
|
135
|
+
if (!prefix) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (prefix === `${new URL(document.baseURI).origin}/`) {
|
|
139
|
+
console.warn(`[host-runtime/stylesheet-registry] Not tracking stylesheets of app "${appId}": its remote URL "${remoteEntryUrl}" resolves to the host page origin root, where its CSS cannot be told apart from the host's own.`);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
if (coversReservedHostScope(prefix)) {
|
|
143
|
+
console.warn(`[host-runtime/stylesheet-registry] Not tracking stylesheets of app "${appId}": its remote URL "${remoteEntryUrl}" covers the host UI module's directory, whose CSS must stay applied.`);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const prefixes = scopePrefixes.get(appId);
|
|
147
|
+
if (!prefixes?.has(prefix)) {
|
|
148
|
+
if (prefixes?.size) {
|
|
149
|
+
// A new remote URL for a known app means its previous build is gone. Those links stay
|
|
150
|
+
// in the document, so they must be disabled before the scope stops covering them.
|
|
151
|
+
forEachOwnedLink(appId, (link) => {
|
|
152
|
+
retiredLinks.add(link);
|
|
153
|
+
setLinkDisabled(link, true);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
scopePrefixes.set(appId, new Set([prefix]));
|
|
157
|
+
}
|
|
158
|
+
ensureObserver();
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Reserves the URL scope of the host's own remote UI module so no app can claim stylesheets
|
|
162
|
+
* served from the same directory. Must be called before any app registers a scope; the host UI
|
|
163
|
+
* is resolved before an app renders inside it.
|
|
164
|
+
*/
|
|
165
|
+
export function reserveHostStylesheetScope(remoteEntryUrl) {
|
|
166
|
+
if (typeof document === "undefined") {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const prefix = deriveScopePrefix(remoteEntryUrl);
|
|
170
|
+
if (prefix) {
|
|
171
|
+
reservedHostPrefixes.add(prefix);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Re-enables all stylesheets attributed to the app. Safe to call before any of the
|
|
176
|
+
* app's CSS exists and repeatedly.
|
|
177
|
+
*/
|
|
178
|
+
export function activateAppStylesheets(appId) {
|
|
179
|
+
if (typeof document === "undefined") {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
deactivatedApps.delete(appId);
|
|
183
|
+
forEachOwnedLink(appId, (link) => setLinkDisabled(link, false));
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Disables all stylesheets attributed to the app. Idempotent; a no-op scan for an
|
|
187
|
+
* unknown appId.
|
|
188
|
+
*/
|
|
189
|
+
export function deactivateAppStylesheets(appId) {
|
|
190
|
+
if (typeof document === "undefined") {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
deactivatedApps.add(appId);
|
|
194
|
+
forEachOwnedLink(appId, (link) => setLinkDisabled(link, true));
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Test-only: clears all registry state and disconnects the head observer.
|
|
198
|
+
*/
|
|
199
|
+
export function resetStylesheetRegistry() {
|
|
200
|
+
scopePrefixes.clear();
|
|
201
|
+
reservedHostPrefixes.clear();
|
|
202
|
+
deactivatedApps.clear();
|
|
203
|
+
linkLoadStates = new WeakMap();
|
|
204
|
+
retiredLinks = new WeakSet();
|
|
205
|
+
observer?.disconnect();
|
|
206
|
+
observer = undefined;
|
|
207
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"failingStorage.d.ts","sourceRoot":"","sources":["../../src/loader/failingStorage.ts"],"names":[],"mappings":"AAEA,KAAK,aAAa,GAAG,SAAS,GAAG,SAAS,GAAG,YAAY,CAAC;AAE1D;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAC9B,MAAM,EAAE,aAAa,EACrB,GAAG,EAAE,MAAM,IAAI,EACf,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GAClC,MAAM,CAiCR"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pluggableApplicationsLoader.d.ts","sourceRoot":"","sources":["../../src/loader/pluggableApplicationsLoader.ts"],"names":[],"mappings":"AAEA,OAAO,EACH,KAAK,gCAAgC,EAIxC,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,2CAA2C,CAAC;
|
|
1
|
+
{"version":3,"file":"pluggableApplicationsLoader.d.ts","sourceRoot":"","sources":["../../src/loader/pluggableApplicationsLoader.ts"],"names":[],"mappings":"AAEA,OAAO,EACH,KAAK,gCAAgC,EAIxC,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,KAAK,aAAa,EAAE,MAAM,2CAA2C,CAAC;AAI/E,OAAO,EAAE,KAAK,sBAAsB,EAAE,MAAM,uBAAuB,CAAC;AAOpE;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,SAAS,EAAE,sBAAsB,GAAG,IAAI,CAErF;AAED,wBAAgB,wBAAwB,IAAI,sBAAsB,GAAG,SAAS,CAE7E;AASD;;;;;;;;;;;GAWG;AACH,wBAAgB,iBAAiB,IAAI,OAAO,CAgB3C;AAED,wBAAgB,2BAA2B,CAAC,GAAG,EAAE,gCAAgC,GAAG,IAAI,CA2BvF;AAED,wBAAsB,wBAAwB,CAC1C,GAAG,EAAE,gCAAgC,GACtC,OAAO,CAAC,aAAa,CAAC,CAiBxB"}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// (C) 2026 GoodData Corporation
|
|
2
2
|
import { isExternalPluggableApplicationRegistryItem, isLocalPluggableApplicationRegistryItem, isRemotePluggableApplicationRegistryItem, } from "@gooddata/sdk-model";
|
|
3
3
|
import { now } from "../debug.js";
|
|
4
|
+
import { registerAppStylesheetScope } from "../lib/stylesheetRegistry.js";
|
|
4
5
|
import { loadLocalPluggableApplication } from "./localLoader.js";
|
|
5
6
|
import { loadRemotePluggableApplication, preloadRemotePluggableApplication } from "./remoteLoader.js";
|
|
6
7
|
let registeredLifecycle;
|
|
@@ -77,6 +78,7 @@ export async function loadPluggableApplication(app) {
|
|
|
77
78
|
return loadLocalPluggableApplication(app.id);
|
|
78
79
|
}
|
|
79
80
|
if (isRemotePluggableApplicationRegistryItem(app)) {
|
|
81
|
+
registerAppStylesheetScope(app.id, app.remote.url);
|
|
80
82
|
return loadRemotePluggableApplication(app.remote);
|
|
81
83
|
}
|
|
82
84
|
throw new Error(`[host-runtime/loader] Unknown application type for "${JSON.stringify(app)}".`);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PluggableApplicationRenderer.d.ts","sourceRoot":"","sources":["../../src/ui/PluggableApplicationRenderer.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,SAAS,EAAsE,MAAM,OAAO,CAAC;AAI3G,OAAO,EAAE,KAAK,iBAAiB,EAAE,KAAK,gCAAgC,EAAE,MAAM,qBAAqB,CAAC;AACpG,OAAO,EACH,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,EAC3B,KAAK,gBAAgB,EASxB,MAAM,2CAA2C,CAAC;
|
|
1
|
+
{"version":3,"file":"PluggableApplicationRenderer.d.ts","sourceRoot":"","sources":["../../src/ui/PluggableApplicationRenderer.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,SAAS,EAAsE,MAAM,OAAO,CAAC;AAI3G,OAAO,EAAE,KAAK,iBAAiB,EAAE,KAAK,gCAAgC,EAAE,MAAM,qBAAqB,CAAC;AACpG,OAAO,EACH,KAAK,iBAAiB,EACtB,KAAK,sBAAsB,EAC3B,KAAK,gBAAgB,EASxB,MAAM,2CAA2C,CAAC;AAgBnD,OAAO,qCAAqC,CAAC;AAC7C,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAkBhD,MAAM,WAAW,kCAAkC;IAC/C,GAAG,EAAE,gCAAgC,CAAC;IACtC,GAAG,EAAE,gBAAgB,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,sFAAsF;IACtF,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,6FAA6F;IAC7F,iBAAiB,CAAC,EAAE,CAChB,QAAQ,CAAC,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,MAAM,EAChB,WAAW,CAAC,EAAE,iBAAiB,EAC/B,YAAY,CAAC,EAAE,OAAO,EACtB,kBAAkB,CAAC,EAAE,OAAO,KAC3B,IAAI,CAAC;IACV,8DAA8D;IAC9D,kBAAkB,CAAC,EAAE,MAAM,IAAI,CAAC;IAChC,oHAAoH;IACpH,oBAAoB,CAAC,EAAE,CAAC,OAAO,EAAE;QAC7B,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;QACvB,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;QACvB,cAAc,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;QAClC,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;QACnB,WAAW,CAAC,EAAE,iBAAiB,CAAC;KACnC,KAAK,IAAI,CAAC;IACX;;;OAGG;IACH,qBAAqB,CAAC,EAAE,SAAS,CAC7B,CAAC,CAAC,IAAI,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,EAAE,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,OAAO,CAAC,GAAG,SAAS,CACtG,CAAC;IACF;;;OAGG;IACH,iBAAiB,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,KAAK,EAAE,cAAc,KAAK,IAAI,CAAC,GAAG,SAAS,CAAC,CAAC;IAC7E;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,OAAO,EAAE,sBAAsB,KAAK,OAAO,CAAC,GAAG,SAAS,CAAC,CAAC;IAC7F,cAAc,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,KAAK,IAAI,CAAC;IACpE,qBAAqB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,GAAG,SAAS,KAAK,IAAI,CAAC;CAClF;AAED,wBAAgB,4BAA4B,CAAC,EACzC,GAAG,EACH,GAAG,EACH,QAAQ,EACR,eAAe,EACf,iBAAiB,EACjB,kBAAkB,EAClB,oBAAoB,EACpB,qBAAqB,EACrB,iBAAiB,EACjB,oBAAoB,EACpB,cAAc,EACd,qBAAqB,EACxB,EAAE,kCAAkC,2CA4TpC"}
|
|
@@ -7,6 +7,7 @@ import { LoadingComponent, useAutoupdateRef } from "@gooddata/sdk-ui";
|
|
|
7
7
|
import { bemFactory } from "@gooddata/sdk-ui-kit";
|
|
8
8
|
import { now } from "../debug.js";
|
|
9
9
|
import { setActiveAppAttribute } from "../lib/activeAppAttribute.js";
|
|
10
|
+
import { activateAppStylesheets, deactivateAppStylesheets } from "../lib/stylesheetRegistry.js";
|
|
10
11
|
import { getSecuredRemoteAppValidUntil, validateAppSecurity, } from "../loader/appSecurityValidation.js";
|
|
11
12
|
import { getAppLifecycleCallbacks, loadPluggableApplication } from "../loader/pluggableApplicationsLoader.js";
|
|
12
13
|
import { getApplicationHref } from "../loader/routing.js";
|
|
@@ -91,6 +92,7 @@ export function PluggableApplicationRenderer({ app, ctx, pathname, aiAssistantOp
|
|
|
91
92
|
mountHandleRef.current = undefined;
|
|
92
93
|
mountedAppRef.current = undefined;
|
|
93
94
|
setViewState({ state: "loading" });
|
|
95
|
+
activateAppStylesheets(app.id);
|
|
94
96
|
// unmount() is synchronous, so calling it inline can re-enter React while the parent render
|
|
95
97
|
// that swapped this application out is still in progress.
|
|
96
98
|
if (prevHandle) {
|
|
@@ -108,6 +110,7 @@ export function PluggableApplicationRenderer({ app, ctx, pathname, aiAssistantOp
|
|
|
108
110
|
const message = intlRef.current.formatMessage(SECURITY_FAILURE_MESSAGES[securityFailure.kind]);
|
|
109
111
|
console.error(`[host-runtime/renderer] Refusing to mount app "${app.id}": ${securityFailure.kind}.`, securityFailure);
|
|
110
112
|
lifecycle?.onLoadFailed?.(app.id, securityFailure.kind);
|
|
113
|
+
deactivateAppStylesheets(app.id);
|
|
111
114
|
setViewState({ state: "error", message });
|
|
112
115
|
return;
|
|
113
116
|
}
|
|
@@ -149,6 +152,7 @@ export function PluggableApplicationRenderer({ app, ctx, pathname, aiAssistantOp
|
|
|
149
152
|
const errorMessage = mountError instanceof Error ? mountError.message : "Unknown module loading error.";
|
|
150
153
|
console.error(`[host-runtime/renderer] Failed to mount app "${app.id}".`, mountError);
|
|
151
154
|
lifecycle?.onLoadFailed?.(app.id, errorMessage);
|
|
155
|
+
deactivateAppStylesheets(app.id);
|
|
152
156
|
setViewState({
|
|
153
157
|
state: "error",
|
|
154
158
|
message: errorMessage,
|
|
@@ -160,6 +164,7 @@ export function PluggableApplicationRenderer({ app, ctx, pathname, aiAssistantOp
|
|
|
160
164
|
if (navigationRequestRef?.current === navigationRequest) {
|
|
161
165
|
navigationRequestRef.current = undefined;
|
|
162
166
|
}
|
|
167
|
+
deactivateAppStylesheets(app.id);
|
|
163
168
|
const handle = mountHandleRef.current;
|
|
164
169
|
if (handle) {
|
|
165
170
|
mountHandleRef.current = undefined;
|
|
@@ -200,6 +205,7 @@ export function PluggableApplicationRenderer({ app, ctx, pathname, aiAssistantOp
|
|
|
200
205
|
if (navigationRequestRef) {
|
|
201
206
|
navigationRequestRef.current = undefined;
|
|
202
207
|
}
|
|
208
|
+
deactivateAppStylesheets(mounted.app.id);
|
|
203
209
|
handle.unmount();
|
|
204
210
|
lifecycle?.onUnmounted?.(mounted.app.id);
|
|
205
211
|
lifecycle?.onLoadFailed?.(mounted.app.id, securityFailure.kind);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"resolveHostUiModule.d.ts","sourceRoot":"","sources":["../../src/ui/resolveHostUiModule.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,gBAAgB,EAAE,KAAK,aAAa,EAAE,MAAM,2CAA2C,CAAC;
|
|
1
|
+
{"version":3,"file":"resolveHostUiModule.d.ts","sourceRoot":"","sources":["../../src/ui/resolveHostUiModule.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,KAAK,gBAAgB,EAAE,KAAK,aAAa,EAAE,MAAM,2CAA2C,CAAC;AAQtG;;;;;GAKG;AACH,wBAAsB,mBAAmB,CAAC,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,aAAa,CAAC,CAgBvF"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// (C) 2026 GoodData Corporation
|
|
2
|
+
import { reserveHostStylesheetScope } from "../lib/stylesheetRegistry.js";
|
|
2
3
|
import { loadRemoteHostUiModule } from "../loader/remoteLoader.js";
|
|
3
4
|
import { getRemoteRegistry } from "../registry/pluggableApplicationsRegistry.js";
|
|
4
5
|
import { defaultHostUiModule } from "./DefaultHostUi.js";
|
|
@@ -11,6 +12,7 @@ import { defaultHostUiModule } from "./DefaultHostUi.js";
|
|
|
11
12
|
export async function resolveHostUiModule(ctx) {
|
|
12
13
|
const remoteRegistry = getRemoteRegistry(ctx);
|
|
13
14
|
if (remoteRegistry?.uiModule) {
|
|
15
|
+
reserveHostStylesheetScope(remoteRegistry.uiModule.url);
|
|
14
16
|
try {
|
|
15
17
|
return await loadRemoteHostUiModule(remoteRegistry.uiModule);
|
|
16
18
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gooddata/sdk-ui-pluggable-host",
|
|
3
|
-
"version": "11.54.0-alpha.
|
|
3
|
+
"version": "11.54.0-alpha.5",
|
|
4
4
|
"description": "GoodData SDK runtime for hosting pluggable applications — registry, loader, routing, platform context, default UI chrome",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "GoodData Corporation",
|
|
@@ -29,20 +29,20 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@module-federation/runtime": "2.6.0",
|
|
31
31
|
"lodash-es": "^4.17.23",
|
|
32
|
-
"@gooddata/sdk-backend-base": "11.54.0-alpha.
|
|
33
|
-
"@gooddata/sdk-
|
|
34
|
-
"@gooddata/sdk-backend-
|
|
35
|
-
"@gooddata/sdk-
|
|
36
|
-
"@gooddata/sdk-
|
|
37
|
-
"@gooddata/sdk-pluggable-application-model": "11.54.0-alpha.
|
|
38
|
-
"@gooddata/sdk-ui": "11.54.0-alpha.
|
|
39
|
-
"@gooddata/sdk-ui-application-header": "11.54.0-alpha.
|
|
40
|
-
"@gooddata/sdk-ui-
|
|
41
|
-
"@gooddata/sdk-ui-
|
|
42
|
-
"@gooddata/sdk-ui-
|
|
43
|
-
"@gooddata/
|
|
44
|
-
"@gooddata/sdk-ui-
|
|
45
|
-
"@gooddata/
|
|
32
|
+
"@gooddata/sdk-backend-base": "11.54.0-alpha.5",
|
|
33
|
+
"@gooddata/sdk-backend-spi": "11.54.0-alpha.5",
|
|
34
|
+
"@gooddata/sdk-backend-tiger": "11.54.0-alpha.5",
|
|
35
|
+
"@gooddata/sdk-embedding": "11.54.0-alpha.5",
|
|
36
|
+
"@gooddata/sdk-model": "11.54.0-alpha.5",
|
|
37
|
+
"@gooddata/sdk-pluggable-application-model": "11.54.0-alpha.5",
|
|
38
|
+
"@gooddata/sdk-ui": "11.54.0-alpha.5",
|
|
39
|
+
"@gooddata/sdk-ui-application-header": "11.54.0-alpha.5",
|
|
40
|
+
"@gooddata/sdk-ui-ext": "11.54.0-alpha.5",
|
|
41
|
+
"@gooddata/sdk-ui-gen-ai": "11.54.0-alpha.5",
|
|
42
|
+
"@gooddata/sdk-ui-theme-provider": "11.54.0-alpha.5",
|
|
43
|
+
"@gooddata/util": "11.54.0-alpha.5",
|
|
44
|
+
"@gooddata/sdk-ui-semantic-search": "11.54.0-alpha.5",
|
|
45
|
+
"@gooddata/sdk-ui-kit": "11.54.0-alpha.5"
|
|
46
46
|
},
|
|
47
47
|
"devDependencies": {
|
|
48
48
|
"@microsoft/api-documenter": "^7.17.0",
|
|
@@ -85,8 +85,8 @@
|
|
|
85
85
|
"vite": "8.0.16",
|
|
86
86
|
"vitest": "4.1.8",
|
|
87
87
|
"vitest-dom": "0.1.1",
|
|
88
|
-
"@gooddata/
|
|
89
|
-
"@gooddata/
|
|
88
|
+
"@gooddata/eslint-config": "11.54.0-alpha.5",
|
|
89
|
+
"@gooddata/oxlint-config": "11.54.0-alpha.5"
|
|
90
90
|
},
|
|
91
91
|
"peerDependencies": {
|
|
92
92
|
"react": ">=18.3.1",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"failingStorage.d.ts","sourceRoot":"","sources":["../../../src/loader/test/failingStorage.ts"],"names":[],"mappings":"AAEA,KAAK,aAAa,GAAG,SAAS,GAAG,SAAS,GAAG,YAAY,CAAC;AAE1D;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAC9B,MAAM,EAAE,aAAa,EACrB,GAAG,EAAE,MAAM,IAAI,EACf,IAAI,GAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAM,GAClC,MAAM,CAiCR"}
|
|
File without changes
|
|
File without changes
|