Files
olivv-cs-ComfyUI-FunPack/app/shell/panels.js
T
digital-garbageandClaude Opus 5 89e4b24b59 composer: a module is handed no way to reach the page, and one test now checks it
The sandbox was a claim the code could not make. `panels.js` said "no DOM
node, no document... a module cannot style anything because it is never
handed anything that could" -- while handing over `composer`, whose factories
return handles wrapping a real element. A module could take one, style it,
and reach the whole document through node.ownerDocument. No lint to evade,
just the intended API.

No module ever used it. Panels are rendered from the DECLARATION, and a
module that wants to draw something should be declaring it. So it is gone,
and the guarantee is now true as stated: the kit hands a module nothing that
touches the DOM.

What is NOT claimed any more: that a module CANNOT reach the DOM. A ui.js is
an ES module in the page realm and `document` is a global. kit_lint is a
discipline check, not a boundary -- globalThis["docu"+"ment"] defeats every
regex and always will. It now catches the bracket-access spellings, which
were the first thing tried and went straight through, and says plainly what
it is and is not.

The test that was supposed to protect this asserted "no value IS a node"
while a value that MAKES nodes sat in the same object. It checks what it
names now.

And the overlay compaction test computed a before-snapshot and discarded it
with `void before`, checking only that survivors do not collide -- true even
if nothing ever moves. Two attempts to fix it failed against CORRECT code
(earlier tests leave live claims, so the rung has a legitimate gap; and some
demos close themselves, so closing them again is a no-op). It now opens two
overlays of its own so the invariant runs every time, and removing notify()
from zlayer fails three tests instead of none.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-29 18:51:03 +03:00

106 lines
4.3 KiB
JavaScript

// Turning announcements into panels.
//
// Everything a module can reach is decided here, and it is deliberately little:
// { values, on, shell }. No DOM node, no document, no element factory.
//
// `composer` used to be in that list, and it was the hole in the guarantee: a
// factory returns a handle whose `.node` is a real element, so a module could
// take one, style it, and reach the whole document through `node.ownerDocument`
// -- no lint to evade, just the intended API. No module ever used it. Panels are
// rendered from the DECLARATION, and a module that wants to draw something is a
// module that should be declaring it instead.
//
// What is true, stated exactly: the kit hands a module nothing that touches the
// DOM. What is NOT true, and must not be claimed: that a module CANNOT reach the
// DOM. A ui.js is an ES module in the page realm and `document` is a global.
// tools/kit_lint raises the cost of doing that accidentally; it is a discipline
// check and not a boundary, and only real isolation (a worker, another realm)
// would make it one. First-party modules in this repo are reviewed; a
// third-party module would need that isolation before it could be trusted.
import { composer } from "../composer/composer.js";
import { renderPanel, defaultsOf } from "../composer/panel.js";
import { hostFor, offered } from "./mounts.js";
import * as values from "./values.js";
import { services } from "./services.js";
/**
* mountAll(manifest) -> { mounted, hidden }
*
* `hidden` is diagnostic only. Nothing about a hidden module reaches the screen:
* no placeholder, no greyed row, no warning chip. A module that failed is
* indistinguishable from one that was never installed, which is the point --
* the alternative is a UI full of things that cannot be used.
*/
export async function mountAll(manifest, { load = (path) => import(path) } = {}) {
const mounted = [];
const hidden = [];
for (const spec of manifest.modules || []) {
// Nothing to render. A module may exist purely to contribute ComfyUI nodes
// (a loader, a model's compatibility module), and those have no panel and
// no mount. Skipping is not hiding: there is no failure to report.
const renders = Object.keys(spec.settings || {}).length > 0 || spec.ui;
if (!renders) continue;
const host = hostFor(spec.mount);
if (!host) {
hidden.push({ id: spec.id, why: `no region offers "${spec.mount}" (offered: ${offered().join(", ") || "none"})` });
continue;
}
try {
// Values first: a module's ui.js may read them during setup, and a
// headless run has them without any of this having happened.
values.seed(spec.id, defaultsOf(spec));
let ui = null;
if (spec.ui) ui = await load(spec.ui);
let teardown = null;
const panel = renderPanel(spec, {
values: values.valuesOf(spec.id),
onChange: (key, value) => values.set(spec.id, key, value),
});
// Only now, with a complete panel in hand, does anything reach the DOM.
host.appendChild(panel.node);
if (ui && typeof ui.setup === "function") {
// Kept, not discarded: setup() returns the unsubscribe from on(), and
// dropping it means every re-mount leaves another live listener behind.
teardown = ui.setup({
values: {
get: () => values.valuesOf(spec.id),
set: (key, value) => values.set(spec.id, key, value),
},
// Scoped to this module: a module hearing about everyone else's
// edits would have to filter, and filtering needs an id it should
// not need to know.
on: (fn) => values.onChange((id, key, value) => {
if (id === spec.id) fn(key, value);
}),
// The app services a module may use, named explicitly. A short,
// reviewable list beats reaching for a global, which is how a module
// ends up depending on something nobody meant to expose.
shell: services,
});
}
mounted.push({
id: spec.id,
panel,
destroy() {
if (typeof teardown === "function") teardown();
panel.destroy();
},
});
} catch (err) {
hidden.push({ id: spec.id, why: `${err.name}: ${err.message}` });
console.warn(`[FunPack] ${spec.id} did not mount: ${err.message}`);
}
}
return { mounted, hidden };
}