homebridge-plugin-utils 2.1.0 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/build/tsconfig.json +15 -0
- package/dist/cli/index.d.ts +20 -3
- package/dist/cli/index.js +77 -7
- package/dist/cli/index.js.map +1 -1
- package/dist/docChrome.d.ts +4 -1
- package/dist/docChrome.js +3 -0
- package/dist/docChrome.js.map +1 -1
- package/dist/ffmpeg/fmp4-builders.d.ts +77 -0
- package/dist/ffmpeg/fmp4-builders.js +163 -0
- package/dist/ffmpeg/fmp4-builders.js.map +1 -0
- package/dist/ffmpeg/index.d.ts +1 -0
- package/dist/ffmpeg/index.js +1 -0
- package/dist/ffmpeg/index.js.map +1 -1
- package/dist/ffmpeg/options.d.ts +17 -0
- package/dist/ffmpeg/options.js +47 -22
- package/dist/ffmpeg/options.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/timer-registry.d.ts +100 -0
- package/dist/timer-registry.js +184 -0
- package/dist/timer-registry.js.map +1 -0
- package/dist/ui/webUi-featureOptions/state.mjs +68 -11
- package/dist/ui/webUi-featureOptions/utils.mjs +16 -5
- package/dist/ui/webUi-featureOptions/views/connectionError.mjs +27 -10
- package/dist/ui/webUi-featureOptions/views/deviceInfo.mjs +7 -0
- package/dist/ui/webUi-featureOptions/views/header.mjs +22 -5
- package/dist/ui/webUi-featureOptions/views/nav.mjs +31 -38
- package/dist/ui/webUi-featureOptions/views/options.mjs +11 -5
- package/dist/ui/webUi-featureOptions.mjs +66 -43
- package/dist/ui/webUi.mjs +5 -0
- package/dist/util.d.ts +29 -4
- package/dist/util.js +34 -0
- package/dist/util.js.map +1 -1
- package/dist/webui-loader.d.ts +80 -0
- package/dist/webui-loader.js +373 -0
- package/dist/webui-loader.js.map +1 -0
- package/package.json +4 -4
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
/* Copyright(C) 2017-2026, HJD (https://github.com/hjdhjd). All rights reserved.
|
|
2
|
+
*
|
|
3
|
+
* webui-loader.ts: The marker-gated stamp for a plugin's webUI boot region.
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* The webUI boot-region stamp. A plugin's `index.html` carries a marker-fenced region that homebridge-plugin-utils generates and every build re-stamps. The region
|
|
7
|
+
* reports its own boot failures on the page, injects an importmap mapping the bare package specifier to the hashed-versioned subdir the `prepare-ui` CLI mirrors into
|
|
8
|
+
* place, and dynamically imports the plugin's entry module. It is identical across the family, so this module renders it from one template: the plugin declares its
|
|
9
|
+
* entry and cache-bust list in a config comment, and `prepare-ui` stamps the rendered region into the marker-fenced block on every build.
|
|
10
|
+
*
|
|
11
|
+
* The region carries three pieces in boot order. A hidden status panel a screenshot turns into a complete support report. A boot monitor - a classic inline script, so
|
|
12
|
+
* it runs during HTML parse ahead of any deferred module evaluation - that owns the on-page failure surface: it reveals the panel with a plain-language message
|
|
13
|
+
* classified by the boot stage that failed, arms a ten-second watchdog for a boot that hangs, and stands down when the app signals it rendered. And the loader - the
|
|
14
|
+
* importmap/cache-bust module script - stage-instrumented so a fetch, importmap, or entry-import failure routes to the monitor with the stage that failed. The
|
|
15
|
+
* classic-versus-module split is for execution ordering alone, not old-engine compatibility: the classic script runs during parse while the module script is deferred.
|
|
16
|
+
*
|
|
17
|
+
* Three exported pieces: the marked-region marker constants (following the family's uniform template - see `docChrome.ts`), {@link parseWebUiLoaderConfig} which reads
|
|
18
|
+
* and validates the plugin's config comment, and {@link renderWebUiBootRegion} which renders the region from that config plus the destination and package facts the CLI
|
|
19
|
+
* supplies. The CLI's `prepareUi` owns the read/splice/atomic-write around them.
|
|
20
|
+
*
|
|
21
|
+
* @module
|
|
22
|
+
*/
|
|
23
|
+
// The config-comment prefix and terminator. The plugin declares its loader config in a single HTML comment - `<!-- WEBUI LOADER CONFIG {...} -->` - placed OUTSIDE the
|
|
24
|
+
// stamped marker pair, so it survives every stamp. The prefix is distinct from the BEGIN marker below (`... LOADER CONFIG ` versus `... LOADER:BEGIN`), so a search for
|
|
25
|
+
// one never matches the other.
|
|
26
|
+
const CONFIG_PREFIX = "<!-- WEBUI LOADER CONFIG ";
|
|
27
|
+
const COMMENT_TERMINATOR = "-->";
|
|
28
|
+
/**
|
|
29
|
+
* The opening marker of the auto-generated webUI boot region. Follows the family's uniform marked-region template (see `docChrome.ts`); the text doubles as an
|
|
30
|
+
* in-document warning not to edit the region by hand.
|
|
31
|
+
*
|
|
32
|
+
* @category WebUI Loader
|
|
33
|
+
*/
|
|
34
|
+
export const WEBUI_LOADER_BEGIN = "<!-- WEBUI LOADER:BEGIN - Auto-generated by homebridge-plugin-utils. Do not edit this region by hand. -->";
|
|
35
|
+
/**
|
|
36
|
+
* The closing marker of the auto-generated webUI boot region. See {@link WEBUI_LOADER_BEGIN}.
|
|
37
|
+
*
|
|
38
|
+
* @category WebUI Loader
|
|
39
|
+
*/
|
|
40
|
+
export const WEBUI_LOADER_END = "<!-- WEBUI LOADER:END -->";
|
|
41
|
+
/**
|
|
42
|
+
* Read and validate the plugin's webUI loader config comment from an `index.html`. The comment is the plugin's sole declaration - its entry module and its cache-bust
|
|
43
|
+
* list - and everything else about the rendered region is a family constant or a fact the CLI supplies, so this is where a mistake in that declaration surfaces with a
|
|
44
|
+
* message naming the file and the defect.
|
|
45
|
+
*
|
|
46
|
+
* Rejects, each with a framed error naming `htmlPath`: an absent config comment, a duplicated one (the region it drives would be ambiguous), an unterminated one,
|
|
47
|
+
* invalid JSON, a mis-shaped object (a missing/non-string `entry`, or a `bust` that is not a string array), and a config comment sitting INSIDE the marked region (the
|
|
48
|
+
* first stamp would erase it, so it must live outside the marker pair).
|
|
49
|
+
*
|
|
50
|
+
* @param html - The full `index.html` text.
|
|
51
|
+
* @param htmlPath - The path the html was read from, named in every diagnostic.
|
|
52
|
+
*
|
|
53
|
+
* @returns The validated {@link WebUiLoaderConfig}.
|
|
54
|
+
*
|
|
55
|
+
* @throws `Error` naming `htmlPath` and the defect for an absent, duplicated, unterminated, invalid-JSON, mis-shaped, or inside-the-region config comment.
|
|
56
|
+
*
|
|
57
|
+
* @category WebUI Loader
|
|
58
|
+
*/
|
|
59
|
+
export function parseWebUiLoaderConfig(html, htmlPath) {
|
|
60
|
+
const configIndex = html.indexOf(CONFIG_PREFIX);
|
|
61
|
+
if (configIndex === -1) {
|
|
62
|
+
throw new Error("webui-loader: no `<!-- WEBUI LOADER CONFIG ... -->` comment found in " + htmlPath + ".");
|
|
63
|
+
}
|
|
64
|
+
// A duplicated config comment leaves the loader config ambiguous - which entry/bust set drives the stamp is undefined - so reject it the same way the splice rejects
|
|
65
|
+
// duplicated markers.
|
|
66
|
+
if (html.includes(CONFIG_PREFIX, configIndex + CONFIG_PREFIX.length)) {
|
|
67
|
+
throw new Error("webui-loader: multiple `WEBUI LOADER CONFIG` comments found in " + htmlPath + "; the loader config is ambiguous.");
|
|
68
|
+
}
|
|
69
|
+
// A config comment inside the marked region would be erased by the first stamp, so it must sit outside the marker pair. We position-check it against the marker
|
|
70
|
+
// indices; when the markers are absent the check is inert (the stamp step gates on the BEGIN marker's presence before this runs).
|
|
71
|
+
const beginIndex = html.indexOf(WEBUI_LOADER_BEGIN);
|
|
72
|
+
const endIndex = html.indexOf(WEBUI_LOADER_END);
|
|
73
|
+
if ((beginIndex !== -1) && (endIndex !== -1) && (configIndex > beginIndex) && (configIndex < endIndex)) {
|
|
74
|
+
throw new Error("webui-loader: the `WEBUI LOADER CONFIG` comment sits inside the marked region in " + htmlPath + "; move it outside the marker pair.");
|
|
75
|
+
}
|
|
76
|
+
const jsonStart = configIndex + CONFIG_PREFIX.length;
|
|
77
|
+
const commentEnd = html.indexOf(COMMENT_TERMINATOR, jsonStart);
|
|
78
|
+
if (commentEnd === -1) {
|
|
79
|
+
throw new Error("webui-loader: the `WEBUI LOADER CONFIG` comment in " + htmlPath + " is not terminated.");
|
|
80
|
+
}
|
|
81
|
+
const rawJson = html.slice(jsonStart, commentEnd).trim();
|
|
82
|
+
let parsed;
|
|
83
|
+
try {
|
|
84
|
+
parsed = JSON.parse(rawJson);
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
throw new Error("webui-loader: the `WEBUI LOADER CONFIG` comment in " + htmlPath + " is not valid JSON: " + (error instanceof Error ? error.message : String(error)) +
|
|
88
|
+
".");
|
|
89
|
+
}
|
|
90
|
+
if ((typeof parsed !== "object") || (parsed === null)) {
|
|
91
|
+
throw new Error("webui-loader: the `WEBUI LOADER CONFIG` in " + htmlPath + " must be a JSON object with an `entry` string and an optional `bust` string array.");
|
|
92
|
+
}
|
|
93
|
+
const config = parsed;
|
|
94
|
+
if (typeof config.entry !== "string") {
|
|
95
|
+
throw new Error("webui-loader: the `WEBUI LOADER CONFIG` in " + htmlPath + " must carry an `entry` string.");
|
|
96
|
+
}
|
|
97
|
+
if ((config.bust !== undefined) && (!Array.isArray(config.bust) || config.bust.some((item) => typeof item !== "string"))) {
|
|
98
|
+
throw new Error("webui-loader: the `WEBUI LOADER CONFIG` `bust` field in " + htmlPath + " must be an array of strings.");
|
|
99
|
+
}
|
|
100
|
+
return { bust: config.bust ?? [], entry: config.entry };
|
|
101
|
+
}
|
|
102
|
+
// The plain-language boot-failure copy the status panel shows, approved in design and expanded to complete sentences. The browser, delivery, and generic strings are
|
|
103
|
+
// the three classified message buckets the boot monitor reveals by stage; the slow notice is the watchdog's still-working signal; the headline sits above them all.
|
|
104
|
+
// The copy lives here as panel text content, not as monitor script constants, so it renders exactly and contiguously on the page and stays clear of this file's
|
|
105
|
+
// line-width wrap - HTML text content carries no such limit, and a long single-sentence string cannot be broken without splitting it away from a support grep.
|
|
106
|
+
const BOOT_HEADLINE = "The settings interface couldn't load.";
|
|
107
|
+
const BOOT_MESSAGE_BROWSER = "Your browser doesn't support features this interface requires. Please use a current version of Safari or Chrome.";
|
|
108
|
+
const BOOT_MESSAGE_DELIVERY = "The interface files couldn't be retrieved from the Homebridge server. Reload the page to try again. " +
|
|
109
|
+
"If this keeps happening, log out of the Homebridge interface and log back in.";
|
|
110
|
+
const BOOT_MESSAGE_GENERIC = "An unexpected error occurred while starting the interface.";
|
|
111
|
+
const BOOT_SLOW_NOTICE = "This is taking longer than expected. If nothing appears shortly, reload the page.";
|
|
112
|
+
// The status panel markup: a hidden, centered block that a screenshot turns into a complete support report. It starts fully hidden and is revealed by the boot monitor
|
|
113
|
+
// alone. The three bucket messages are pre-rendered and each hidden, so the monitor reveals the classified one without ever assigning message text at runtime; only the
|
|
114
|
+
// untrusted technical details are filled in through `textContent` at reveal time. The `<details open>` disclosure shows those details the moment the panel appears - a
|
|
115
|
+
// screenshot is then a complete report - while staying collapsible after reading. The sibling slow notice carries the watchdog's still-working line, hidden until armed.
|
|
116
|
+
const BOOT_STATUS_PANEL = [
|
|
117
|
+
"<div id=\"pageBootError\" class=\"mt-4 text-center\" style=\"display: none;\">",
|
|
118
|
+
" <h5>" + BOOT_HEADLINE + "</h5>",
|
|
119
|
+
" <div id=\"bootErrorMessage\">",
|
|
120
|
+
" <p data-boot-bucket=\"browser\" style=\"display: none;\">" + BOOT_MESSAGE_BROWSER + "</p>",
|
|
121
|
+
" <p data-boot-bucket=\"delivery\" style=\"display: none;\">" + BOOT_MESSAGE_DELIVERY + "</p>",
|
|
122
|
+
" <p data-boot-bucket=\"generic\" style=\"display: none;\">" + BOOT_MESSAGE_GENERIC + "</p>",
|
|
123
|
+
" </div>",
|
|
124
|
+
" <details open>",
|
|
125
|
+
" <summary>Technical details</summary>",
|
|
126
|
+
" <div id=\"bootErrorDetails\"></div>",
|
|
127
|
+
" </details>",
|
|
128
|
+
"</div>",
|
|
129
|
+
"<div id=\"bootSlowNotice\" class=\"mt-4 text-center\" style=\"display: none;\">" + BOOT_SLOW_NOTICE + "</div>"
|
|
130
|
+
];
|
|
131
|
+
// The boot monitor: a classic inline script that runs during HTML parse, ahead of the deferred module loader, so it can catch a failure anywhere along the boot path
|
|
132
|
+
// and report it on the page. Its whole body is an IIFE guarded against a second execution in the same window, exposing a frozen `window.webUiBoot` with `fail` and
|
|
133
|
+
// `ready`, window `error`/`unhandledrejection` listeners, and a ten-second watchdog. First failure wins; `ready()` from the app supersedes it. The block is static -
|
|
134
|
+
// no plugin fact reaches it - so it is a constant the renderer splices verbatim between the panel and the loader.
|
|
135
|
+
const BOOT_MONITOR = [
|
|
136
|
+
"<script>",
|
|
137
|
+
" /* Boot monitor for the plugin webUI, generated by homebridge-plugin-utils - do not edit this region by hand. This is a classic inline script, so it runs during",
|
|
138
|
+
" * HTML parse ahead of the deferred module loader below and can report a failure anywhere along the boot path. It reveals the hidden status panel with the message",
|
|
139
|
+
" * bucket the failing stage names, arms a ten-second watchdog for a boot that hangs, and stands down when webUi.show() signals the app rendered or displayed its",
|
|
140
|
+
" * own error. The classic-versus-module split is for execution ordering alone: a classic inline script runs during parse while a module script is deferred.",
|
|
141
|
+
" */",
|
|
142
|
+
" (function() {",
|
|
143
|
+
"",
|
|
144
|
+
" // A second execution in the same window - a reused iframe across repeated settings-panel opens - returns immediately, so the first run's listeners and watchdog",
|
|
145
|
+
" // are never duplicated into an orphaned registration that could resurrect the panel over a working app.",
|
|
146
|
+
" if(window.webUiBoot) {",
|
|
147
|
+
"",
|
|
148
|
+
" return;",
|
|
149
|
+
" }",
|
|
150
|
+
"",
|
|
151
|
+
" const watchdogMs = 10000;",
|
|
152
|
+
"",
|
|
153
|
+
" // The stage-to-bucket map. The browser stage stands alone, the manifest and import delivery stages share the delivery bucket, and every other stage - an",
|
|
154
|
+
" // uncaught window error or unhandled rejection included - falls through the nullish default to the generic bucket.",
|
|
155
|
+
" const buckets = { browser: \"browser\", import: \"delivery\", manifest: \"delivery\" };",
|
|
156
|
+
"",
|
|
157
|
+
" let settled = false;",
|
|
158
|
+
" let watchdog;",
|
|
159
|
+
"",
|
|
160
|
+
" // Set an element's display by id, tolerating an absent node so a stamped region missing a panel element never throws from the monitor.",
|
|
161
|
+
" const setDisplay = (id, value) => {",
|
|
162
|
+
"",
|
|
163
|
+
" const element = document.getElementById(id);",
|
|
164
|
+
"",
|
|
165
|
+
" if(element) {",
|
|
166
|
+
"",
|
|
167
|
+
" element.style.display = value;",
|
|
168
|
+
" }",
|
|
169
|
+
" };",
|
|
170
|
+
"",
|
|
171
|
+
" // Stop watching: clear the watchdog and remove both window listeners. Called from the first failure and from ready(), so nothing is left open once the boot has",
|
|
172
|
+
" // resolved one way or the other.",
|
|
173
|
+
" const stop = () => {",
|
|
174
|
+
"",
|
|
175
|
+
" clearTimeout(watchdog);",
|
|
176
|
+
" window.removeEventListener(\"error\", onError);",
|
|
177
|
+
" window.removeEventListener(\"unhandledrejection\", onRejection);",
|
|
178
|
+
" };",
|
|
179
|
+
"",
|
|
180
|
+
" // The first failure wins. Reveal the panel with the classified bucket message and the technical details, retract any slow notice the watchdog raised, stop",
|
|
181
|
+
" // watching, and drop the host spinner so it cannot mask the panel. A later failure, and any failure after ready(), is ignored. The handlers never call",
|
|
182
|
+
" // preventDefault(), so the browser console keeps every error and the panel only complements it.",
|
|
183
|
+
" const fail = (stage, error) => {",
|
|
184
|
+
"",
|
|
185
|
+
" if(settled) {",
|
|
186
|
+
"",
|
|
187
|
+
" return;",
|
|
188
|
+
" }",
|
|
189
|
+
"",
|
|
190
|
+
" settled = true;",
|
|
191
|
+
"",
|
|
192
|
+
" stop();",
|
|
193
|
+
"",
|
|
194
|
+
" // Reveal the one pre-rendered message the failing stage's bucket names; the bucket keys are internal constants, never user input, so the selector is safe.",
|
|
195
|
+
" const chosen = document.querySelector(\"#bootErrorMessage [data-boot-bucket='\" + (buckets[stage] ?? \"generic\") + \"']\");",
|
|
196
|
+
"",
|
|
197
|
+
" if(chosen) {",
|
|
198
|
+
"",
|
|
199
|
+
" chosen.style.display = \"block\";",
|
|
200
|
+
" }",
|
|
201
|
+
"",
|
|
202
|
+
" const details = document.getElementById(\"bootErrorDetails\");",
|
|
203
|
+
"",
|
|
204
|
+
" if(details) {",
|
|
205
|
+
"",
|
|
206
|
+
" // The error text is untrusted, so each detail line is assigned as plain text through textContent and is never parsed as markup.",
|
|
207
|
+
" const lines = [ \"Stage: \" + stage, \"Error: \" + ((error instanceof Error) ? error.message : String(error)), \"Browser: \" + navigator.userAgent ];",
|
|
208
|
+
"",
|
|
209
|
+
" details.textContent = \"\";",
|
|
210
|
+
"",
|
|
211
|
+
" for(const line of lines) {",
|
|
212
|
+
"",
|
|
213
|
+
" const row = document.createElement(\"div\");",
|
|
214
|
+
"",
|
|
215
|
+
" row.textContent = line;",
|
|
216
|
+
" details.appendChild(row);",
|
|
217
|
+
" }",
|
|
218
|
+
" }",
|
|
219
|
+
"",
|
|
220
|
+
" setDisplay(\"bootSlowNotice\", \"none\");",
|
|
221
|
+
" setDisplay(\"pageBootError\", \"block\");",
|
|
222
|
+
" globalThis.homebridge?.hideSpinner?.();",
|
|
223
|
+
" };",
|
|
224
|
+
"",
|
|
225
|
+
" // The app is authoritative and supersedes anything the monitor showed. webUi.show() calls this from its finally once it has rendered or displayed its own toast,",
|
|
226
|
+
" // so an earlier boot-phase error was evidently non-fatal - browser-extension noise on window.onerror is real. Stop watching, retract both the slow notice and",
|
|
227
|
+
" // the panel, and mark settled so a later fail() is a no-op.",
|
|
228
|
+
" const ready = () => {",
|
|
229
|
+
"",
|
|
230
|
+
" settled = true;",
|
|
231
|
+
"",
|
|
232
|
+
" stop();",
|
|
233
|
+
"",
|
|
234
|
+
" setDisplay(\"bootSlowNotice\", \"none\");",
|
|
235
|
+
" setDisplay(\"pageBootError\", \"none\");",
|
|
236
|
+
" };",
|
|
237
|
+
"",
|
|
238
|
+
" const onError = (event) => fail(\"uncaught error\", event.error ?? event.message);",
|
|
239
|
+
" const onRejection = (event) => fail(\"unhandled rejection\", event.reason);",
|
|
240
|
+
"",
|
|
241
|
+
" window.webUiBoot = Object.freeze({ fail, ready });",
|
|
242
|
+
"",
|
|
243
|
+
" window.addEventListener(\"error\", onError);",
|
|
244
|
+
" window.addEventListener(\"unhandledrejection\", onRejection);",
|
|
245
|
+
"",
|
|
246
|
+
" // Arm the watchdog. Nothing reported by the deadline means the boot has stalled, so show the slow notice in the spinner's place and drop the spinner; a boot",
|
|
247
|
+
" // that finishes later retracts the notice through ready(). The watchdog tears nothing down - boot may still succeed.",
|
|
248
|
+
" watchdog = setTimeout(() => {",
|
|
249
|
+
"",
|
|
250
|
+
" setDisplay(\"bootSlowNotice\", \"block\");",
|
|
251
|
+
" globalThis.homebridge?.hideSpinner?.();",
|
|
252
|
+
" }, watchdogMs);",
|
|
253
|
+
" })();",
|
|
254
|
+
"</script>"
|
|
255
|
+
];
|
|
256
|
+
/**
|
|
257
|
+
* Render the webUI boot region from the plugin's config plus the destination and package facts the CLI supplies. The region carries, in boot order, a hidden status
|
|
258
|
+
* panel, a classic-script boot monitor, and the importmap/cache-bust module loader. The loader reproduces the proven runtime semantics of the hand-authored block: a
|
|
259
|
+
* single `Date.now()` cache-bust stamp shared across the page load, a `bust()` helper that resolves a path against `import.meta.url` and appends the stamp, a
|
|
260
|
+
* `cache: "no-store"` fetch of the manifest, an importmap, and a dynamic import of the entry, all wrapped in a stage-instrumented `try`/`catch` that routes a boot
|
|
261
|
+
* failure to the monitor with the stage that failed, without adding any await or work to the happy path.
|
|
262
|
+
*
|
|
263
|
+
* The catch classifies by stage. A manifest-stage failure reports directly, with no capability probe: the importmap is not injected yet, so probing resolution there
|
|
264
|
+
* would misroute every ordinary delivery failure into the browser bucket. At the import stage the map is injected, so an engine without `import.meta.resolve` predates
|
|
265
|
+
* the loader's floor, and a probe of the injected prefix that throws proves the map never applied - either is the browser bucket - while a resolving probe leaves the
|
|
266
|
+
* failure in the delivery bucket. The probe proves the map applied, not that the delivered module content is sound: a top-level throw inside a delivered module is
|
|
267
|
+
* indistinguishable from a fetch failure here and knowingly folds into the delivery bucket, where the panel's raw error line disambiguates it for support.
|
|
268
|
+
*
|
|
269
|
+
* Every importmap module entry keeps the BARE module path as its KEY with the busted URL as its VALUE - the entry module ITSELF plus every declared `bust` entry, so
|
|
270
|
+
* the most important module (the entry) is never served stale. The trailing-slash `<packageName>/` prefix entry maps the bare package specifier to the hashed-versioned
|
|
271
|
+
* subdir, and transitive imports inherit it through relative-URL resolution.
|
|
272
|
+
*
|
|
273
|
+
* `libPath` is the destination-relative segment the CLI derives from its `dest` basename (`"./lib/"` for the family convention), so the manifest fetch path and the
|
|
274
|
+
* prefix URL are path-correct for any destination rather than a hardcoded `./lib/` that is only coincidentally right for one plugin.
|
|
275
|
+
*
|
|
276
|
+
* @param args
|
|
277
|
+
* @param args.bust - The hand-authored modules (the entry aside) that each need a cache-bust.
|
|
278
|
+
* @param args.entry - The entry module the loader dynamically imports; it is cache-busted alongside the `bust` list.
|
|
279
|
+
* @param args.libPath - The destination-relative segment (e.g. `"./lib/"`) the manifest fetch and the importmap prefix resolve against.
|
|
280
|
+
* @param args.packageName - The mirrored package's name; the importmap prefix maps `<packageName>/` to the hashed-versioned subdir, and the import-stage probe resolves
|
|
281
|
+
* `<packageName>/webUi.mjs` against it.
|
|
282
|
+
*
|
|
283
|
+
* @returns The marked-region content - the status panel, the boot monitor, and the `<script type="module">` loader - to splice between the marker pair.
|
|
284
|
+
*
|
|
285
|
+
* @category WebUI Loader
|
|
286
|
+
*/
|
|
287
|
+
export function renderWebUiBootRegion({ bust, entry, libPath, packageName }) {
|
|
288
|
+
// The busted modules: the entry itself plus every declared bust entry, de-duplicated and sorted so the importmap is stable across runs. The entry is busted too -
|
|
289
|
+
// it sits outside the package's hashed versioning, so an unbusted entry would let the browser serve a stale copy of the most important module.
|
|
290
|
+
const bustedModules = [...new Set([entry, ...bust])].toSorted();
|
|
291
|
+
const importLines = bustedModules.map((module) => " \"" + module + "\": bust(\"" + module + "\"),");
|
|
292
|
+
const prefixLine = " \"" + packageName + "/\": new URL(\"" + libPath + "\" + manifest.subdir + \"/\", import.meta.url).href";
|
|
293
|
+
// The stage-instrumented module loader. Its runtime happy path is identical to the hand-authored block; the `try`/`catch` and the `let stage` add only the failure
|
|
294
|
+
// route to the monitor. `stage` starts `"manifest"` and flips to `"import"` immediately before the entry import, so the importmap build and injection between them
|
|
295
|
+
// still report under `"manifest"`.
|
|
296
|
+
const loaderLines = [
|
|
297
|
+
"<script type=\"module\">",
|
|
298
|
+
" /* Plugin webUI loader, generated by homebridge-plugin-utils from the loader-config comment above - do not edit this region by hand. It reads the",
|
|
299
|
+
" * homebridge-plugin-utils manifest at load time and injects an importmap mapping the bare \"" + packageName + "/\" specifier to the hashed-versioned subdir the",
|
|
300
|
+
" * prepare-ui CLI mirrors into place; the trailing-slash prefix carries transitive imports through relative-URL resolution, and each hand-authored module carries",
|
|
301
|
+
" * a Date.now() cache-bust so a single page load stays coherent across them. The body is stage-instrumented: a fetch, importmap, or entry-import failure is",
|
|
302
|
+
" * reported to the boot monitor above with the stage that failed, which reveals the on-page status panel. The import-stage probe proves the injected importmap",
|
|
303
|
+
" * applied - a browser that ignored it lands in the browser bucket - but it cannot tell a delivery failure from a top-level throw inside a delivered module, so",
|
|
304
|
+
" * that case knowingly folds into the delivery bucket and the panel's raw error line is what disambiguates it for support.",
|
|
305
|
+
" */",
|
|
306
|
+
"",
|
|
307
|
+
" const cb = Date.now();",
|
|
308
|
+
" const bust = (path) => new URL(path, import.meta.url).href + \"?cb=\" + cb;",
|
|
309
|
+
"",
|
|
310
|
+
" let stage = \"manifest\";",
|
|
311
|
+
"",
|
|
312
|
+
" try {",
|
|
313
|
+
"",
|
|
314
|
+
" const response = await fetch(\"" + libPath + "manifest.json\", { cache: \"no-store\" });",
|
|
315
|
+
"",
|
|
316
|
+
" if(!response.ok) {",
|
|
317
|
+
"",
|
|
318
|
+
" throw new Error(\"The manifest request failed with HTTP status \" + response.status + \".\");",
|
|
319
|
+
" }",
|
|
320
|
+
"",
|
|
321
|
+
" const manifest = await response.json();",
|
|
322
|
+
"",
|
|
323
|
+
" const importMap = {",
|
|
324
|
+
"",
|
|
325
|
+
" imports: {",
|
|
326
|
+
"",
|
|
327
|
+
...importLines,
|
|
328
|
+
prefixLine,
|
|
329
|
+
" }",
|
|
330
|
+
" };",
|
|
331
|
+
"",
|
|
332
|
+
" const mapScript = document.createElement(\"script\");",
|
|
333
|
+
"",
|
|
334
|
+
" mapScript.type = \"importmap\";",
|
|
335
|
+
" mapScript.textContent = JSON.stringify(importMap);",
|
|
336
|
+
" document.head.appendChild(mapScript);",
|
|
337
|
+
"",
|
|
338
|
+
" stage = \"import\";",
|
|
339
|
+
"",
|
|
340
|
+
" await import(\"" + entry + "\");",
|
|
341
|
+
" } catch(error) {",
|
|
342
|
+
"",
|
|
343
|
+
" // The monitor owns the on-page display, so after classifying we report and swallow: rethrowing would only add a duplicate uncaught error to the console the",
|
|
344
|
+
" // original failure already produced.",
|
|
345
|
+
" if(stage === \"import\") {",
|
|
346
|
+
"",
|
|
347
|
+
" if(typeof import.meta.resolve !== \"function\") {",
|
|
348
|
+
"",
|
|
349
|
+
" window.webUiBoot.fail(\"browser\", error);",
|
|
350
|
+
" } else {",
|
|
351
|
+
"",
|
|
352
|
+
" try {",
|
|
353
|
+
"",
|
|
354
|
+
" import.meta.resolve(\"" + packageName + "/webUi.mjs\");",
|
|
355
|
+
" window.webUiBoot.fail(\"import\", error);",
|
|
356
|
+
" } catch {",
|
|
357
|
+
"",
|
|
358
|
+
" window.webUiBoot.fail(\"browser\", error);",
|
|
359
|
+
" }",
|
|
360
|
+
" }",
|
|
361
|
+
" } else {",
|
|
362
|
+
"",
|
|
363
|
+
" window.webUiBoot.fail(\"manifest\", error);",
|
|
364
|
+
" }",
|
|
365
|
+
" }",
|
|
366
|
+
"</script>"
|
|
367
|
+
];
|
|
368
|
+
// Assemble the region: the status panel, then the boot monitor, then the loader, separated by a blank line and joined with LF. The panel and monitor are static
|
|
369
|
+
// constants; only the loader carries the plugin's busted entries and dynamic segments, kept explicit at each interpolation point through string concatenation rather
|
|
370
|
+
// than template literals, matching the house style.
|
|
371
|
+
return [...BOOT_STATUS_PANEL, "", ...BOOT_MONITOR, "", ...loaderLines].join("\n");
|
|
372
|
+
}
|
|
373
|
+
//# sourceMappingURL=webui-loader.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webui-loader.js","sourceRoot":"","sources":["../src/webui-loader.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "homebridge-plugin-utils",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"displayName": "Homebridge Plugin Utilities",
|
|
5
5
|
"description": "Opinionated utilities to provide common capabilities and create rich configuration webUI experiences for Homebridge plugins.",
|
|
6
6
|
"author": {
|
|
@@ -56,13 +56,13 @@
|
|
|
56
56
|
"hblog": "dist/logclient/cli.js"
|
|
57
57
|
},
|
|
58
58
|
"devDependencies": {
|
|
59
|
-
"@homebridge/hap-nodejs": "2.1.
|
|
59
|
+
"@homebridge/hap-nodejs": "2.1.9",
|
|
60
60
|
"@stylistic/eslint-plugin": "5.10.0",
|
|
61
61
|
"@types/node": "26.1.1",
|
|
62
62
|
"aedes": "1.0.2",
|
|
63
63
|
"eslint": "9.39.2",
|
|
64
|
-
"happy-dom": "20.
|
|
65
|
-
"homebridge": "2.
|
|
64
|
+
"happy-dom": "20.11.0",
|
|
65
|
+
"homebridge": "2.2.1",
|
|
66
66
|
"typedoc": "0.28.20",
|
|
67
67
|
"typedoc-plugin-markdown": "4.12.0",
|
|
68
68
|
"typescript": "5.9.3",
|