docs: audit the ui library and record the 0.8.6 migration
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:
@@ -2029,6 +2029,49 @@ const MIGRATIONS: Migration[] = [
|
||||
// else arrives through the dependency update.
|
||||
},
|
||||
},
|
||||
{
|
||||
version: "0.8.6",
|
||||
id: "0.8.6-navigation-and-layout-groups",
|
||||
description:
|
||||
"Rebuilds the navigation and layout component groups, moves them off Tailwind utilities onto wire-* classes, and defines theme tokens that components referenced but nothing declared.",
|
||||
apply() {
|
||||
// Source changes no codemod can make safely, so they are listed rather
|
||||
// than attempted.
|
||||
//
|
||||
// Tabs replaced its raw CustomEvents with declared outputs. Code
|
||||
// listening for the old change and select events on the element must
|
||||
// move to the @change and @select bindings.
|
||||
//
|
||||
// Sidebar renamed its classes to the BEM form used everywhere else:
|
||||
// wire-sidebar-shell, -items, -group, -toggle, -backdrop, -panel and
|
||||
// -layout became wire-sidebar__*. Nesting via children still works.
|
||||
//
|
||||
// The layout, page and section components moved from Tailwind utility
|
||||
// classes to wire-* classes with their own styles. Application CSS
|
||||
// selecting on the utility classes they used to render -- max-w-7xl,
|
||||
// gap-5, sm:grid-cols-2 and the rest -- no longer matches. Variants are
|
||||
// data attributes now, so target [data-variant] and friends instead.
|
||||
//
|
||||
// LayoutSplitter and CustomScrollbar previously declared outputs they
|
||||
// never emitted. LayoutSplitter now resizes and emits sizeChange rather
|
||||
// than resizeStart, resize and resizeEnd, and its props are size,
|
||||
// minSize, step and orientation rather than columns, gap and maxWidth.
|
||||
// CustomScrollbar dropped its scroll output; listen for the plain scroll
|
||||
// event on the element. Its props are axis, thickness, maxHeight and
|
||||
// radius.
|
||||
//
|
||||
// ui.css lost several application-pattern class families that nothing
|
||||
// referenced: wire-catalog-*, wire-page-shell, wire-product-card,
|
||||
// wire-legal-toc, wire-sdk-tabs, wire-cookie-* and wire-analytics-preview.
|
||||
// Anything hand-written against those needs its own styles.
|
||||
//
|
||||
// Themes gain tokens that were referenced but never defined, including
|
||||
// --wire-color-focus, --wire-color-surface-soft, --wire-color-on-danger
|
||||
// and the input-* family. A custom theme that declared these itself
|
||||
// keeps winning; one that did not will see focus rings and soft surfaces
|
||||
// start painting where they previously rendered as nothing.
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
/** Release tooling uses this to require an explicit migration entry per version. */
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user