1
import * as api from "./api.js";
2
3
+/**
4
+ * @typedef {Object} WebuiExtension
5
+ * @property {string} path
6
+ */
7
+
8
+
9
+
10
+/**
11
+ * @typedef {Object} LoadWebuiExtensionsResponse
12
+ * @property {WebuiExtension[]} extensions
13
+ */
14
+
15
+/**
16
+ * @typedef {Object} JsExtensionImport
17
+ * @property {string} path
18
+ * @property {{ default: (data: any) => (void|Promise<void>) }} module
19
+ */
20
+
21
+/** @type {Map<string, JsExtensionImport[]>} */
22
+const jsExtensionsCache = new Map();
23
+
24
+/** @type {Map<string, string>} */
25
+const htmlExtensionsCache = new Map();
26
+
27
+export function invalidateCache() {
28
+ jsExtensionsCache.clear();
29
+ htmlExtensionsCache.clear();
30
+}
31
+
32
+/**
33
+ * Call all JS extensions for a given extension point.
34
+ *
35
+ * @param {string} extensionPoint
36
+ * @param {any} data
37
+ * @returns {Promise<void>}
38
+ */
39
+export async function callJsExtensions(extensionPoint, data){
40
+ const extensions = jsExtensionsCache.get(extensionPoint) || await loadJsExtensions(extensionPoint);
41
+ for(const extension of extensions){
42
+ try{
43
+ await extension.module.default(data);
44
+ }catch(error){
45
+ console.error(`Error calling extension: ${extension.path}`, error);
46
+ }
47
+ }
48
+}
49
+
50
+/**
51
+ * Load JS extension modules for an extension point.
52
+ *
53
+ * @param {string} extensionPoint
54
+ * @returns {Promise<JsExtensionImport[]>}
55
+ */
56
+export async function loadJsExtensions(extensionPoint) {
57
+ try {
58
+ /** @type {LoadWebuiExtensionsResponse} */
59
+ const response = await api.callJsonApi(`/api/load_webui_extensions`, {
60
+ extension_point: extensionPoint,
61
+ filters: ["*.js", "*.mjs"],
62
+ });
63
+ /** @type {JsExtensionImport[]} */
64
+ const imports = await Promise.all(
65
+ response.extensions.map(async extension => ({
66
+ path: extension.path,
67
+ module: await import(normalizePath(extension.path))
68
+ }))
69
+ );
70
+ jsExtensionsCache.set(extensionPoint, imports);
71
+ return imports;
72
+ } catch (error) {
73
+ console.error("Error loading JS extensions:", error);
74
+ return [];
75
+ }
76
+}
77
+
78
// Load all x-component tags starting from root elements
4
-export async function loadExtensions(roots = [document.documentElement]) {
79
+/**
80
+ * Load and render all HTML extensions in the given DOM roots.
81
+ *
82
+ * @param {Element | Document | Array<Element | Document>} [roots]
83
+ * @returns {Promise<void>}
84
+ */
85
+export async function loadHtmlExtensions(roots = [document.documentElement]) {
86
try {
87
// Convert single root to array if needed
88
+ /** @type {Array<Element | Document>} */
89
const rootElements = Array.isArray(roots) ? roots : [roots];
90
91
// Find all top-level components and load them in parallel
92
+ /** @type {Element[]} */
93
const extensions = rootElements.flatMap((root) =>
94
Array.from(root.querySelectorAll("x-extension")),
95
);
103
console.error("x-extension missing id attribute:", extension);
104
return;
105
}
23
- await importExtensions(path, extension);
106
+ await importHtmlExtensions(path, /** @type {HTMLElement} */ (extension));
107
}),
108
);
109
} catch (error) {
27
- console.error("Error loading extensions:", error);
110
+ console.error("Error loading HTML extensions:", error);
111
}
112
}
113
114
// import all extensions for extension point via backend api
32
-export async function importExtensions(extensionPointId, targetElement) {
115
+/**
116
+ * Import all HTML extensions for an extension point and inject them as `<x-component>` tags.
117
+ *
118
+ * @param {string} extensionPoint
119
+ * @param {HTMLElement} targetElement
120
+ * @returns {Promise<void>}
121
+ */
122
+export async function importHtmlExtensions(extensionPoint, targetElement) {
123
try {
124
+ const cachedHtml = htmlExtensionsCache.get(extensionPoint);
125
+ if (cachedHtml != null) {
126
+ targetElement.innerHTML = cachedHtml;
127
+ return;
128
+ }
129
+
130
+ /** @type {LoadWebuiExtensionsResponse} */
131
const response = await api.callJsonApi(`/api/load_webui_extensions`, {
35
- extension_point: extensionPointId,
132
+ extension_point: extensionPoint,
133
+ filters: ["*.html", "*.htm", "*.xhtml"],
134
});
135
let combinedHTML = "";
136
for (const extension of response.extensions) {
39
- combinedHTML += extension.html.trim();
137
+ const path = normalizePath(extension.path);
138
+ combinedHTML += `<x-component path="${path}"></x-component>`;
139
}
140
+ htmlExtensionsCache.set(extensionPoint, combinedHTML);
141
targetElement.innerHTML = combinedHTML;
142
} catch (error) {
43
- console.error("Error importing extensions:", error);
143
+ console.error("Error importing HTML extensions:", error);
144
+ return [];
145
}
146
}
147
148
+/**
149
+ * @param {string} path
150
+ * @returns {string}
151
+ */
152
+function normalizePath(path) {
153
+ return path.startsWith("/") ? path : "/" + path;
154
+}
155
+
156
// Watch for DOM changes to dynamically load x-extensions
157
+/** @type {MutationCallback} */
158
const extensionObserver = new MutationObserver((mutations) => {
159
for (const mutation of mutations) {
160
for (const node of mutation.addedNodes) {
161
if (node.nodeType === 1) {
162
// ELEMENT_NODE
163
// Check if this node or its descendants contain x-extension(s)
54
- if (node.matches?.("x-extension")) {
55
- importExtensions(node.getAttribute("id"), node);
164
+ const el = /** @type {Element} */ (node);
165
+ if (el.matches?.("x-extension")) {
166
+ const id = el.getAttribute("id");
167
+ if (id) importHtmlExtensions(id, /** @type {HTMLElement} */ (el));
168
} else if (node.querySelectorAll) {
57
- loadExtensions([node]);
169
+ loadHtmlExtensions([node]);
170
}
171
}
172
}