docs: audit the ui library and record the 0.8.6 migration
Quality / quality (ubuntu-latest) (push) Failing after 12m40s
Quality / quality (windows-latest) (push) Canceled after 0s

A measured pass rather than a bulk rewrite. Numbers come from the source and
from a browser.

The migration entry covers what has accumulated since 0.8.5 and would
otherwise reach upgraders unannounced: the Tabs output contract, the Sidebar
BEM rename, the layout components leaving Tailwind so their rendered class
lists changed, LayoutSplitter and CustomScrollbar changing props and outputs,
the ui.css families that were removed, and the theme tokens that now paint
where they previously resolved to nothing.

The audit records what is still wrong, with counts: 32 outputs across 16
components that nothing emits, 23 components still on the scaffold pattern, 66
without a local style block and therefore dependent on ui.css, and 10 still
using Tailwind. A test pins the dead-output count at 32 as a ceiling that only
moves down, so rebuilding a component tightens it and no new one can be added
quietly.

It also records what is not worth doing. Splitting the runtime saves 3 to 4 kB
gzipped on a first visit to a file cached for a year, and hydration costs
1.5 ms for 21 scopes across 4325 elements, so neither is a real problem.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-08 17:16:57 +05:30
co-authored by Claude Opus 5
parent c7ab605e85
commit 84dc4e06a2
3 changed files with 201 additions and 0 deletions
+64
View File
@@ -3100,3 +3100,67 @@ test("every wire color token a component references is defined by the theme", ()
expect([...missing].map(([token, users]) => `${token} <- ${users.join(", ")}`)).toEqual([]);
});
test("no component gains an output that nothing ever emits", () => {
/*
* LayoutSplitter declared resizeStart, resize and resizeEnd with no pointer
* handling at all: a caller wired up @resize and received nothing, for ever,
* with no error. The same shape survives in other components, so this pins
* the count rather than letting it grow while the rest are rebuilt.
*
* Native event names are excluded. The runtime binds a DOM-listener fallback
* on component tags, so declaring click or input as an output does reach a
* parent binding through bubbling.
*/
const native = new Set([
"click",
"focus",
"blur",
"input",
"change",
"submit",
"copy",
"paste",
"load",
"error",
"scroll",
"toggle",
"drop",
"dragstart",
"dragend",
"dragenter",
"dragleave",
"dragover",
"keydown",
"keyup",
"select",
]);
const runtime = readFileSync(
join(uiComponentsDir(), "..", "..", "csr", "src", "reactive-runtime.ts"),
"utf8",
);
const offenders: string[] = [];
for (const name of uiComponentNames()) {
const source = readFileSync(uiComponentPath(name), "utf8");
const block = /^ {2}outputs \{([\s\S]*?)^ {2}\}/m.exec(source);
if (!block) continue;
for (const match of block[1]!.matchAll(/^\s*([A-Za-z][A-Za-z0-9_]*)\s*\(/gm)) {
const output = match[1]!;
if (source.slice(block.index).includes(`output.${output}(`)) continue;
if (source.includes("output[")) continue;
if (native.has(output.toLowerCase())) continue;
// Emitted by a runtime controller written for this component.
if (runtime.includes(`"${output}"`) && new RegExp(name, "i").test(runtime)) continue;
offenders.push(`${name}.${output}`);
}
}
/*
* A ceiling, not a target. It only ever moves down: rebuilding one of these
* components should tighten it.
*/
expect(offenders.length).toBeLessThanOrEqual(32);
expect(offenders).not.toContain("LayoutSplitter.sizeChange");
expect(offenders).not.toContain("CustomScrollbar.scroll");
});