@variance-authority/core 0.2.0 → 0.3.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/CHANGELOG.md +81 -36
- package/README.md +255 -128
- package/dist/attribute/call-site.d.ts +20 -2
- package/dist/attribute/call-site.js +17 -20
- package/dist/attribute/call-site.js.map +1 -1
- package/dist/attribute/composition.js +67 -11
- package/dist/attribute/composition.js.map +1 -1
- package/dist/attribute/index.d.ts +2 -0
- package/dist/attribute/index.js +1 -0
- package/dist/attribute/index.js.map +1 -1
- package/dist/attribute/landmark.d.ts +97 -0
- package/dist/attribute/landmark.js +144 -0
- package/dist/attribute/landmark.js.map +1 -0
- package/dist/attribute/lexicon.d.ts +25 -1
- package/dist/attribute/lexicon.js +87 -27
- package/dist/attribute/lexicon.js.map +1 -1
- package/dist/attribute/stack.d.ts +62 -1
- package/dist/attribute/stack.js +116 -5
- package/dist/attribute/stack.js.map +1 -1
- package/dist/judge/inspect.js +57 -4
- package/dist/judge/inspect.js.map +1 -1
- package/dist/relate/before.d.ts +137 -0
- package/dist/relate/before.js +155 -0
- package/dist/relate/before.js.map +1 -0
- package/dist/relate/graph.d.ts +28 -11
- package/dist/relate/graph.js +20 -9
- package/dist/relate/graph.js.map +1 -1
- package/dist/relate/index.d.ts +2 -1
- package/dist/relate/index.js +1 -0
- package/dist/relate/index.js.map +1 -1
- package/dist/relate/merkle.d.ts +6 -0
- package/dist/relate/merkle.js +6 -0
- package/dist/relate/merkle.js.map +1 -1
- package/dist/relate/records.d.ts +67 -1
- package/dist/relate/records.js +34 -7
- package/dist/relate/records.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"call-site.js","sourceRoot":"","sources":["../../src/attribute/call-site.ts"],"names":[],"mappings":"AAEA,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,cAAc,EACd,kBAAkB,GAEnB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAqD5D,MAAM,UAAU,sBAAsB,CAAC,WAAwB;IAC7D,uFAAuF;IACvF,MAAM,IAAI,GAAG,IAAI,GAAG,EAAgD,CAAC;IACrE,kFAAkF;IAClF,MAAM,KAAK,GAAG,IAAI,GAAG,EAA0C,CAAC;IAEhE,MAAM,KAAK,GAAG,EAAE,OAAO,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC;IAEnD,KAAK,UAAU,OAAO,CAAC,GAAW;QAChC,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC7B,IAAI,MAAM,KAAK,SAAS;YAAE,OAAO,MAAM,CAAC;QAExC,MAAM,OAAO,GAAG,CAAC,KAAK,IAAI,EAAE;YAC1B,KAAK,CAAC,OAAO,IAAI,CAAC,CAAC;YAEnB,MAAM,IAAI,GAAG,MAAM,WAAW,CAAC,GAAG,CAAC,CAAC;YACpC,IAAI,IAAI,KAAK,IAAI;gBAAE,OAAO,IAAI,CAAC;YAE/B,MAAM,UAAU,GAAG,kBAAkB,CAAC,IAAI,CAAC,CAAC;YAC5C,IAAI,UAAU,KAAK,IAAI;gBAAE,OAAO,IAAI,CAAC;YAErC,MAAM,MAAM,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAC;YAC7C,IAAI,MAAM,KAAK,IAAI;gBAAE,OAAO,cAAc,CAAC,MAAM,CAAC,CAAC;YAEnD,sEAAsE;YACtE,6EAA6E;YAC7E,wEAAwE;YACxE,2CAA2C;YAC3C,MAAM,OAAO,GAAG,QAAQ,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;YAC1C,MAAM,IAAI,GAAG,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,WAAW,CAAC,OAAO,CAAC,CAAC;YAClE,OAAO,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,cAAc,CAAC,IAAI,CAAC,CAAC;QACrD,CAAC,CAAC,EAAE,CAAC;QAEL,IAAI,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QACvB,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,KAAK,UAAU,WAAW,CAAC,KAAiB;QAC1C,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;QAExC,8EAA8E;QAC9E,2EAA2E;QAC3E,yEAAyE;QACzE,2EAA2E;QAC3E,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,IAAI,CAAC;QAExD,MAAM,QAAQ,GAAG,mBAAmB,CAAC,MAAM,EAAE,KAAK,CAAC,IAAI,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;QACvE,IAAI,QAAQ,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;QAEnC,OAAO;YACL,IAAI,EAAE,UAAU,CAAC,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,CAAC;YAC5C,IAAI,EAAE,QAAQ,CAAC,IAAI;YACnB,MAAM,EAAE,QAAQ,CAAC,MAAM;SACxB,CAAC;IACJ,CAAC;IAED,KAAK,UAAU,MAAM,CAAC,MAA6B;QACjD,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YAC3B,IAAI,YAAY,CAAC,KAAK,CAAC,GAAG,CAAC;gBAAE,SAAS;YAEtC,MAAM,GAAG,GAAG,GAAG,KAAK,CAAC,GAAG,IAAI,KAAK,CAAC,IAAI,IAAI,KAAK,CAAC,MAAM,EAAE,CAAC;YACzD,IAAI,MAAM,GAAG,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAE5B,IAAI,MAAM,KAAK,SAAS,EAAE,CAAC;gBACzB,KAAK,CAAC,KAAK,IAAI,CAAC,CAAC;gBACjB,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;gBAC5B,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;YACzB,CAAC;YAED,MAAM,OAAO,GAAG,MAAM,MAAM,CAAC;YAE7B,4EAA4E;YAC5E,+DAA+D;YAC/D,MAAM,MAAM,GACV,OAAO,KAAK,IAAI;gBACd,CAAC,CAAC,gBAAgB,CAAC,CAAC,KAAK,CAAC,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC;gBACvC,CAAC,CAAC,YAAY,CAAC,OAAO,CAAC,IAAI,CAAC;oBAC1B,CAAC,CAAC,IAAI;oBACN,CAAC,CAAC,OAAO,CAAC;YAEhB,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;gBACpB,KAAK,CAAC,OAAO,IAAI,CAAC,CAAC;gBACnB,OAAO,MAAM,CAAC;YAChB,CAAC;QACH,CAAC;QAED,OAAO,IAAI,CAAC;IACd,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,KAAsB,EAAE,CAAC;AACnD,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,UAAsB,EACtB,QAA0B;IAE1B,MAAM,EAAE,KAAK,EAAE,GAAG,OAAO,EAAE,GAAG,UAAU,CAAC;IACzC,IAAI,KAAK,KAAK,SAAS,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,UAAU,CAAC;IAEjE,IAAI,MAAM,GAA0B,IAAI,CAAC;IACzC,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,QAAQ,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;IACxC,CAAC;IAAC,MAAM,CAAC;QACP,MAAM,GAAG,IAAI,CAAC;IAChB,CAAC;IAED,OAAO,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG,OAAO,EAAE,MAAM,EAAE,CAAC;AAC5D,CAAC;AAgBD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,KAAK,UAAU,WAAW,CAC/B,KAAmB,EACnB,QAA0B,EAC1B,QAA0B;IAE1B,MAAM,MAAM,GAAG,KAAK,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,MAAM,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC;IAC1F,IAAI,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAE1B,+EAA+E;IAC/E,4EAA4E;IAC5E,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IAEnC,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,CAC/B,KAAK,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE;QACvB,IAAI,IAAI,CAAC,MAAM,KAAK,SAAS,IAAI,IAAI,CAAC,IAAI,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC;QAEtE,MAAM,UAAU,GAAG,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,UAAU,CAAC;QACpD,IAAI,UAAU,EAAE,KAAK,KAAK,SAAS;YAAE,OAAO,IAAI,CAAC;QAEjD,MAAM,KAAK,GAAG,MAAM,gBAAgB,CAAC,UAAU,EAAE,QAAQ,CAAC,CAAC;QAC3D,OAAO,KAAK,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC;IAC/E,CAAC,CAAC,CACH,CAAC;IAEF,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE,CAAC,IAAI,KAAK,KAAK,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,OAAO,CAAC;AAC3E,CAAC;AAED,SAAS,KAAK,CAAC,IAAkB;IAC/B,MAAM,KAAK,GAAG,IAAI,GAAG,EAA0B,CAAC;IAEhD,MAAM,IAAI,GAAG,CAAC,IAAkB,EAAQ,EAAE;QACxC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;QAC3B,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ;YAAE,IAAI,CAAC,KAAK,CAAC,CAAC;IACjD,CAAC,CAAC;IACF,IAAI,CAAC,IAAI,CAAC,CAAC;IAEX,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;;;;GASG;AACH,SAAS,UAAU,CAAC,MAAc,EAAE,SAAiB;IACnD,MAAM,QAAQ,GAAG,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC,IAAI,MAAM,CAAC;IACvD,MAAM,MAAM,GAAG,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAElC,oEAAoE;IACpE,yEAAyE;IACzE,gEAAgE;IAChE,OAAO,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;AAC1E,CAAC;AAED,SAAS,QAAQ,CAAC,GAAW,EAAE,IAAY;IACzC,OAAO,QAAQ,CAAC,GAAG,EAAE,IAAI,CAAC,EAAE,IAAI,IAAI,IAAI,CAAC;AAC3C,CAAC;AAgBD,MAAM,MAAM,GAAI,UAAsE,CAAC,GAAG,CAAC;AAE3F,SAAS,QAAQ,CAAC,GAAW,EAAE,IAAa;IAC1C,IAAI,MAAM,KAAK,SAAS;QAAE,OAAO,IAAI,CAAC;IAEtC,IAAI,CAAC;QACH,OAAO,IAAI,MAAM,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC","sourcesContent":["import type { Provenance, SourceLocation, StackFrame } from '../format/provenance.js';\nimport type { NodePath, SemanticNode, SemanticSnapshot } from '../format/snapshot.js';\nimport {\n inlineSourceMapOf,\n originalPositionFor,\n parseSourceMap,\n sourceMappingUrlOf,\n type SourceMap,\n} from './source-map.js';\nimport { isVendorPath, writerLocationOf } from './stack.js';\n\n/**\n * Spending the frames a page read, to get the file a reviewer opens.\n *\n * This is the half of the zero-install path that cannot happen in the page. A\n * frame names the module the browser was *served*, and turning that into a line\n * in the repository means fetching that module and reading the source map the\n * build already emitted beside it. Fetching is not something a page agent should\n * do — it runs inside somebody else's application, on a budget measured against\n * their render loop — and it is exactly what the collector is for.\n *\n * **The economics are what make this worth doing rather than clever.** Measured\n * on a 4211-node document: every fiber carried a stack, and between them they\n * held **14 distinct call sites**. A hundred-row table writes two thousand cells\n * from one line of JSX. So the work is per *call site*, not per node, and the\n * cache below is not an optimization but the thing that makes the cost bounded —\n * a handful of module fetches for a whole page.\n *\n * **And it is asked on a signal rather than on every capture.** Fourteen sites is\n * a bounded cost, not a free one, and a run that settles every subject on its\n * document digest has nobody to hand a location to: no region was drawn, no\n * finding was raised, and the fetches would answer a question nothing asked. So\n * frames ride the snapshot — they are provenance, and no hash projects\n * provenance — and {@link locateSites} spends them for the handful of nodes a\n * report is about to name. A page whose only change is one button resolves one\n * call site, not fourteen; a page that did not change resolves none.\n *\n * `fetchModule` is injected because `core` may not assume a network (ADR-0013),\n * and because the right way to fetch differs by caller: a browser-driving\n * collector should fetch from the page's own context, where the origin, the\n * cookies and the dev server's module graph are already correct.\n */\n\n/** Fetch a module's text, or answer that it cannot be had. Never throws. */\nexport type FetchModule = (url: string) => Promise<string | null>;\n\nexport interface CallSiteResolver {\n /** The location that wrote an element, from the frames its fiber carried. */\n locate(frames: readonly StackFrame[]): Promise<SourceLocation | null>;\n /** How many modules were fetched, and how many call sites they answered. */\n readonly stats: CallSiteStats;\n}\n\nexport interface CallSiteStats {\n /** Distinct modules fetched. The number that costs anything. */\n readonly modules: number;\n /** Distinct call sites asked about — the ratio to `modules` is the point. */\n readonly sites: number;\n /** Call sites that resolved to a file. */\n readonly located: number;\n}\n\nexport function createCallSiteResolver(fetchModule: FetchModule): CallSiteResolver {\n /** Module URL → its map, `null` once we know it has none. Also the in-flight guard. */\n const maps = new Map<string, Promise<readonly SourceMap[] | null>>();\n /** `url:line:column` → the answer, so a page's 4211 nodes cost 14 resolutions. */\n const sites = new Map<string, Promise<SourceLocation | null>>();\n\n const stats = { modules: 0, sites: 0, located: 0 };\n\n async function mapsFor(url: string): Promise<readonly SourceMap[] | null> {\n const cached = maps.get(url);\n if (cached !== undefined) return cached;\n\n const pending = (async () => {\n stats.modules += 1;\n\n const code = await fetchModule(url);\n if (code === null) return null;\n\n const annotation = sourceMappingUrlOf(code);\n if (annotation === null) return null;\n\n const inline = inlineSourceMapOf(annotation);\n if (inline !== null) return parseSourceMap(inline);\n\n // A sibling file, which is what a production bundler writes. Resolved\n // against the module rather than the page: `//# sourceMappingURL=out.js.map`\n // means \"beside this module\", and a page several directories deep would\n // otherwise ask for it in the wrong place.\n const sibling = absolute(annotation, url);\n const text = sibling === null ? null : await fetchModule(sibling);\n return text === null ? null : parseSourceMap(text);\n })();\n\n maps.set(url, pending);\n return pending;\n }\n\n async function locateFrame(frame: StackFrame): Promise<SourceLocation | null> {\n const parsed = await mapsFor(frame.url);\n\n // No map is not failure. A dev server serving a plain `.js` as written, and a\n // Node test runner — which applies source maps to `Error.stack` itself, so\n // the frame arrives already original — both land here with the answer in\n // hand. `writerLocationOf` keeps the frame's own coordinates in that case.\n if (parsed === null || parsed.length === 0) return null;\n\n const original = originalPositionFor(parsed, frame.line, frame.column);\n if (original === null) return null;\n\n return {\n file: sourcePath(original.source, frame.url),\n line: original.line,\n column: original.column,\n };\n }\n\n async function locate(frames: readonly StackFrame[]): Promise<SourceLocation | null> {\n for (const frame of frames) {\n if (isVendorPath(frame.url)) continue;\n\n const key = `${frame.url}:${frame.line}:${frame.column}`;\n let answer = sites.get(key);\n\n if (answer === undefined) {\n stats.sites += 1;\n answer = locateFrame(frame);\n sites.set(key, answer);\n }\n\n const located = await answer;\n\n // The frame's own coordinates, for a module served as written. Delegated so\n // there is one statement of the choosing rule rather than two.\n const chosen =\n located === null\n ? writerLocationOf([frame], () => null)\n : isVendorPath(located.file)\n ? null\n : located;\n\n if (chosen !== null) {\n stats.located += 1;\n return chosen;\n }\n }\n\n return null;\n }\n\n return { locate, stats: stats as CallSiteStats };\n}\n\n/**\n * Provenance with its frames spent: `source` filled in, `stack` gone.\n *\n * Returned unchanged when there is nothing to do — which is the common case once\n * a project installs `jsx-source`, and the case a production build is always in.\n * Never throws: a report that names a location for most of its nodes is the\n * normal outcome, and one node's unreachable module must not take the run down.\n */\nexport async function locateProvenance(\n provenance: Provenance,\n resolver: CallSiteResolver,\n): Promise<Provenance> {\n const { stack, ...carried } = provenance;\n if (stack === undefined || stack.length === 0) return provenance;\n\n let source: SourceLocation | null = null;\n try {\n source = await resolver.locate(stack);\n } catch {\n source = null;\n }\n\n return source === null ? carried : { ...carried, source };\n}\n\n/**\n * Anything a report points at: it names a node, and may already know its line.\n *\n * An `AttributedRegion` and a `Finding` are both this, arrived at from opposite\n * directions — one from a mask, one from an inspection — and both are the *few*.\n * That is the whole reason this shape is worth having rather than two functions:\n * what makes resolution affordable is being asked about a handful of nodes, and\n * a handful is what a region list and a finding list are.\n */\nexport interface LocatableSite {\n readonly path?: NodePath;\n readonly source?: SourceLocation;\n}\n\n/**\n * The line for each site a report is about to name, and for nothing else.\n *\n * This is the demand side of the zero-install path. The page reads frames off\n * every fiber because reading them is nearly free; turning one into a file is\n * not, and this is the only place that spends it.\n *\n * Three ways a site costs nothing, all of them the common case:\n *\n * - **It has no `path`.** A region no box contains names no node.\n * - **It already has a `source`.** React ≤18 and `@variance-authority/jsx-source`\n * both record the location outright, so there is nothing to resolve. Only\n * React 19, which throws its location away and captures an `Error` instead,\n * reaches the map.\n * - **Its node carried no frames.** A production build captures nothing.\n *\n * And a subject that settled on its document digest never calls this at all,\n * which is the point: no region, no finding, no fetch.\n *\n * Sites are resolved concurrently. The resolver's own cache collapses them onto\n * the modules they share, so two regions in one component cost one fetch.\n */\nexport async function locateSites<T extends LocatableSite>(\n sites: readonly T[],\n snapshot: SemanticSnapshot,\n resolver: CallSiteResolver,\n): Promise<readonly T[]> {\n const wanted = sites.some((site) => site.source === undefined && site.path !== undefined);\n if (!wanted) return sites;\n\n // Built once and only when something asked. A path lookup on a tree is a walk,\n // and doing it per site would make a fifty-region page walk it fifty times.\n const nodes = index(snapshot.root);\n\n const located = await Promise.all(\n sites.map(async (site) => {\n if (site.source !== undefined || site.path === undefined) return site;\n\n const provenance = nodes.get(site.path)?.provenance;\n if (provenance?.stack === undefined) return site;\n\n const spent = await locateProvenance(provenance, resolver);\n return spent.source === undefined ? site : { ...site, source: spent.source };\n }),\n );\n\n return located.every((site, at) => site === sites[at]) ? sites : located;\n}\n\nfunction index(root: SemanticNode): ReadonlyMap<NodePath, SemanticNode> {\n const nodes = new Map<NodePath, SemanticNode>();\n\n const walk = (node: SemanticNode): void => {\n nodes.set(node.path, node);\n for (const child of node.children) walk(child);\n };\n walk(root);\n\n return nodes;\n}\n\n/**\n * A map's `sources` entry as a path, resolved against the module it describes.\n *\n * Maps state sources relatively — Vite writes `probe.jsx` for `/src/probe.jsx` —\n * so the entry alone is ambiguous between two directories. Resolved and then\n * stripped to a path, because the origin is a fact about the machine that ran\n * the capture: a baseline holding `http://localhost:5199/src/probe.jsx` would\n * disagree with the next run on a different port. What is left is what the\n * source index and `relativizeSource` already speak.\n */\nfunction sourcePath(source: string, moduleUrl: string): string {\n const resolved = absolute(source, moduleUrl) ?? source;\n const parsed = parseUrl(resolved);\n\n // Not a URL: a bundler that wrote an absolute filesystem path, or a\n // `webpack://` specifier. Left as it is — `relativizeSource` handles the\n // first, and inventing a shape for the second would be a guess.\n return parsed === null ? resolved : parsed.pathname.replace(/^\\/+/, '');\n}\n\nfunction absolute(url: string, base: string): string | null {\n return parseUrl(url, base)?.href ?? null;\n}\n\n/**\n * The one host global this file needs, asked for rather than assumed.\n *\n * `core` types no host library (ADR-0001) and joining URLs by hand is a bug farm\n * — `..` past the root, a query string on the base, a protocol-relative\n * specifier. `URL` is present in every Node release this project supports and in\n * every browser, so the honest thing is to use it and to degrade if it is not\n * there, exactly as `inlineSourceMapOf` does for `atob`.\n */\ninterface ParsedUrl {\n readonly href: string;\n readonly pathname: string;\n}\n\nconst URL_OF = (globalThis as { URL?: new (url: string, base?: string) => ParsedUrl }).URL;\n\nfunction parseUrl(url: string, base?: string): ParsedUrl | null {\n if (URL_OF === undefined) return null;\n\n try {\n return new URL_OF(url, base);\n } catch {\n return null;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"call-site.js","sourceRoot":"","sources":["../../src/attribute/call-site.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n relativizeSource,\n type Provenance,\n type SourceLocation,\n type StackFrame,\n} from '../format/provenance.js';\nimport type { NodePath, SemanticNode, SemanticSnapshot } from '../format/snapshot.js';\nimport {\n inlineSourceMapOf,\n originalPositionFor,\n parseSourceMap,\n sourceMappingUrlOf,\n type SourceMap,\n} from './source-map.js';\nimport { isVendorPath, servedPath, writerLocationOf } from './stack.js';\n\n/**\n * Spending the frames a page read, to get the file a reviewer opens.\n *\n * This is the half of the zero-install path that cannot happen in the page. A\n * frame names the module the browser was *served*, and turning that into a line\n * in the repository means fetching that module and reading the source map the\n * build already emitted beside it. Fetching is not something a page agent should\n * do — it runs inside somebody else's application, on a budget measured against\n * their render loop — and it is exactly what the collector is for.\n *\n * **The economics are what make this worth doing rather than clever.** Measured\n * on a 4211-node document: every fiber carried a stack, and between them they\n * held **14 distinct call sites**. A hundred-row table writes two thousand cells\n * from one line of JSX. So the work is per *call site*, not per node, and the\n * cache below is not an optimization but the thing that makes the cost bounded —\n * a handful of module fetches for a whole page.\n *\n * **And it is asked on a signal rather than on every capture.** Fourteen sites is\n * a bounded cost, not a free one, and a run that settles every subject on its\n * document digest has nobody to hand a location to: no region was drawn, no\n * finding was raised, and the fetches would answer a question nothing asked. So\n * frames ride the snapshot — they are provenance, and no hash projects\n * provenance — and {@link locateSites} spends them for the handful of nodes a\n * report is about to name. A page whose only change is one button resolves one\n * call site, not fourteen; a page that did not change resolves none.\n *\n * `fetchModule` is injected because `core` may not assume a network (ADR-0013),\n * and because the right way to fetch differs by caller: a browser-driving\n * collector should fetch from the page's own context, where the origin, the\n * cookies and the dev server's module graph are already correct.\n */\n\n/** Fetch a module's text, or answer that it cannot be had. Never throws. */\nexport type FetchModule = (url: string) => Promise<string | null>;\n\nexport interface CallSiteOptions {\n /**\n * The repository root, for a location that arrives holding an absolute path.\n *\n * Two kinds of frame reach {@link CallSiteResolver.locate} and only one of\n * them is relative already. A frame that needed a map is answered in the map's\n * own terms, which are the build's and so the repository's. A frame that\n * needed none — a Node stack, or the synthetic frame React manufactures for a\n * server component — is answered in the terms of the disk it was captured on,\n * which means somebody's home directory in the middle of a path that is\n * otherwise a fact about the project.\n *\n * Given, the two shapes come out as one. Absent, a location is returned as it\n * arrived, because guessing at a root is how a path stops resolving for the\n * reader who opens it.\n */\n readonly sourceRoot?: string;\n}\n\nexport interface CallSiteResolver {\n /** The location that wrote an element, from the frames its fiber carried. */\n locate(frames: readonly StackFrame[]): Promise<SourceLocation | null>;\n /** How many modules were fetched, and how many call sites they answered. */\n readonly stats: CallSiteStats;\n}\n\nexport interface CallSiteStats {\n /** Distinct modules fetched. The number that costs anything. */\n readonly modules: number;\n /** Distinct call sites asked about — the ratio to `modules` is the point. */\n readonly sites: number;\n /** Call sites that resolved to a file. */\n readonly located: number;\n}\n\nexport function createCallSiteResolver(\n fetchModule: FetchModule,\n options: CallSiteOptions = {},\n): CallSiteResolver {\n /** Module URL → its map, `null` once we know it has none. Also the in-flight guard. */\n const maps = new Map<string, Promise<readonly SourceMap[] | null>>();\n /** `url:line:column` → the answer, so a page's 4211 nodes cost 14 resolutions. */\n const sites = new Map<string, Promise<SourceLocation | null>>();\n\n const stats = { modules: 0, sites: 0, located: 0 };\n\n async function mapsFor(url: string): Promise<readonly SourceMap[] | null> {\n const cached = maps.get(url);\n if (cached !== undefined) return cached;\n\n const pending = (async () => {\n stats.modules += 1;\n\n const code = await fetchModule(url);\n if (code === null) return null;\n\n const annotation = sourceMappingUrlOf(code);\n if (annotation === null) return null;\n\n const inline = inlineSourceMapOf(annotation);\n if (inline !== null) return parseSourceMap(inline);\n\n // A sibling file, which is what a production bundler writes. Resolved\n // against the module rather than the page: `//# sourceMappingURL=out.js.map`\n // means \"beside this module\", and a page several directories deep would\n // otherwise ask for it in the wrong place.\n const sibling = absolute(annotation, url);\n const text = sibling === null ? null : await fetchModule(sibling);\n return text === null ? null : parseSourceMap(text);\n })();\n\n maps.set(url, pending);\n return pending;\n }\n\n async function locateFrame(frame: StackFrame): Promise<SourceLocation | null> {\n const parsed = await mapsFor(frame.url);\n\n // No map is not failure, and it is not an answer either. A Node test runner\n // applies source maps to `Error.stack` itself, so its frame arrives already\n // original and there is nothing left to add. A served module carries no such\n // guarantee: a position in the text a server sent is a position in the\n // repository only if a map says so, so `writerLocationOf` refuses it.\n if (parsed === null || parsed.length === 0) return null;\n\n const original = originalPositionFor(parsed, frame.line, frame.column);\n if (original === null) return null;\n\n return {\n file: sourcePath(original.source, frame.url),\n line: original.line,\n column: original.column,\n };\n }\n\n async function locate(frames: readonly StackFrame[]): Promise<SourceLocation | null> {\n for (const frame of frames) {\n if (isVendorPath(frame.url)) continue;\n\n const key = `${frame.url}:${frame.line}:${frame.column}`;\n let answer = sites.get(key);\n\n if (answer === undefined) {\n stats.sites += 1;\n answer = locateFrame(frame);\n sites.set(key, answer);\n }\n\n const located = await answer;\n\n // The frame's own coordinates, for a frame that names a file on disk.\n // Delegated so there is one statement of the choosing rule rather than two.\n const chosen =\n located === null\n ? writerLocationOf([frame], () => null)\n : isVendorPath(located.file)\n ? null\n : located;\n\n if (chosen !== null) {\n stats.located += 1;\n return options.sourceRoot === undefined\n ? chosen\n : relativizeSource(chosen, options.sourceRoot);\n }\n }\n\n return null;\n }\n\n return { locate, stats: stats as CallSiteStats };\n}\n\n/**\n * Provenance with its frames spent: `source` filled in, `stack` gone.\n *\n * Returned unchanged when there is nothing to do — which is the common case once\n * a project installs `jsx-source`, and the case a production build is always in.\n * Never throws: a report that names a location for most of its nodes is the\n * normal outcome, and one node's unreachable module must not take the run down.\n */\nexport async function locateProvenance(\n provenance: Provenance,\n resolver: CallSiteResolver,\n): Promise<Provenance> {\n const { stack, ...carried } = provenance;\n if (stack === undefined || stack.length === 0) return provenance;\n\n let source: SourceLocation | null = null;\n try {\n source = await resolver.locate(stack);\n } catch {\n source = null;\n }\n\n return source === null ? carried : { ...carried, source };\n}\n\n/**\n * Anything a report points at: it names a node, and may already know its line.\n *\n * An `AttributedRegion` and a `Finding` are both this, arrived at from opposite\n * directions — one from a mask, one from an inspection — and both are the *few*.\n * That is the whole reason this shape is worth having rather than two functions:\n * what makes resolution affordable is being asked about a handful of nodes, and\n * a handful is what a region list and a finding list are.\n */\nexport interface LocatableSite {\n readonly path?: NodePath;\n readonly source?: SourceLocation;\n}\n\n/**\n * The line for each site a report is about to name, and for nothing else.\n *\n * This is the demand side of the zero-install path. The page reads frames off\n * every fiber because reading them is nearly free; turning one into a file is\n * not, and this is the only place that spends it.\n *\n * Three ways a site costs nothing, all of them the common case:\n *\n * - **It has no `path`.** A region no box contains names no node.\n * - **It already has a `source`.** React ≤18 and `@variance-authority/jsx-source`\n * both record the location outright, so there is nothing to resolve. Only\n * React 19, which throws its location away and captures an `Error` instead,\n * reaches the map.\n * - **Its node carried no frames.** A production build captures nothing.\n *\n * And a subject that settled on its document digest never calls this at all,\n * which is the point: no region, no finding, no fetch.\n *\n * Sites are resolved concurrently. The resolver's own cache collapses them onto\n * the modules they share, so two regions in one component cost one fetch.\n */\nexport async function locateSites<T extends LocatableSite>(\n sites: readonly T[],\n snapshot: SemanticSnapshot,\n resolver: CallSiteResolver,\n): Promise<readonly T[]> {\n const wanted = sites.some((site) => site.source === undefined && site.path !== undefined);\n if (!wanted) return sites;\n\n // Built once and only when something asked. A path lookup on a tree is a walk,\n // and doing it per site would make a fifty-region page walk it fifty times.\n const nodes = index(snapshot.root);\n\n const located = await Promise.all(\n sites.map(async (site) => {\n if (site.source !== undefined || site.path === undefined) return site;\n\n const provenance = nodes.get(site.path)?.provenance;\n if (provenance?.stack === undefined) return site;\n\n const spent = await locateProvenance(provenance, resolver);\n return spent.source === undefined ? site : { ...site, source: spent.source };\n }),\n );\n\n return located.every((site, at) => site === sites[at]) ? sites : located;\n}\n\nfunction index(root: SemanticNode): ReadonlyMap<NodePath, SemanticNode> {\n const nodes = new Map<NodePath, SemanticNode>();\n\n const walk = (node: SemanticNode): void => {\n nodes.set(node.path, node);\n for (const child of node.children) walk(child);\n };\n walk(root);\n\n return nodes;\n}\n\n/**\n * A map's `sources` entry as a path, resolved against the module it describes.\n *\n * Maps state sources relatively — Vite writes `probe.jsx` for `/src/probe.jsx` —\n * so the entry alone is ambiguous between two directories. Resolved against the\n * module first, then stripped of the origin it resolved against, which is a fact\n * about the machine that served it rather than about the code.\n */\nfunction sourcePath(source: string, moduleUrl: string): string {\n return servedPath(absolute(source, moduleUrl) ?? source);\n}\n\nfunction absolute(url: string, base: string): string | null {\n return parseUrl(url, base)?.href ?? null;\n}\n\n/**\n * The one host global this file needs, asked for rather than assumed.\n *\n * `core` types no host library (ADR-0001) and joining URLs by hand is a bug farm\n * — `..` past the root, a query string on the base, a protocol-relative\n * specifier. `URL` is present in every Node release this project supports and in\n * every browser, so the honest thing is to use it and to degrade if it is not\n * there, exactly as `inlineSourceMapOf` does for `atob`.\n */\ninterface ParsedUrl {\n readonly href: string;\n readonly pathname: string;\n}\n\nconst URL_OF = (globalThis as { URL?: new (url: string, base?: string) => ParsedUrl }).URL;\n\nfunction parseUrl(url: string, base?: string): ParsedUrl | null {\n if (URL_OF === undefined) return null;\n\n try {\n return new URL_OF(url, base);\n } catch {\n return null;\n }\n}\n"]}
|
|
@@ -10,8 +10,9 @@ import { attributed } from './instances.js';
|
|
|
10
10
|
export function composeSubjects(subjects) {
|
|
11
11
|
const entries = new Map();
|
|
12
12
|
const order = subjects.map((subject) => subject.subject);
|
|
13
|
+
const structural = structuralComponents(subjects);
|
|
13
14
|
for (const subject of subjects) {
|
|
14
|
-
const example = exampleOf(subject.instances);
|
|
15
|
+
const example = exampleOf(subject.instances, structural);
|
|
15
16
|
for (const instance of subject.instances) {
|
|
16
17
|
if (!attributed(instance))
|
|
17
18
|
continue;
|
|
@@ -111,21 +112,76 @@ function echoesOf(components) {
|
|
|
111
112
|
}
|
|
112
113
|
return echoes.sort((a, b) => b.sites.length - a.sites.length || byCodeUnit(a.component, b.component));
|
|
113
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* Components more than half the suite mounts.
|
|
117
|
+
*
|
|
118
|
+
* Structure, not subject matter. A harness wrapper, a theme or store provider,
|
|
119
|
+
* a portal root and an HOC that every screen is wrapped in are all mounted by
|
|
120
|
+
* nearly every subject, and none of them is what any subject is about. The test
|
|
121
|
+
* is a count over the suite rather than a list of names, which is the only form
|
|
122
|
+
* that survives contact with a real application: nothing here knows that
|
|
123
|
+
* `withStyles(x)` is an HOC, that a minified `aL` is a decorator, or that a
|
|
124
|
+
* class component is a context consumer, and nothing here needs to.
|
|
125
|
+
*
|
|
126
|
+
* More than half, because that is where the claim becomes checkable. A
|
|
127
|
+
* component two thirds of a suite mounts cannot be what distinguishes a subject
|
|
128
|
+
* from its neighbours, whatever it is called.
|
|
129
|
+
*/
|
|
130
|
+
function structuralComponents(subjects) {
|
|
131
|
+
const mounts = new Map();
|
|
132
|
+
for (const subject of subjects) {
|
|
133
|
+
for (const instance of subject.instances) {
|
|
134
|
+
if (!attributed(instance))
|
|
135
|
+
continue;
|
|
136
|
+
mounts.set(instance.component, (mounts.get(instance.component) ?? 0) + 1);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const structural = new Set();
|
|
140
|
+
for (const [component, held] of mounts) {
|
|
141
|
+
if (held * 2 > subjects.length)
|
|
142
|
+
structural.add(component);
|
|
143
|
+
}
|
|
144
|
+
return structural;
|
|
145
|
+
}
|
|
114
146
|
/**
|
|
115
147
|
* The component a subject exists to show, when one component does.
|
|
116
148
|
*
|
|
117
|
-
* The shallowest attributed boundary, and only when it is
|
|
118
|
-
* A story usually mounts one thing; a page mounts a layout
|
|
119
|
-
* and calling the first of them the subject's component
|
|
120
|
-
* winner out of document order.
|
|
149
|
+
* The shallowest attributed boundary that is not structure, and only when it is
|
|
150
|
+
* alone at its depth. A story usually mounts one thing; a page mounts a layout
|
|
151
|
+
* that mounts several, and calling the first of them the subject's component
|
|
152
|
+
* would be picking a winner out of document order.
|
|
153
|
+
*
|
|
154
|
+
* The descent is the whole of the difference between this and the obvious rule.
|
|
155
|
+
* Taking the shallowest boundary outright answers with whatever the harness
|
|
156
|
+
* mounted first, which on a real suite is one value for every subject in it —
|
|
157
|
+
* `Wrapper` across a component library, a minified decorator root across a
|
|
158
|
+
* built Storybook — and a field with one value is a field that says nothing. A
|
|
159
|
+
* depth every one of whose names is structure is passed through however many
|
|
160
|
+
* names it has, because a harness that mounts a provider beside a portal root
|
|
161
|
+
* has still not said what the subject is about.
|
|
162
|
+
*
|
|
163
|
+
* Where the descent finds nothing it answers as the shallowest rule would, so
|
|
164
|
+
* this can name more subjects than that rule and never fewer.
|
|
121
165
|
*/
|
|
122
|
-
function exampleOf(instances) {
|
|
123
|
-
const
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
166
|
+
function exampleOf(instances, structural) {
|
|
167
|
+
const held = instances.filter(attributed);
|
|
168
|
+
const depths = [...new Set(held.map((instance) => instance.depth))].sort((a, b) => a - b);
|
|
169
|
+
for (const depth of depths) {
|
|
170
|
+
const names = new Set(held.filter((instance) => instance.depth === depth).map((i) => i.component));
|
|
171
|
+
const matter = [...names].filter((name) => !structural.has(name));
|
|
172
|
+
if (matter.length === 0)
|
|
173
|
+
continue;
|
|
174
|
+
return matter.length === 1 ? matter[0] : shallowestOf(held, depths[0]);
|
|
175
|
+
}
|
|
176
|
+
return shallowestOf(held, depths[0]);
|
|
177
|
+
}
|
|
178
|
+
/** The shallowest boundary when it is alone at its depth: the rule before the descent. */
|
|
179
|
+
function shallowestOf(held, shallowest) {
|
|
180
|
+
if (shallowest === undefined)
|
|
181
|
+
return undefined;
|
|
182
|
+
const leads = held.filter((instance) => instance.depth === shallowest);
|
|
127
183
|
const names = new Set(leads.map((instance) => instance.component));
|
|
128
|
-
return
|
|
184
|
+
return names.size === 1 ? leads[0]?.component : undefined;
|
|
129
185
|
}
|
|
130
186
|
/**
|
|
131
187
|
* The key an instance with no props digest is filed under.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"composition.js","sourceRoot":"","sources":["../../src/attribute/composition.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Digest } from '../format/hash.js';\nimport type { NodePath, SemanticSnapshot } from '../format/snapshot.js';\nimport { divergencesOf, type Divergence } from './divergence.js';\nimport { attributed, type ComponentInstance } from './instances.js';\n\n/**\n * The suite read as one graph, at one commit.\n *\n * Every other comparison in this system is between two revisions of one subject.\n * This is the other axis: **many subjects, one revision**, joined on the\n * components they share. It answers the question a per-subject tool cannot even\n * phrase — *this component, here, is the same component you already have an\n * example of over there* — and it answers it from digests a run already\n * computed, with no second render, no image and no store.\n *\n * ## What the join is worth\n *\n * A suite of examples is a suite of *compositions*. The narrow example is a\n * component at a boundary; the page examples contain that same boundary again,\n * sometimes with the same props and sometimes not. Once the instances are\n * addressable, three facts fall out that nobody had:\n *\n * - **An echo.** One rendering, several subjects. The narrow example and the\n * page are watching literally the same bytes, so a change in one is a change\n * in all of them, and a reviewer looking at eleven diffs is looking at one.\n * - **A divergence.** Same component, same props, *different* rendering, at one\n * commit. Nothing about the component's own inputs can explain that. Either\n * something outside it decides its output — an ancestor's cascade, a token, its\n * own state — or the reading is not repeatable. Both are findings and neither\n * is a regression.\n * - **A control group.** For any instance that moved, the same rendering\n * somewhere else that did *not* is a stable state to refer to. That is the\n * referent flake attribution has always needed and never had, and the suite\n * supplies it for free: one commit already holds N renderings of every shared\n * component.\n *\n * ## What it deliberately does not do\n *\n * It never decides anything. Two subjects sharing a rendering is not a reason to\n * delete either — that is the argument `coverage.ts` makes at the level of names\n * and it holds harder here, because a component can be correct in one context\n * and broken in the next, which is why the contexts are separate subjects.\n */\n\n/** One instance, located. */\nexport interface Site {\n readonly subject: string;\n readonly path: NodePath;\n readonly depth: number;\n readonly within?: string;\n\n /**\n * The component whose JSX wrote this element, here.\n *\n * On the entry beside it, `createdBy` is a set folded over the whole suite —\n * every component that ever wrote one of these, anywhere. That is the right\n * shape for *who mounts this* and the wrong one for *who mounted this here*,\n * and the ladder asks the second: an edit to a component that writes a `Chip`\n * on the footer explains nothing about a `Chip` the sidebar wrote. Kept per\n * site so the question can be asked where it was answered.\n */\n readonly createdBy?: string;\n}\n\n/** One rendering of one component, and everywhere it occurred. */\nexport interface Rendering {\n readonly rendering: Digest;\n readonly structure: Digest;\n readonly semantics: Digest;\n readonly text: Digest;\n readonly style: Digest;\n\n /**\n * Child components this rendering mounted, in document order.\n *\n * Carried on the rendering rather than only on the entry because it is the\n * only evidence available about what was passed *in*: `propsDigest` excludes\n * `children` by design, so two renderings that mount different children are\n * two different inputs wearing one props digest. `divergencesOf` refuses to\n * call that a contradiction, and this is the field it reads.\n */\n readonly renders: readonly string[];\n\n /** In subject order, then document order. */\n readonly sites: readonly Site[];\n}\n\n/** One props digest of one component, and every way it rendered under it. */\nexport interface PropsClass {\n /** Absent when the collector supplied no provenance — unknown, not \"none\". */\n readonly props?: Digest;\n readonly renderings: readonly Rendering[];\n}\n\nexport interface ComponentEntry {\n readonly component: string;\n /** Subjects containing at least one boundary of it, in the order supplied. */\n readonly subjects: readonly string[];\n /** Boundaries summed across every subject. Distinct from `subjects.length`. */\n readonly instances: number;\n\n /**\n * Subjects whose shallowest attributed boundary is this component.\n *\n * The narrow example — the subject that exists to show this thing, rather than\n * a page that happens to contain it. Defined by depth rather than by a naming\n * convention because a naming convention is a different tool's `story:` prefix,\n * and this has to hold for a route suite too. Ties resolve to nothing: a\n * subject whose two shallowest boundaries are siblings has no single subject.\n */\n readonly examples: readonly string[];\n\n /** Components that enclose it somewhere, sorted. The graph, upwards. */\n readonly within: readonly string[];\n\n /**\n * Components that *mounted* it somewhere, sorted. The graph, upwards, again —\n * and usually the more useful of the two.\n *\n * `within` is where the boundary sits in the DOM and this is who wrote the\n * element. They are the same thing only for a component that authors a node of\n * its own, and a real application is full of components that do not: measured\n * on `examples/todomvc`, every `Chip` reports `within: ['Stack']` and\n * `createdBy: ['TodoFooter']`: the `Stack` that encloses a chip is a layout\n * primitive that knows nothing about it, and the footer that wrote the element\n * stands one boundary further out. A component that renders nothing but other\n * components owns no node, is a boundary nowhere, and appears in this graph\n * *only* here — while being the file a reviewer has to open.\n *\n * Empty on a production build, where `_debugOwner` is absent (ADR-0007). Empty\n * is *not* \"nothing mounted it\": a component with no caller is the subject root\n * and every other case is missing data, which is why the `upstream` rung in\n * `movement.ts` reads this and never concludes from its silence.\n */\n readonly createdBy: readonly string[];\n\n /** Components it encloses somewhere, sorted. The graph, downwards. */\n readonly renders: readonly string[];\n\n /**\n * Custom properties its own nodes resolve through, anywhere in the suite.\n *\n * The set a token movement is intersected against, which is how \"`--va-space-3`\n * moved\" becomes \"`--va-space-3` moved and these four components read it\".\n */\n readonly tokens: readonly string[];\n\n /** Sorted by props digest; the class with unknown props sorts last. */\n readonly classes: readonly PropsClass[];\n}\n\n/**\n * The same component, the same props, the same bytes, in more than one subject.\n *\n * The connected dots. `example` names the narrow subject among the sites when\n * there is one, because that is the artefact a reviewer already has an opinion\n * about — \"this is the Chip from the Chip story\" is a sentence, and \"these four\n * paths share a digest\" is not.\n */\nexport interface Echo {\n readonly component: string;\n readonly props?: Digest;\n readonly rendering: Digest;\n /** At least two, spanning at least two subjects. */\n readonly sites: readonly Site[];\n readonly example?: string;\n}\n\n\nexport interface SubjectComposition {\n readonly subject: string;\n readonly instances: readonly ComponentInstance[];\n\n /**\n * The subject as read, kept so a divergence can say *which input* moved.\n *\n * Optional because `composeSubjects` is a fold over instance lists and stays\n * one — a caller holding only a report's sidecars still gets the graph, the\n * echoes and the divergences, and gets them without a `partings` field it\n * would have to explain away as empty.\n */\n readonly snapshot?: SemanticSnapshot;\n}\n\nexport interface Composition {\n /** In the order supplied, which the run guarantees is plan order. */\n readonly subjects: readonly string[];\n /** Sorted by name, code-unit order. */\n readonly components: readonly ComponentEntry[];\n /** Sorted by how many sites each has, widest first, then by component. */\n readonly echoes: readonly Echo[];\n /** Sorted by component name. */\n readonly divergences: readonly Divergence[];\n}\n\n/**\n * Fold a run's per-subject instance lists into the graph.\n *\n * Pure, ordered, and a function of its input alone — the run's report has to be\n * a function of the plan, and a phase that accumulated as a worker pool finished\n * would produce a different artefact from the same suite on a slower machine.\n */\nexport function composeSubjects(subjects: readonly SubjectComposition[]): Composition {\n const entries = new Map<string, Accumulator>();\n const order = subjects.map((subject) => subject.subject);\n\n for (const subject of subjects) {\n const example = exampleOf(subject.instances);\n\n for (const instance of subject.instances) {\n if (!attributed(instance)) continue;\n\n const entry = accumulate(entries, instance.component);\n if (entry.subjects.at(-1) !== subject.subject) entry.subjects.push(subject.subject);\n entry.instances += 1;\n if (example === instance.component && entry.examples.at(-1) !== subject.subject) {\n entry.examples.push(subject.subject);\n }\n if (instance.within !== undefined) entry.within.add(instance.within);\n if (instance.createdBy !== undefined) entry.createdBy.add(instance.createdBy);\n for (const child of instance.renders) entry.renders.add(child);\n for (const token of instance.tokens) entry.tokens.add(token);\n\n const key = instance.props ?? UNKNOWN_PROPS;\n const byProps = entry.classes.get(key) ?? new Map<Digest, MutableRendering>();\n const rendering = byProps.get(instance.rendering) ?? {\n rendering: instance.rendering,\n structure: instance.structure,\n semantics: instance.semantics,\n text: instance.text,\n style: instance.style,\n renders: instance.renders,\n sites: [],\n };\n rendering.sites.push({\n subject: subject.subject,\n path: instance.path,\n depth: instance.depth,\n ...(instance.within === undefined ? {} : { within: instance.within }),\n ...(instance.createdBy === undefined ? {} : { createdBy: instance.createdBy }),\n });\n byProps.set(instance.rendering, rendering);\n entry.classes.set(key, byProps);\n }\n }\n\n const components = [...entries.entries()]\n .map(([component, entry]) => ({\n component,\n subjects: entry.subjects,\n instances: entry.instances,\n examples: entry.examples,\n within: [...entry.within].sort(byCodeUnit),\n createdBy: [...entry.createdBy].sort(byCodeUnit),\n renders: [...entry.renders].sort(byCodeUnit),\n tokens: [...entry.tokens].sort(byCodeUnit),\n classes: [...entry.classes.entries()]\n .sort(([a], [b]) => byCodeUnit(a, b))\n .map(([props, renderings]) => ({\n ...(props === UNKNOWN_PROPS ? {} : { props }),\n renderings: [...renderings.values()].sort(widestFirst),\n })),\n }))\n .sort((a, b) => byCodeUnit(a.component, b.component));\n\n const snapshots = new Map<string, SemanticSnapshot>();\n for (const subject of subjects) {\n if (subject.snapshot !== undefined) snapshots.set(subject.subject, subject.snapshot);\n }\n\n return {\n subjects: order,\n components,\n echoes: echoesOf(components),\n divergences: divergencesOf(components, snapshots),\n };\n}\n\n/**\n * Every rendering shared by two or more subjects.\n *\n * Two instances of one component inside a *single* subject sharing a rendering\n * is ordinary and says nothing — a list of three identical chips is three\n * identical chips. The finding is a rendering that survives being mounted\n * somewhere else, because that is the one a second subject is watching.\n */\nfunction echoesOf(components: readonly ComponentEntry[]): readonly Echo[] {\n const echoes: Echo[] = [];\n\n for (const entry of components) {\n for (const group of entry.classes) {\n for (const rendering of group.renderings) {\n const spread = new Set(rendering.sites.map((site) => site.subject));\n if (spread.size < 2) continue;\n\n const example = entry.examples.find((subject) => spread.has(subject));\n echoes.push({\n component: entry.component,\n ...(group.props === undefined ? {} : { props: group.props }),\n rendering: rendering.rendering,\n sites: rendering.sites,\n ...(example === undefined ? {} : { example }),\n });\n }\n }\n }\n\n return echoes.sort(\n (a, b) => b.sites.length - a.sites.length || byCodeUnit(a.component, b.component),\n );\n}\n\n\n/**\n * The component a subject exists to show, when one component does.\n *\n * The shallowest attributed boundary, and only when it is alone at that depth.\n * A story usually mounts one thing; a page mounts a layout that mounts several,\n * and calling the first of them the subject's component would be picking a\n * winner out of document order.\n */\nfunction exampleOf(instances: readonly ComponentInstance[]): string | undefined {\n const shallowest = instances.filter(attributed).reduce<number>((depth, instance) => {\n return Math.min(depth, instance.depth);\n }, Number.POSITIVE_INFINITY);\n\n const leads = instances.filter((instance) => attributed(instance) && instance.depth === shallowest);\n const names = new Set(leads.map((instance) => instance.component));\n return leads.length === 1 || names.size === 1 ? leads[0]?.component : undefined;\n}\n\n/**\n * The key an instance with no props digest is filed under.\n *\n * A sentinel rather than `undefined`, because a `Map` keyed on `Digest |\n * undefined` reads as though unknown props were a props class like any other,\n * and every consumer would then have to remember that it is not. It sorts last\n * by construction — no digest starts with a space.\n */\nconst UNKNOWN_PROPS = ' unknown';\n\ninterface Accumulator {\n readonly subjects: string[];\n instances: number;\n readonly examples: string[];\n readonly within: Set<string>;\n readonly createdBy: Set<string>;\n readonly renders: Set<string>;\n readonly tokens: Set<string>;\n readonly classes: Map<string, Map<Digest, MutableRendering>>;\n}\n\n/** A `Rendering` while its sites are still being collected. */\ninterface MutableRendering extends Omit<Rendering, 'sites'> {\n readonly sites: Site[];\n}\n\nfunction accumulate(entries: Map<string, Accumulator>, component: string): Accumulator {\n const existing = entries.get(component);\n if (existing !== undefined) return existing;\n\n const created: Accumulator = {\n subjects: [],\n instances: 0,\n examples: [],\n within: new Set(),\n createdBy: new Set(),\n renders: new Set(),\n tokens: new Set(),\n classes: new Map(),\n };\n entries.set(component, created);\n return created;\n}\n\nfunction widestFirst(a: Rendering, b: Rendering): number {\n return b.sites.length - a.sites.length || byCodeUnit(a.rendering, b.rendering);\n}\n\n/**\n * Code-unit order, never `localeCompare`.\n *\n * The same rule `hashComponents` sorts by, for the same reason: this output\n * reaches a report that is committed, diffed and read back on another runner,\n * and a locale-aware comparison makes the byte order a promise about `LANG`.\n */\nfunction byCodeUnit(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"composition.js","sourceRoot":"","sources":["../../src/attribute/composition.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Digest } from '../format/hash.js';\nimport type { NodePath, SemanticSnapshot } from '../format/snapshot.js';\nimport { divergencesOf, type Divergence } from './divergence.js';\nimport { attributed, type ComponentInstance } from './instances.js';\n\n/**\n * The suite read as one graph, at one commit.\n *\n * Every other comparison in this system is between two revisions of one subject.\n * This is the other axis: **many subjects, one revision**, joined on the\n * components they share. It answers the question a per-subject tool cannot even\n * phrase — *this component, here, is the same component you already have an\n * example of over there* — and it answers it from digests a run already\n * computed, with no second render, no image and no store.\n *\n * ## What the join is worth\n *\n * A suite of examples is a suite of *compositions*. The narrow example is a\n * component at a boundary; the page examples contain that same boundary again,\n * sometimes with the same props and sometimes not. Once the instances are\n * addressable, three facts fall out that nobody had:\n *\n * - **An echo.** One rendering, several subjects. The narrow example and the\n * page are watching literally the same bytes, so a change in one is a change\n * in all of them, and a reviewer looking at eleven diffs is looking at one.\n * - **A divergence.** Same component, same props, *different* rendering, at one\n * commit. Nothing about the component's own inputs can explain that. Either\n * something outside it decides its output — an ancestor's cascade, a token, its\n * own state — or the reading is not repeatable. Both are findings and neither\n * is a regression.\n * - **A control group.** For any instance that moved, the same rendering\n * somewhere else that did *not* is a stable state to refer to. That is the\n * referent flake attribution has always needed and never had, and the suite\n * supplies it for free: one commit already holds N renderings of every shared\n * component.\n *\n * ## What it deliberately does not do\n *\n * It never decides anything. Two subjects sharing a rendering is not a reason to\n * delete either — that is the argument `coverage.ts` makes at the level of names\n * and it holds harder here, because a component can be correct in one context\n * and broken in the next, which is why the contexts are separate subjects.\n */\n\n/** One instance, located. */\nexport interface Site {\n readonly subject: string;\n readonly path: NodePath;\n readonly depth: number;\n readonly within?: string;\n\n /**\n * The component whose JSX wrote this element, here.\n *\n * On the entry beside it, `createdBy` is a set folded over the whole suite —\n * every component that ever wrote one of these, anywhere. That is the right\n * shape for *who mounts this* and the wrong one for *who mounted this here*,\n * and the ladder asks the second: an edit to a component that writes a `Chip`\n * on the footer explains nothing about a `Chip` the sidebar wrote. Kept per\n * site so the question can be asked where it was answered.\n */\n readonly createdBy?: string;\n}\n\n/** One rendering of one component, and everywhere it occurred. */\nexport interface Rendering {\n readonly rendering: Digest;\n readonly structure: Digest;\n readonly semantics: Digest;\n readonly text: Digest;\n readonly style: Digest;\n\n /**\n * Child components this rendering mounted, in document order.\n *\n * Carried on the rendering rather than only on the entry because it is the\n * only evidence available about what was passed *in*: `propsDigest` excludes\n * `children` by design, so two renderings that mount different children are\n * two different inputs wearing one props digest. `divergencesOf` refuses to\n * call that a contradiction, and this is the field it reads.\n */\n readonly renders: readonly string[];\n\n /** In subject order, then document order. */\n readonly sites: readonly Site[];\n}\n\n/** One props digest of one component, and every way it rendered under it. */\nexport interface PropsClass {\n /** Absent when the collector supplied no provenance — unknown, not \"none\". */\n readonly props?: Digest;\n readonly renderings: readonly Rendering[];\n}\n\nexport interface ComponentEntry {\n readonly component: string;\n /** Subjects containing at least one boundary of it, in the order supplied. */\n readonly subjects: readonly string[];\n /** Boundaries summed across every subject. Distinct from `subjects.length`. */\n readonly instances: number;\n\n /**\n * Subjects whose shallowest attributed boundary is this component.\n *\n * The narrow example — the subject that exists to show this thing, rather than\n * a page that happens to contain it. Defined by depth rather than by a naming\n * convention because a naming convention is a different tool's `story:` prefix,\n * and this has to hold for a route suite too. Ties resolve to nothing: a\n * subject whose two shallowest boundaries are siblings has no single subject.\n */\n readonly examples: readonly string[];\n\n /** Components that enclose it somewhere, sorted. The graph, upwards. */\n readonly within: readonly string[];\n\n /**\n * Components that *mounted* it somewhere, sorted. The graph, upwards, again —\n * and usually the more useful of the two.\n *\n * `within` is where the boundary sits in the DOM and this is who wrote the\n * element. They are the same thing only for a component that authors a node of\n * its own, and a real application is full of components that do not: measured\n * on `examples/todomvc`, every `Chip` reports `within: ['Stack']` and\n * `createdBy: ['TodoFooter']`: the `Stack` that encloses a chip is a layout\n * primitive that knows nothing about it, and the footer that wrote the element\n * stands one boundary further out. A component that renders nothing but other\n * components owns no node, is a boundary nowhere, and appears in this graph\n * *only* here — while being the file a reviewer has to open.\n *\n * Empty on a production build, where `_debugOwner` is absent (ADR-0007). Empty\n * is *not* \"nothing mounted it\": a component with no caller is the subject root\n * and every other case is missing data, which is why the `upstream` rung in\n * `movement.ts` reads this and never concludes from its silence.\n */\n readonly createdBy: readonly string[];\n\n /** Components it encloses somewhere, sorted. The graph, downwards. */\n readonly renders: readonly string[];\n\n /**\n * Custom properties its own nodes resolve through, anywhere in the suite.\n *\n * The set a token movement is intersected against, which is how \"`--va-space-3`\n * moved\" becomes \"`--va-space-3` moved and these four components read it\".\n */\n readonly tokens: readonly string[];\n\n /** Sorted by props digest; the class with unknown props sorts last. */\n readonly classes: readonly PropsClass[];\n}\n\n/**\n * The same component, the same props, the same bytes, in more than one subject.\n *\n * The connected dots. `example` names the narrow subject among the sites when\n * there is one, because that is the artefact a reviewer already has an opinion\n * about — \"this is the Chip from the Chip story\" is a sentence, and \"these four\n * paths share a digest\" is not.\n */\nexport interface Echo {\n readonly component: string;\n readonly props?: Digest;\n readonly rendering: Digest;\n /** At least two, spanning at least two subjects. */\n readonly sites: readonly Site[];\n readonly example?: string;\n}\n\n\nexport interface SubjectComposition {\n readonly subject: string;\n readonly instances: readonly ComponentInstance[];\n\n /**\n * The subject as read, kept so a divergence can say *which input* moved.\n *\n * Optional because `composeSubjects` is a fold over instance lists and stays\n * one — a caller holding only a report's sidecars still gets the graph, the\n * echoes and the divergences, and gets them without a `partings` field it\n * would have to explain away as empty.\n */\n readonly snapshot?: SemanticSnapshot;\n}\n\nexport interface Composition {\n /** In the order supplied, which the run guarantees is plan order. */\n readonly subjects: readonly string[];\n /** Sorted by name, code-unit order. */\n readonly components: readonly ComponentEntry[];\n /** Sorted by how many sites each has, widest first, then by component. */\n readonly echoes: readonly Echo[];\n /** Sorted by component name. */\n readonly divergences: readonly Divergence[];\n}\n\n/**\n * Fold a run's per-subject instance lists into the graph.\n *\n * Pure, ordered, and a function of its input alone — the run's report has to be\n * a function of the plan, and a phase that accumulated as a worker pool finished\n * would produce a different artefact from the same suite on a slower machine.\n */\nexport function composeSubjects(subjects: readonly SubjectComposition[]): Composition {\n const entries = new Map<string, Accumulator>();\n const order = subjects.map((subject) => subject.subject);\n const structural = structuralComponents(subjects);\n\n for (const subject of subjects) {\n const example = exampleOf(subject.instances, structural);\n\n for (const instance of subject.instances) {\n if (!attributed(instance)) continue;\n\n const entry = accumulate(entries, instance.component);\n if (entry.subjects.at(-1) !== subject.subject) entry.subjects.push(subject.subject);\n entry.instances += 1;\n if (example === instance.component && entry.examples.at(-1) !== subject.subject) {\n entry.examples.push(subject.subject);\n }\n if (instance.within !== undefined) entry.within.add(instance.within);\n if (instance.createdBy !== undefined) entry.createdBy.add(instance.createdBy);\n for (const child of instance.renders) entry.renders.add(child);\n for (const token of instance.tokens) entry.tokens.add(token);\n\n const key = instance.props ?? UNKNOWN_PROPS;\n const byProps = entry.classes.get(key) ?? new Map<Digest, MutableRendering>();\n const rendering = byProps.get(instance.rendering) ?? {\n rendering: instance.rendering,\n structure: instance.structure,\n semantics: instance.semantics,\n text: instance.text,\n style: instance.style,\n renders: instance.renders,\n sites: [],\n };\n rendering.sites.push({\n subject: subject.subject,\n path: instance.path,\n depth: instance.depth,\n ...(instance.within === undefined ? {} : { within: instance.within }),\n ...(instance.createdBy === undefined ? {} : { createdBy: instance.createdBy }),\n });\n byProps.set(instance.rendering, rendering);\n entry.classes.set(key, byProps);\n }\n }\n\n const components = [...entries.entries()]\n .map(([component, entry]) => ({\n component,\n subjects: entry.subjects,\n instances: entry.instances,\n examples: entry.examples,\n within: [...entry.within].sort(byCodeUnit),\n createdBy: [...entry.createdBy].sort(byCodeUnit),\n renders: [...entry.renders].sort(byCodeUnit),\n tokens: [...entry.tokens].sort(byCodeUnit),\n classes: [...entry.classes.entries()]\n .sort(([a], [b]) => byCodeUnit(a, b))\n .map(([props, renderings]) => ({\n ...(props === UNKNOWN_PROPS ? {} : { props }),\n renderings: [...renderings.values()].sort(widestFirst),\n })),\n }))\n .sort((a, b) => byCodeUnit(a.component, b.component));\n\n const snapshots = new Map<string, SemanticSnapshot>();\n for (const subject of subjects) {\n if (subject.snapshot !== undefined) snapshots.set(subject.subject, subject.snapshot);\n }\n\n return {\n subjects: order,\n components,\n echoes: echoesOf(components),\n divergences: divergencesOf(components, snapshots),\n };\n}\n\n/**\n * Every rendering shared by two or more subjects.\n *\n * Two instances of one component inside a *single* subject sharing a rendering\n * is ordinary and says nothing — a list of three identical chips is three\n * identical chips. The finding is a rendering that survives being mounted\n * somewhere else, because that is the one a second subject is watching.\n */\nfunction echoesOf(components: readonly ComponentEntry[]): readonly Echo[] {\n const echoes: Echo[] = [];\n\n for (const entry of components) {\n for (const group of entry.classes) {\n for (const rendering of group.renderings) {\n const spread = new Set(rendering.sites.map((site) => site.subject));\n if (spread.size < 2) continue;\n\n const example = entry.examples.find((subject) => spread.has(subject));\n echoes.push({\n component: entry.component,\n ...(group.props === undefined ? {} : { props: group.props }),\n rendering: rendering.rendering,\n sites: rendering.sites,\n ...(example === undefined ? {} : { example }),\n });\n }\n }\n }\n\n return echoes.sort(\n (a, b) => b.sites.length - a.sites.length || byCodeUnit(a.component, b.component),\n );\n}\n\n\n/**\n * Components more than half the suite mounts.\n *\n * Structure, not subject matter. A harness wrapper, a theme or store provider,\n * a portal root and an HOC that every screen is wrapped in are all mounted by\n * nearly every subject, and none of them is what any subject is about. The test\n * is a count over the suite rather than a list of names, which is the only form\n * that survives contact with a real application: nothing here knows that\n * `withStyles(x)` is an HOC, that a minified `aL` is a decorator, or that a\n * class component is a context consumer, and nothing here needs to.\n *\n * More than half, because that is where the claim becomes checkable. A\n * component two thirds of a suite mounts cannot be what distinguishes a subject\n * from its neighbours, whatever it is called.\n */\nfunction structuralComponents(subjects: readonly SubjectComposition[]): ReadonlySet<string> {\n const mounts = new Map<string, number>();\n for (const subject of subjects) {\n for (const instance of subject.instances) {\n if (!attributed(instance)) continue;\n mounts.set(instance.component, (mounts.get(instance.component) ?? 0) + 1);\n }\n }\n\n const structural = new Set<string>();\n for (const [component, held] of mounts) {\n if (held * 2 > subjects.length) structural.add(component);\n }\n return structural;\n}\n\n/**\n * The component a subject exists to show, when one component does.\n *\n * The shallowest attributed boundary that is not structure, and only when it is\n * alone at its depth. A story usually mounts one thing; a page mounts a layout\n * that mounts several, and calling the first of them the subject's component\n * would be picking a winner out of document order.\n *\n * The descent is the whole of the difference between this and the obvious rule.\n * Taking the shallowest boundary outright answers with whatever the harness\n * mounted first, which on a real suite is one value for every subject in it —\n * `Wrapper` across a component library, a minified decorator root across a\n * built Storybook — and a field with one value is a field that says nothing. A\n * depth every one of whose names is structure is passed through however many\n * names it has, because a harness that mounts a provider beside a portal root\n * has still not said what the subject is about.\n *\n * Where the descent finds nothing it answers as the shallowest rule would, so\n * this can name more subjects than that rule and never fewer.\n */\nfunction exampleOf(\n instances: readonly ComponentInstance[],\n structural: ReadonlySet<string>,\n): string | undefined {\n const held = instances.filter(attributed);\n const depths = [...new Set(held.map((instance) => instance.depth))].sort((a, b) => a - b);\n\n for (const depth of depths) {\n const names = new Set(held.filter((instance) => instance.depth === depth).map((i) => i.component));\n const matter = [...names].filter((name) => !structural.has(name));\n if (matter.length === 0) continue;\n return matter.length === 1 ? matter[0] : shallowestOf(held, depths[0]);\n }\n\n return shallowestOf(held, depths[0]);\n}\n\n/** The shallowest boundary when it is alone at its depth: the rule before the descent. */\nfunction shallowestOf(held: readonly ComponentInstance[], shallowest: number | undefined): string | undefined {\n if (shallowest === undefined) return undefined;\n const leads = held.filter((instance) => instance.depth === shallowest);\n const names = new Set(leads.map((instance) => instance.component));\n return names.size === 1 ? leads[0]?.component : undefined;\n}\n\n/**\n * The key an instance with no props digest is filed under.\n *\n * A sentinel rather than `undefined`, because a `Map` keyed on `Digest |\n * undefined` reads as though unknown props were a props class like any other,\n * and every consumer would then have to remember that it is not. It sorts last\n * by construction — no digest starts with a space.\n */\nconst UNKNOWN_PROPS = ' unknown';\n\ninterface Accumulator {\n readonly subjects: string[];\n instances: number;\n readonly examples: string[];\n readonly within: Set<string>;\n readonly createdBy: Set<string>;\n readonly renders: Set<string>;\n readonly tokens: Set<string>;\n readonly classes: Map<string, Map<Digest, MutableRendering>>;\n}\n\n/** A `Rendering` while its sites are still being collected. */\ninterface MutableRendering extends Omit<Rendering, 'sites'> {\n readonly sites: Site[];\n}\n\nfunction accumulate(entries: Map<string, Accumulator>, component: string): Accumulator {\n const existing = entries.get(component);\n if (existing !== undefined) return existing;\n\n const created: Accumulator = {\n subjects: [],\n instances: 0,\n examples: [],\n within: new Set(),\n createdBy: new Set(),\n renders: new Set(),\n tokens: new Set(),\n classes: new Map(),\n };\n entries.set(component, created);\n return created;\n}\n\nfunction widestFirst(a: Rendering, b: Rendering): number {\n return b.sites.length - a.sites.length || byCodeUnit(a.rendering, b.rendering);\n}\n\n/**\n * Code-unit order, never `localeCompare`.\n *\n * The same rule `hashComponents` sorts by, for the same reason: this output\n * reaches a report that is committed, diffed and read back on another runner,\n * and a locale-aware comparison makes the byte order a promise about `LANG`.\n */\nfunction byCodeUnit(a: string, b: string): number {\n return a < b ? -1 : a > b ? 1 : 0;\n}\n"]}
|
|
@@ -37,6 +37,8 @@ export { composeSubjects } from './composition.js';
|
|
|
37
37
|
export type { Composition, ComponentEntry, Echo, PropsClass, Rendering, Site, SubjectComposition, } from './composition.js';
|
|
38
38
|
export type { Divergence, DivergenceParting } from './divergence.js';
|
|
39
39
|
export { lexiconOf, structureOf, LEXICON_CAP } from './lexicon.js';
|
|
40
|
+
export { LANDMARK_CAP } from './landmark.js';
|
|
41
|
+
export type { Landmark } from './landmark.js';
|
|
40
42
|
export type { BoundaryRow, LexiconField, LexiconOptions, SubjectLexicon } from './lexicon.js';
|
|
41
43
|
export { attributeMovement } from './movement.js';
|
|
42
44
|
export type { Attribution, Cause, Evidence, Moved, Movement } from './movement.js';
|
package/dist/attribute/index.js
CHANGED
|
@@ -25,6 +25,7 @@ export { componentInstances, attributed } from './instances.js';
|
|
|
25
25
|
export { nodeClosures, sharedClosures } from './closure.js';
|
|
26
26
|
export { composeSubjects } from './composition.js';
|
|
27
27
|
export { lexiconOf, structureOf, LEXICON_CAP } from './lexicon.js';
|
|
28
|
+
export { LANDMARK_CAP } from './landmark.js';
|
|
28
29
|
export { attributeMovement } from './movement.js';
|
|
29
30
|
export { coverageOf, alsoCovering, summarizeCoverage } from './coverage.js';
|
|
30
31
|
export { locateInstability, summarizeInstability } from './instability.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/attribute/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAU3E,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG5D,OAAO,EACL,aAAa,EACb,YAAY,EACZ,WAAW,EACX,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAG9E,OAAO,EACL,cAAc,EACd,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAGvF,OAAO,EACL,cAAc,EACd,aAAa,EACb,UAAU,EACV,iBAAiB,EACjB,YAAY,EACZ,YAAY,GACb,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAGhE,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAG5D,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAanD,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/attribute/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAGrC,OAAO,EAAE,cAAc,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,WAAW,CAAC;AAU3E,OAAO,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAG5D,OAAO,EACL,aAAa,EACb,YAAY,EACZ,WAAW,EACX,kBAAkB,EAClB,kBAAkB,GACnB,MAAM,aAAa,CAAC;AAGrB,OAAO,EAAE,gBAAgB,EAAE,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAG9E,OAAO,EACL,cAAc,EACd,mBAAmB,EACnB,kBAAkB,EAClB,iBAAiB,GAClB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC;AAGvF,OAAO,EACL,cAAc,EACd,aAAa,EACb,UAAU,EACV,iBAAiB,EACjB,YAAY,EACZ,YAAY,GACb,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAAE,kBAAkB,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAGhE,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAG5D,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAanD,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AACnE,OAAO,EAAE,YAAY,EAAE,MAAM,eAAe,CAAC;AAI7C,OAAO,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAGlD,OAAO,EAAE,UAAU,EAAE,YAAY,EAAE,iBAAiB,EAAE,MAAM,eAAe,CAAC;AAG5E,OAAO,EAAE,iBAAiB,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC","sourcesContent":["/**\n * `@variance-authority/core/attribute` — from a position to a name to a file.\n *\n * The group that makes a difference actionable. Pixels cluster into regions,\n * regions join the box tree, the tree knows which component produced each node,\n * and the component resolves to a source location. The same machinery answers\n * \"which component is not holding still\" and \"which subjects cover this\n * component\".\n *\n * This is the answer to *\"a pixel differ reports 1530 pixels changed\"* — and the\n * reason it is a group rather than a function is that every step of the journey\n * is separately useful, and separately capable of refusing. `locateInstability`\n * will not name a component for a difference that is beneath the semantic tier;\n * `coverage` will not recommend deleting anything.\n */\n\nexport { locate } from './locate.js';\nexport type { Location, LocationStep } from './locate.js';\n\nexport { isolateRegions, subtractRegions, excludedBoxes } from './mask.js';\nexport type {\n ChangeMask,\n DiffRegion,\n ExcludedBox,\n Isolation,\n IsolationOptions,\n Subtraction,\n} from './mask.js';\n\nexport { attributeRegions, rankRegions } from './region.js';\nexport type { AttributedRegion, AttributionOptions, RankedRegion } from './region.js';\n\nexport {\n resolveSource,\n formatSource,\n indexSource,\n mergeSourceIndexes,\n overlaySourceIndex,\n} from './source.js';\nexport type { SourceIndex, SourceRef, Resolution } from './source.js';\n\nexport { parseStackFrames, isVendorPath, writerLocationOf } from './stack.js';\nexport type { StackFrame } from './stack.js';\n\nexport {\n parseSourceMap,\n originalPositionFor,\n sourceMappingUrlOf,\n inlineSourceMapOf,\n} from './source-map.js';\nexport type { SourceMap, OriginalPosition } from './source-map.js';\n\nexport { createCallSiteResolver, locateProvenance, locateSites } from './call-site.js';\nexport type { CallSiteResolver, CallSiteStats, FetchModule, LocatableSite } from './call-site.js';\n\nexport {\n hashComponents,\n causesBetween,\n movedBands,\n movedBandsBetween,\n bandsBetween,\n UNATTRIBUTED,\n} from './component-hash.js';\nexport type { BandDigests, ComponentBands, ComponentHash } from './component-hash.js';\n\nexport { componentInstances, attributed } from './instances.js';\nexport type { ComponentInstance } from './instances.js';\n\nexport { nodeClosures, sharedClosures } from './closure.js';\nexport type { ClosureSite, NodeClosure, SharedClosure, SharedClosureOptions, SubjectTree } from './closure.js';\n\nexport { composeSubjects } from './composition.js';\nexport type {\n Composition,\n ComponentEntry,\n Echo,\n PropsClass,\n Rendering,\n Site,\n SubjectComposition,\n} from './composition.js';\n\nexport type { Divergence, DivergenceParting } from './divergence.js';\n\nexport { lexiconOf, structureOf, LEXICON_CAP } from './lexicon.js';\nexport { LANDMARK_CAP } from './landmark.js';\nexport type { Landmark } from './landmark.js';\nexport type { BoundaryRow, LexiconField, LexiconOptions, SubjectLexicon } from './lexicon.js';\n\nexport { attributeMovement } from './movement.js';\nexport type { Attribution, Cause, Evidence, Moved, Movement } from './movement.js';\n\nexport { coverageOf, alsoCovering, summarizeCoverage } from './coverage.js';\nexport type { Coverage, ComponentCoverage, SubjectCoverage, SubjectValue } from './coverage.js';\n\nexport { locateInstability, summarizeInstability } from './instability.js';\nexport type {\n Instability,\n InstabilityBand,\n UnstableLocation,\n UnstableProperty,\n} from './instability.js';\n"]}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { SemanticNode } from '../format/snapshot.js';
|
|
2
|
+
/**
|
|
3
|
+
* Where each thing on a subject was, kept as the run read it.
|
|
4
|
+
*
|
|
5
|
+
* The bags next door say what a subject holds; these say where it held it, off
|
|
6
|
+
* the same walk over the same tree, because two walks eventually disagree about
|
|
7
|
+
* what was there. A node earns a place by bearing a role, an accessible name,
|
|
8
|
+
* or words of its own — never by what it is called — so a screen written under
|
|
9
|
+
* an era's worth of higher-order components reduces to the same landmarks as
|
|
10
|
+
* the same screen written flat.
|
|
11
|
+
*/
|
|
12
|
+
/** One thing on a subject a person could point at, where the run saw it. */
|
|
13
|
+
export interface Landmark {
|
|
14
|
+
/** Accessible role, when the reading resolved one. */
|
|
15
|
+
readonly role?: string;
|
|
16
|
+
/** Accessible name — what a screen reader would announce. */
|
|
17
|
+
readonly name?: string;
|
|
18
|
+
/**
|
|
19
|
+
* The words directly inside it, and only those. Text belonging to a nested
|
|
20
|
+
* landmark stays on that landmark, so a container does not inherit the page.
|
|
21
|
+
*/
|
|
22
|
+
readonly text?: string;
|
|
23
|
+
/** Index of the nearest enclosing landmark. Absent on a top-level one. */
|
|
24
|
+
readonly within?: number;
|
|
25
|
+
/**
|
|
26
|
+
* `[x, y, width, height]` in layout pixels, integers.
|
|
27
|
+
*
|
|
28
|
+
* Absent, never zeroed, exactly as `rect` is: a reading that did not resolve
|
|
29
|
+
* layout has no opinion about what sits beneath what, and must say so rather
|
|
30
|
+
* than answer from document order while sounding equally sure.
|
|
31
|
+
*/
|
|
32
|
+
readonly box?: readonly [number, number, number, number];
|
|
33
|
+
/** Where the element was written, when the project installed the plugin. */
|
|
34
|
+
readonly file?: string;
|
|
35
|
+
readonly line?: number;
|
|
36
|
+
/**
|
|
37
|
+
* The innermost component that owns it, off the fiber's owner chain.
|
|
38
|
+
*
|
|
39
|
+
* The line an element was written at needs the JSX-source plugin, which a
|
|
40
|
+
* production build strips — and a built Storybook is one, so `file` and
|
|
41
|
+
* `line` are absent on exactly the runs that matter most. The owner chain
|
|
42
|
+
* survives that build: not the line, but the component whose source you would
|
|
43
|
+
* open, joined to a path through `LexiconReport.declaredIn`.
|
|
44
|
+
*/
|
|
45
|
+
readonly component?: string;
|
|
46
|
+
/** The component whose JSX created it. The unit somebody owns. */
|
|
47
|
+
readonly createdBy?: string;
|
|
48
|
+
/** A test handle, when one was set. The one name a suite chose deliberately. */
|
|
49
|
+
readonly handle?: string;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Landmarks kept per subject.
|
|
53
|
+
*
|
|
54
|
+
* Higher than the cap above on purpose. A field that loses its two hundredth
|
|
55
|
+
* distinct value loses a word the subject also says somewhere else; a subject
|
|
56
|
+
* that loses its last landmark loses a *place*, and a place has no second
|
|
57
|
+
* spelling.
|
|
58
|
+
*
|
|
59
|
+
* Four hundred was a guess and a 572-subject application falsified it: p50 9,
|
|
60
|
+
* p90 52, p99 227, and four subjects pinned at the cap, losing 1,973 places
|
|
61
|
+
* between them off screens of roughly 1,179, 926, 836 and 632. Those four are
|
|
62
|
+
* its biggest screens, which are the ones somebody most needs orienting on, and
|
|
63
|
+
* what a cap cuts there is the bottom of the page. Twelve hundred clears the
|
|
64
|
+
* largest with room and leaves the other 539 an order of magnitude below it.
|
|
65
|
+
*/
|
|
66
|
+
export declare const LANDMARK_CAP = 1200;
|
|
67
|
+
/** A digest is a coordinate, not a word: matching one matches a hash. */
|
|
68
|
+
export declare function isDigest(value: string): boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Document order, carrying what encloses what.
|
|
71
|
+
*
|
|
72
|
+
* `visit` returns the index a child should call its enclosure, which is how
|
|
73
|
+
* containment survives a tree whose intermediate nodes are nearly all
|
|
74
|
+
* scaffolding: a landmark's `within` names the nearest *landmark* above it, not
|
|
75
|
+
* the nearest node, so the depth of the wrapper stack between them costs
|
|
76
|
+
* nothing.
|
|
77
|
+
*/
|
|
78
|
+
export declare function walk(node: SemanticNode, within: number | undefined, visit: (node: SemanticNode, within: number | undefined) => number | undefined): void;
|
|
79
|
+
/**
|
|
80
|
+
* The landmark this node earns, or nothing.
|
|
81
|
+
*
|
|
82
|
+
* The test is what the node says. A text node's words belong to the element
|
|
83
|
+
* holding them, which was visited first and has already taken them — visited
|
|
84
|
+
* all the same, because the `text` bag is indexed from exactly these.
|
|
85
|
+
*/
|
|
86
|
+
export declare function landmarkFor(node: SemanticNode, within: number | undefined): Landmark | undefined;
|
|
87
|
+
/**
|
|
88
|
+
* Which landmarks survive a subject that overran the cap.
|
|
89
|
+
*
|
|
90
|
+
* Words first: a landmark with a name or a sentence is one a question can
|
|
91
|
+
* reach, and a bare `generic` with a rectangle is one nothing will ever ask
|
|
92
|
+
* for. Ties keep document order, so what survives is still an arrangement.
|
|
93
|
+
* `within` is rewritten to the nearest surviving enclosure, because an index
|
|
94
|
+
* into a list that has been cut is otherwise a pointer at the wrong thing.
|
|
95
|
+
*/
|
|
96
|
+
export declare function capLandmarks(landmarks: readonly Landmark[]): readonly Landmark[];
|
|
97
|
+
//# sourceMappingURL=landmark.d.ts.map
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { UNATTRIBUTED } from './boundary.js';
|
|
2
|
+
/**
|
|
3
|
+
* Landmarks kept per subject.
|
|
4
|
+
*
|
|
5
|
+
* Higher than the cap above on purpose. A field that loses its two hundredth
|
|
6
|
+
* distinct value loses a word the subject also says somewhere else; a subject
|
|
7
|
+
* that loses its last landmark loses a *place*, and a place has no second
|
|
8
|
+
* spelling.
|
|
9
|
+
*
|
|
10
|
+
* Four hundred was a guess and a 572-subject application falsified it: p50 9,
|
|
11
|
+
* p90 52, p99 227, and four subjects pinned at the cap, losing 1,973 places
|
|
12
|
+
* between them off screens of roughly 1,179, 926, 836 and 632. Those four are
|
|
13
|
+
* its biggest screens, which are the ones somebody most needs orienting on, and
|
|
14
|
+
* what a cap cuts there is the bottom of the page. Twelve hundred clears the
|
|
15
|
+
* largest with room and leaves the other 539 an order of magnitude below it.
|
|
16
|
+
*/
|
|
17
|
+
export const LANDMARK_CAP = 1_200;
|
|
18
|
+
/** A digest is a coordinate, not a word: matching one matches a hash. */
|
|
19
|
+
export function isDigest(value) {
|
|
20
|
+
return /^v\d+:[0-9a-f]+$/.test(value);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Document order, carrying what encloses what.
|
|
24
|
+
*
|
|
25
|
+
* `visit` returns the index a child should call its enclosure, which is how
|
|
26
|
+
* containment survives a tree whose intermediate nodes are nearly all
|
|
27
|
+
* scaffolding: a landmark's `within` names the nearest *landmark* above it, not
|
|
28
|
+
* the nearest node, so the depth of the wrapper stack between them costs
|
|
29
|
+
* nothing.
|
|
30
|
+
*/
|
|
31
|
+
export function walk(node, within, visit) {
|
|
32
|
+
const at = visit(node, within);
|
|
33
|
+
for (const child of node.children)
|
|
34
|
+
walk(child, at, visit);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* The landmark this node earns, or nothing.
|
|
38
|
+
*
|
|
39
|
+
* The test is what the node says. A text node's words belong to the element
|
|
40
|
+
* holding them, which was visited first and has already taken them — visited
|
|
41
|
+
* all the same, because the `text` bag is indexed from exactly these.
|
|
42
|
+
*/
|
|
43
|
+
export function landmarkFor(node, within) {
|
|
44
|
+
if (node.tag === '#text')
|
|
45
|
+
return undefined;
|
|
46
|
+
const text = ownTextOf(node);
|
|
47
|
+
const name = node.name ?? node.description ?? labelOf(node);
|
|
48
|
+
if (node.role === undefined && name === undefined && text === undefined)
|
|
49
|
+
return undefined;
|
|
50
|
+
return landmarkOf(node, text, name, within);
|
|
51
|
+
}
|
|
52
|
+
function landmarkOf(node, text, name, within) {
|
|
53
|
+
const source = node.provenance?.source;
|
|
54
|
+
const handle = node.attributes['data-testid'] ?? node.attributes['data-test-id'];
|
|
55
|
+
// `owners[0]` is the innermost frame: the component that rendered this node,
|
|
56
|
+
// not the one it was placed by. That is the file somebody opens.
|
|
57
|
+
const component = node.provenance?.owners?.[0]?.name;
|
|
58
|
+
return {
|
|
59
|
+
...(node.role === undefined ? {} : { role: node.role }),
|
|
60
|
+
...(name === undefined ? {} : { name }),
|
|
61
|
+
...(text === undefined ? {} : { text }),
|
|
62
|
+
...(within === undefined ? {} : { within }),
|
|
63
|
+
...(node.rect === undefined
|
|
64
|
+
? {}
|
|
65
|
+
: {
|
|
66
|
+
box: [
|
|
67
|
+
Math.round(node.rect.x),
|
|
68
|
+
Math.round(node.rect.y),
|
|
69
|
+
Math.round(node.rect.width),
|
|
70
|
+
Math.round(node.rect.height),
|
|
71
|
+
],
|
|
72
|
+
}),
|
|
73
|
+
...(source === undefined ? {} : { file: source.file, line: source.line }),
|
|
74
|
+
...(component === undefined || component === UNATTRIBUTED ? {} : { component }),
|
|
75
|
+
...(node.provenance?.createdBy === undefined ? {} : { createdBy: node.provenance.createdBy }),
|
|
76
|
+
...(handle === undefined ? {} : { handle }),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The words this node says itself.
|
|
81
|
+
*
|
|
82
|
+
* Direct text children only. A container does not inherit the sentence its
|
|
83
|
+
* button holds, because the question a landmark answers is *where does this
|
|
84
|
+
* sentence live*, and an answer that says "on the page" is not one.
|
|
85
|
+
*/
|
|
86
|
+
function ownTextOf(node) {
|
|
87
|
+
let held = '';
|
|
88
|
+
for (const child of node.children) {
|
|
89
|
+
if (child.tag !== '#text')
|
|
90
|
+
continue;
|
|
91
|
+
const text = child.text;
|
|
92
|
+
if (text === undefined || isDigest(text))
|
|
93
|
+
continue;
|
|
94
|
+
held = held === '' ? text : `${held} ${text}`;
|
|
95
|
+
}
|
|
96
|
+
const trimmed = held.replace(/\s+/g, ' ').trim();
|
|
97
|
+
return trimmed === '' ? undefined : trimmed;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* A name for a node that resolved none, from the attributes that are a label
|
|
101
|
+
* for a person rather than a value for a machine. The same three `names` reads.
|
|
102
|
+
*/
|
|
103
|
+
function labelOf(node) {
|
|
104
|
+
const held = node.attributes['placeholder'] ?? node.attributes['alt'] ?? node.attributes['title'];
|
|
105
|
+
if (held === undefined)
|
|
106
|
+
return undefined;
|
|
107
|
+
const trimmed = held.trim();
|
|
108
|
+
return trimmed === '' ? undefined : trimmed;
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Which landmarks survive a subject that overran the cap.
|
|
112
|
+
*
|
|
113
|
+
* Words first: a landmark with a name or a sentence is one a question can
|
|
114
|
+
* reach, and a bare `generic` with a rectangle is one nothing will ever ask
|
|
115
|
+
* for. Ties keep document order, so what survives is still an arrangement.
|
|
116
|
+
* `within` is rewritten to the nearest surviving enclosure, because an index
|
|
117
|
+
* into a list that has been cut is otherwise a pointer at the wrong thing.
|
|
118
|
+
*/
|
|
119
|
+
export function capLandmarks(landmarks) {
|
|
120
|
+
const worth = (landmark) => (landmark.name === undefined ? 0 : 2) + (landmark.text === undefined ? 0 : 1);
|
|
121
|
+
const keep = landmarks
|
|
122
|
+
.map((landmark, at) => ({ landmark, at }))
|
|
123
|
+
.sort((left, right) => worth(right.landmark) - worth(left.landmark) || left.at - right.at)
|
|
124
|
+
.slice(0, LANDMARK_CAP)
|
|
125
|
+
.sort((left, right) => left.at - right.at);
|
|
126
|
+
const moved = new Map();
|
|
127
|
+
keep.forEach(({ at }, to) => moved.set(at, to));
|
|
128
|
+
const enclosure = (at) => {
|
|
129
|
+
let held = at;
|
|
130
|
+
while (held !== undefined) {
|
|
131
|
+
const to = moved.get(held);
|
|
132
|
+
if (to !== undefined)
|
|
133
|
+
return to;
|
|
134
|
+
held = landmarks[held]?.within;
|
|
135
|
+
}
|
|
136
|
+
return undefined;
|
|
137
|
+
};
|
|
138
|
+
return keep.map(({ landmark }) => {
|
|
139
|
+
const within = enclosure(landmark.within);
|
|
140
|
+
const { within: _dropped, ...rest } = landmark;
|
|
141
|
+
return within === undefined ? rest : { ...rest, within };
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
//# sourceMappingURL=landmark.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"landmark.js","sourceRoot":"","sources":["../../src/attribute/landmark.ts"],"names":[],"mappings":"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","sourcesContent":["import type { SemanticNode } from '../format/snapshot.js';\nimport { UNATTRIBUTED } from './boundary.js';\n\n/**\n * Where each thing on a subject was, kept as the run read it.\n *\n * The bags next door say what a subject holds; these say where it held it, off\n * the same walk over the same tree, because two walks eventually disagree about\n * what was there. A node earns a place by bearing a role, an accessible name,\n * or words of its own — never by what it is called — so a screen written under\n * an era's worth of higher-order components reduces to the same landmarks as\n * the same screen written flat.\n */\n\n/** One thing on a subject a person could point at, where the run saw it. */\nexport interface Landmark {\n /** Accessible role, when the reading resolved one. */\n readonly role?: string;\n /** Accessible name — what a screen reader would announce. */\n readonly name?: string;\n /**\n * The words directly inside it, and only those. Text belonging to a nested\n * landmark stays on that landmark, so a container does not inherit the page.\n */\n readonly text?: string;\n /** Index of the nearest enclosing landmark. Absent on a top-level one. */\n readonly within?: number;\n /**\n * `[x, y, width, height]` in layout pixels, integers.\n *\n * Absent, never zeroed, exactly as `rect` is: a reading that did not resolve\n * layout has no opinion about what sits beneath what, and must say so rather\n * than answer from document order while sounding equally sure.\n */\n readonly box?: readonly [number, number, number, number];\n /** Where the element was written, when the project installed the plugin. */\n readonly file?: string;\n readonly line?: number;\n /**\n * The innermost component that owns it, off the fiber's owner chain.\n *\n * The line an element was written at needs the JSX-source plugin, which a\n * production build strips — and a built Storybook is one, so `file` and\n * `line` are absent on exactly the runs that matter most. The owner chain\n * survives that build: not the line, but the component whose source you would\n * open, joined to a path through `LexiconReport.declaredIn`.\n */\n readonly component?: string;\n /** The component whose JSX created it. The unit somebody owns. */\n readonly createdBy?: string;\n /** A test handle, when one was set. The one name a suite chose deliberately. */\n readonly handle?: string;\n}\n\n/**\n * Landmarks kept per subject.\n *\n * Higher than the cap above on purpose. A field that loses its two hundredth\n * distinct value loses a word the subject also says somewhere else; a subject\n * that loses its last landmark loses a *place*, and a place has no second\n * spelling.\n *\n * Four hundred was a guess and a 572-subject application falsified it: p50 9,\n * p90 52, p99 227, and four subjects pinned at the cap, losing 1,973 places\n * between them off screens of roughly 1,179, 926, 836 and 632. Those four are\n * its biggest screens, which are the ones somebody most needs orienting on, and\n * what a cap cuts there is the bottom of the page. Twelve hundred clears the\n * largest with room and leaves the other 539 an order of magnitude below it.\n */\nexport const LANDMARK_CAP = 1_200;\n\n/** A digest is a coordinate, not a word: matching one matches a hash. */\nexport function isDigest(value: string): boolean {\n return /^v\\d+:[0-9a-f]+$/.test(value);\n}\n\n/**\n * Document order, carrying what encloses what.\n *\n * `visit` returns the index a child should call its enclosure, which is how\n * containment survives a tree whose intermediate nodes are nearly all\n * scaffolding: a landmark's `within` names the nearest *landmark* above it, not\n * the nearest node, so the depth of the wrapper stack between them costs\n * nothing.\n */\nexport function walk(\n node: SemanticNode,\n within: number | undefined,\n visit: (node: SemanticNode, within: number | undefined) => number | undefined,\n): void {\n const at = visit(node, within);\n for (const child of node.children) walk(child, at, visit);\n}\n\n/**\n * The landmark this node earns, or nothing.\n *\n * The test is what the node says. A text node's words belong to the element\n * holding them, which was visited first and has already taken them — visited\n * all the same, because the `text` bag is indexed from exactly these.\n */\nexport function landmarkFor(node: SemanticNode, within: number | undefined): Landmark | undefined {\n if (node.tag === '#text') return undefined;\n const text = ownTextOf(node);\n const name = node.name ?? node.description ?? labelOf(node);\n if (node.role === undefined && name === undefined && text === undefined) return undefined;\n return landmarkOf(node, text, name, within);\n}\n\nfunction landmarkOf(\n node: SemanticNode,\n text: string | undefined,\n name: string | undefined,\n within: number | undefined,\n): Landmark {\n const source = node.provenance?.source;\n const handle = node.attributes['data-testid'] ?? node.attributes['data-test-id'];\n // `owners[0]` is the innermost frame: the component that rendered this node,\n // not the one it was placed by. That is the file somebody opens.\n const component = node.provenance?.owners?.[0]?.name;\n\n return {\n ...(node.role === undefined ? {} : { role: node.role }),\n ...(name === undefined ? {} : { name }),\n ...(text === undefined ? {} : { text }),\n ...(within === undefined ? {} : { within }),\n ...(node.rect === undefined\n ? {}\n : {\n box: [\n Math.round(node.rect.x),\n Math.round(node.rect.y),\n Math.round(node.rect.width),\n Math.round(node.rect.height),\n ] as const,\n }),\n ...(source === undefined ? {} : { file: source.file, line: source.line }),\n ...(component === undefined || component === UNATTRIBUTED ? {} : { component }),\n ...(node.provenance?.createdBy === undefined ? {} : { createdBy: node.provenance.createdBy }),\n ...(handle === undefined ? {} : { handle }),\n };\n}\n\n/**\n * The words this node says itself.\n *\n * Direct text children only. A container does not inherit the sentence its\n * button holds, because the question a landmark answers is *where does this\n * sentence live*, and an answer that says \"on the page\" is not one.\n */\nfunction ownTextOf(node: SemanticNode): string | undefined {\n let held = '';\n for (const child of node.children) {\n if (child.tag !== '#text') continue;\n const text = child.text;\n if (text === undefined || isDigest(text)) continue;\n held = held === '' ? text : `${held} ${text}`;\n }\n const trimmed = held.replace(/\\s+/g, ' ').trim();\n return trimmed === '' ? undefined : trimmed;\n}\n\n/**\n * A name for a node that resolved none, from the attributes that are a label\n * for a person rather than a value for a machine. The same three `names` reads.\n */\nfunction labelOf(node: SemanticNode): string | undefined {\n const held = node.attributes['placeholder'] ?? node.attributes['alt'] ?? node.attributes['title'];\n if (held === undefined) return undefined;\n const trimmed = held.trim();\n return trimmed === '' ? undefined : trimmed;\n}\n\n/**\n * Which landmarks survive a subject that overran the cap.\n *\n * Words first: a landmark with a name or a sentence is one a question can\n * reach, and a bare `generic` with a rectangle is one nothing will ever ask\n * for. Ties keep document order, so what survives is still an arrangement.\n * `within` is rewritten to the nearest surviving enclosure, because an index\n * into a list that has been cut is otherwise a pointer at the wrong thing.\n */\nexport function capLandmarks(landmarks: readonly Landmark[]): readonly Landmark[] {\n const worth = (landmark: Landmark): number =>\n (landmark.name === undefined ? 0 : 2) + (landmark.text === undefined ? 0 : 1);\n\n const keep = landmarks\n .map((landmark, at) => ({ landmark, at }))\n .sort((left, right) => worth(right.landmark) - worth(left.landmark) || left.at - right.at)\n .slice(0, LANDMARK_CAP)\n .sort((left, right) => left.at - right.at);\n\n const moved = new Map<number, number>();\n keep.forEach(({ at }, to) => moved.set(at, to));\n\n const enclosure = (at: number | undefined): number | undefined => {\n let held = at;\n while (held !== undefined) {\n const to = moved.get(held);\n if (to !== undefined) return to;\n held = landmarks[held]?.within;\n }\n return undefined;\n };\n\n return keep.map(({ landmark }) => {\n const within = enclosure(landmark.within);\n const { within: _dropped, ...rest } = landmark;\n return within === undefined ? rest : { ...rest, within };\n });\n}\n"]}
|