@variance-authority/core 0.1.1 → 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 +245 -10
- package/README.md +279 -110
- 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/closure.d.ts +105 -0
- package/dist/attribute/closure.js +180 -0
- package/dist/attribute/closure.js.map +1 -0
- package/dist/attribute/composition.d.ts +5 -3
- package/dist/attribute/composition.js +67 -11
- package/dist/attribute/composition.js.map +1 -1
- package/dist/attribute/divergence.js +7 -1
- package/dist/attribute/divergence.js.map +1 -1
- package/dist/attribute/index.d.ts +7 -1
- package/dist/attribute/index.js +4 -1
- 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 +112 -0
- package/dist/attribute/lexicon.js +210 -0
- package/dist/attribute/lexicon.js.map +1 -0
- package/dist/attribute/source.d.ts +19 -2
- package/dist/attribute/source.js +12 -0
- package/dist/attribute/source.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/compare/derive.d.ts +17 -0
- package/dist/compare/derive.js.map +1 -1
- package/dist/compare/diff/index.d.ts +25 -1
- package/dist/compare/diff/index.js +8 -5
- package/dist/compare/diff/index.js.map +1 -1
- package/dist/compare/explain.d.ts +1 -1
- package/dist/compare/explain.js +15 -8
- package/dist/compare/explain.js.map +1 -1
- package/dist/compare/index.d.ts +2 -2
- package/dist/compare/index.js +1 -1
- package/dist/compare/index.js.map +1 -1
- package/dist/compare/observability.d.ts +25 -1
- package/dist/compare/observability.js +54 -0
- package/dist/compare/observability.js.map +1 -1
- package/dist/compare/parting.d.ts +24 -2
- package/dist/compare/parting.js +14 -9
- package/dist/compare/parting.js.map +1 -1
- package/dist/compare/slice.d.ts +15 -2
- package/dist/compare/slice.js +6 -2
- package/dist/compare/slice.js.map +1 -1
- package/dist/format/capture.d.ts +15 -0
- package/dist/format/capture.js +30 -1
- package/dist/format/capture.js.map +1 -1
- package/dist/format/document.d.ts +65 -5
- package/dist/format/document.js +34 -0
- package/dist/format/document.js.map +1 -1
- package/dist/format/hash.d.ts +26 -0
- package/dist/format/hash.js +41 -0
- package/dist/format/hash.js.map +1 -1
- package/dist/format/index.d.ts +4 -3
- package/dist/format/index.js +3 -2
- package/dist/format/index.js.map +1 -1
- package/dist/format/sha256.d.ts +10 -2
- package/dist/format/sha256.js +42 -9
- package/dist/format/sha256.js.map +1 -1
- package/dist/format/stabilize.d.ts +15 -0
- package/dist/format/stabilize.js +22 -1
- package/dist/format/stabilize.js.map +1 -1
- package/dist/index.d.ts +3 -11
- package/dist/index.js +3 -11
- package/dist/index.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 +53 -16
- package/dist/relate/graph.js +33 -15
- package/dist/relate/graph.js.map +1 -1
- package/dist/relate/index.d.ts +3 -2
- package/dist/relate/index.js +2 -1
- package/dist/relate/index.js.map +1 -1
- package/dist/relate/merkle.d.ts +14 -5
- package/dist/relate/merkle.js +14 -6
- package/dist/relate/merkle.js.map +1 -1
- package/dist/relate/reach.d.ts +19 -5
- package/dist/relate/reach.js +10 -8
- package/dist/relate/reach.js.map +1 -1
- package/dist/relate/records.d.ts +95 -4
- package/dist/relate/records.js +93 -13
- package/dist/relate/records.js.map +1 -1
- package/dist/segment/index.d.ts +123 -0
- package/dist/segment/index.js +265 -0
- package/dist/segment/index.js.map +1 -0
- package/dist/share/index.d.ts +148 -0
- package/dist/share/index.js +190 -0
- package/dist/share/index.js.map +1 -0
- package/package.json +10 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"call-site.js","sourceRoot":"","sources":["../../src/attribute/call-site.ts"],"names":[],"mappings":"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","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"]}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { type Digest } from '../format/hash.js';
|
|
2
|
+
import type { NodePath, SemanticNode } from '../format/snapshot.js';
|
|
3
|
+
/**
|
|
4
|
+
* A digest per node over everything under it, on the document tree.
|
|
5
|
+
*
|
|
6
|
+
* The component hash deliberately is not this. It covers a component's own
|
|
7
|
+
* nodes and names a hole where a child sits, so that an edit stays local and
|
|
8
|
+
* the page root does not move on every commit. The price is a sentence no such
|
|
9
|
+
* digest can say: *this whole subtree, to the leaf, is the one on three other
|
|
10
|
+
* pages*. A closure says exactly that, and it is content-addressed the way a
|
|
11
|
+
* fingerprint is — position removed, so the same bytes at two paths in two
|
|
12
|
+
* subjects are one key — which is what lets *where else is this* become a
|
|
13
|
+
* lookup rather than a comparison.
|
|
14
|
+
*
|
|
15
|
+
* Two closures per node, at two grains of the same tree. `structure` is the
|
|
16
|
+
* tag and the children's structure closures and nothing else: the join key,
|
|
17
|
+
* because two subtrees with different shapes have nothing further to compare.
|
|
18
|
+
* `semantics` is the same walk carrying what the semantics band keeps for each
|
|
19
|
+
* node — role, accessible name, state, the surviving attributes, the text — and
|
|
20
|
+
* it qualifies a match rather than making one.
|
|
21
|
+
*
|
|
22
|
+
* ## What an id contributes
|
|
23
|
+
*
|
|
24
|
+
* An identical widget with a different generated id is the case this tree is
|
|
25
|
+
* for, so an id contributes its presence and never its value. A reference to
|
|
26
|
+
* one is a fact about a *pair* of nodes, and it enters the closure of the
|
|
27
|
+
* lowest node holding both — as the two paths relative to that node, which is
|
|
28
|
+
* position-free with respect to everything outside it. A label and the input
|
|
29
|
+
* it names read the same on every page; a label naming something outside the
|
|
30
|
+
* subtree stays an unbound reference at every closure up to the one that
|
|
31
|
+
* contains its target, and at the root if nothing does.
|
|
32
|
+
*
|
|
33
|
+
* That is the one place a closure cannot be a plain fold of its children's
|
|
34
|
+
* digests: whether a reference is bound is not a property of the node carrying
|
|
35
|
+
* it. So each subtree also carries up what it defines and what it has not yet
|
|
36
|
+
* resolved, and a binding is hashed once, where it closes.
|
|
37
|
+
*
|
|
38
|
+
* ## What enters no closure
|
|
39
|
+
*
|
|
40
|
+
* Style, geometry, tokens, wiring, holding, provenance. A closure is a
|
|
41
|
+
* statement about what was rendered, and it is computed by a run over what it
|
|
42
|
+
* already collected — it enters no baseline and no stored digest.
|
|
43
|
+
*/
|
|
44
|
+
export interface NodeClosure {
|
|
45
|
+
readonly path: NodePath;
|
|
46
|
+
readonly tag: string;
|
|
47
|
+
/** Tag and the children's structure closures, nothing else. */
|
|
48
|
+
readonly structure: Digest;
|
|
49
|
+
/** The structure walk, carrying what the semantics band keeps per node. */
|
|
50
|
+
readonly semantics: Digest;
|
|
51
|
+
/** Nodes under this one, itself included. */
|
|
52
|
+
readonly nodes: number;
|
|
53
|
+
}
|
|
54
|
+
/** One closure per node, in document order. */
|
|
55
|
+
export declare function nodeClosures(root: SemanticNode): readonly NodeClosure[];
|
|
56
|
+
/** One subject's document tree, named. */
|
|
57
|
+
export interface SubjectTree {
|
|
58
|
+
readonly subject: string;
|
|
59
|
+
readonly root: SemanticNode;
|
|
60
|
+
}
|
|
61
|
+
export interface ClosureSite {
|
|
62
|
+
readonly subject: string;
|
|
63
|
+
readonly path: NodePath;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* One structure closure that recurs across subjects, at the shallowest node
|
|
67
|
+
* where it does.
|
|
68
|
+
*/
|
|
69
|
+
export interface SharedClosure {
|
|
70
|
+
readonly structure: Digest;
|
|
71
|
+
readonly tag: string;
|
|
72
|
+
/** Nodes under each site, the site included; every site has the same count. */
|
|
73
|
+
readonly nodes: number;
|
|
74
|
+
/** Sorted by subject, then path. Spans at least two subjects. */
|
|
75
|
+
readonly sites: readonly ClosureSite[];
|
|
76
|
+
/**
|
|
77
|
+
* `held` when every site agrees on the semantics closure too — the same
|
|
78
|
+
* shape with the same content. `parted` is the same shape carrying different
|
|
79
|
+
* content, which is what the two grains exist to keep apart.
|
|
80
|
+
*/
|
|
81
|
+
readonly semantics: 'held' | 'parted';
|
|
82
|
+
}
|
|
83
|
+
export interface SharedClosureOptions {
|
|
84
|
+
/**
|
|
85
|
+
* The smallest subtree worth reporting, in nodes. A shared `<span>` is true
|
|
86
|
+
* and says nothing; the floor is the report's to choose, and absent means
|
|
87
|
+
* every size.
|
|
88
|
+
*/
|
|
89
|
+
readonly floor?: number;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Every structure closure that recurs in more than one subject, reported once
|
|
93
|
+
* and at its widest.
|
|
94
|
+
*
|
|
95
|
+
* The two-subject rule is composition's: three identical rows in one list say
|
|
96
|
+
* nothing, and the same rows in two subjects say that two diffs are one review.
|
|
97
|
+
*
|
|
98
|
+
* A site is dropped when its parent recurs over exactly the same partners — the
|
|
99
|
+
* rule that makes an echo's example the shallowest boundary, applied to every
|
|
100
|
+
* node. A whole page rendered twice is one entry at the root, not one per
|
|
101
|
+
* element. A subtree that recurs in *more* places than its parent does is kept,
|
|
102
|
+
* because the extra places are the finding.
|
|
103
|
+
*/
|
|
104
|
+
export declare function sharedClosures(subjects: readonly SubjectTree[], options?: SharedClosureOptions): readonly SharedClosure[];
|
|
105
|
+
//# sourceMappingURL=closure.d.ts.map
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { digestCombine, digestString, digestValue } from '../format/hash.js';
|
|
2
|
+
import { ID_REFERENCE_ATTRIBUTES, ID_REFERENCE_LIST_ATTRIBUTES } from '../rules/ruleset.js';
|
|
3
|
+
/** One closure per node, in document order. */
|
|
4
|
+
export function nodeClosures(root) {
|
|
5
|
+
const found = [];
|
|
6
|
+
const flatten = (closed) => {
|
|
7
|
+
found.push(closed.closure);
|
|
8
|
+
for (const child of closed.children)
|
|
9
|
+
flatten(child);
|
|
10
|
+
};
|
|
11
|
+
flatten(close(root));
|
|
12
|
+
return found;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Every structure closure that recurs in more than one subject, reported once
|
|
16
|
+
* and at its widest.
|
|
17
|
+
*
|
|
18
|
+
* The two-subject rule is composition's: three identical rows in one list say
|
|
19
|
+
* nothing, and the same rows in two subjects say that two diffs are one review.
|
|
20
|
+
*
|
|
21
|
+
* A site is dropped when its parent recurs over exactly the same partners — the
|
|
22
|
+
* rule that makes an echo's example the shallowest boundary, applied to every
|
|
23
|
+
* node. A whole page rendered twice is one entry at the root, not one per
|
|
24
|
+
* element. A subtree that recurs in *more* places than its parent does is kept,
|
|
25
|
+
* because the extra places are the finding.
|
|
26
|
+
*/
|
|
27
|
+
export function sharedClosures(subjects, options = {}) {
|
|
28
|
+
const index = new Map();
|
|
29
|
+
const file = (subject, closed, parent) => {
|
|
30
|
+
const { closure } = closed;
|
|
31
|
+
let entry = index.get(closure.structure);
|
|
32
|
+
if (entry === undefined) {
|
|
33
|
+
entry = { tag: closure.tag, nodes: closure.nodes, sites: [] };
|
|
34
|
+
index.set(closure.structure, entry);
|
|
35
|
+
}
|
|
36
|
+
entry.sites.push({ subject, path: closure.path, semantics: closure.semantics, parent });
|
|
37
|
+
const here = { structure: closure.structure, site: `${subject}\u0000${closure.path}` };
|
|
38
|
+
for (const child of closed.children)
|
|
39
|
+
file(subject, child, here);
|
|
40
|
+
};
|
|
41
|
+
for (const { subject, root } of subjects)
|
|
42
|
+
file(subject, close(root), undefined);
|
|
43
|
+
const shared = [];
|
|
44
|
+
for (const [structure, entry] of index) {
|
|
45
|
+
if (entry.nodes < (options.floor ?? 0))
|
|
46
|
+
continue;
|
|
47
|
+
if (new Set(entry.sites.map((site) => site.subject)).size < 2)
|
|
48
|
+
continue;
|
|
49
|
+
if (subsumed(entry, index))
|
|
50
|
+
continue;
|
|
51
|
+
const sites = entry.sites
|
|
52
|
+
.map(({ subject, path }) => ({ subject, path }))
|
|
53
|
+
.sort((left, right) => codeUnitOrder(left.subject, right.subject) || codeUnitOrder(left.path, right.path));
|
|
54
|
+
const held = new Set(entry.sites.map((site) => site.semantics)).size === 1;
|
|
55
|
+
shared.push({ structure, tag: entry.tag, nodes: entry.nodes, sites, semantics: held ? 'held' : 'parted' });
|
|
56
|
+
}
|
|
57
|
+
return shared.sort((left, right) => codeUnitOrder(left.structure, right.structure));
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Whether every site sits under one recurring parent, and that parent recurs
|
|
61
|
+
* nowhere else. Two identical rows under one `<tbody>` are one parent site, so
|
|
62
|
+
* the parents are counted distinct: the rows are subsumed by the body, and the
|
|
63
|
+
* body by the table above it, up to the node that recurs on its own.
|
|
64
|
+
*/
|
|
65
|
+
function subsumed(entry, index) {
|
|
66
|
+
const first = entry.sites[0]?.parent;
|
|
67
|
+
if (first === undefined)
|
|
68
|
+
return false;
|
|
69
|
+
const parents = new Set();
|
|
70
|
+
for (const site of entry.sites) {
|
|
71
|
+
if (site.parent === undefined || site.parent.structure !== first.structure)
|
|
72
|
+
return false;
|
|
73
|
+
parents.add(site.parent.site);
|
|
74
|
+
}
|
|
75
|
+
return index.get(first.structure)?.sites.length === parents.size;
|
|
76
|
+
}
|
|
77
|
+
function close(node) {
|
|
78
|
+
const children = node.children.map((child) => close(child));
|
|
79
|
+
const defined = new Map();
|
|
80
|
+
if (node.alias !== undefined)
|
|
81
|
+
defined.set(node.alias, '');
|
|
82
|
+
children.forEach((child, index) => {
|
|
83
|
+
for (const [alias, path] of child.defined)
|
|
84
|
+
defined.set(alias, under(index, path));
|
|
85
|
+
});
|
|
86
|
+
const bindings = [];
|
|
87
|
+
const unbound = [];
|
|
88
|
+
const settle = (reference) => {
|
|
89
|
+
const to = defined.get(reference.alias);
|
|
90
|
+
if (to === undefined)
|
|
91
|
+
unbound.push(reference);
|
|
92
|
+
else
|
|
93
|
+
bindings.push({ attribute: reference.attribute, from: reference.from, to });
|
|
94
|
+
};
|
|
95
|
+
for (const reference of referencesOf(node))
|
|
96
|
+
settle(reference);
|
|
97
|
+
children.forEach((child, index) => {
|
|
98
|
+
for (const reference of child.unbound)
|
|
99
|
+
settle({ ...reference, from: under(index, reference.from) });
|
|
100
|
+
});
|
|
101
|
+
bindings.sort((left, right) => codeUnitOrder(left.from, right.from) ||
|
|
102
|
+
codeUnitOrder(left.attribute, right.attribute) ||
|
|
103
|
+
codeUnitOrder(left.to, right.to));
|
|
104
|
+
let nodes = 1;
|
|
105
|
+
for (const child of children)
|
|
106
|
+
nodes += child.closure.nodes;
|
|
107
|
+
const structure = digestCombine('closure:structure', [
|
|
108
|
+
digestString(node.tag),
|
|
109
|
+
...children.map((child) => child.closure.structure),
|
|
110
|
+
]);
|
|
111
|
+
const semantics = digestCombine('closure:semantics', [
|
|
112
|
+
digestValue({ own: ownSemantics(node), bindings }),
|
|
113
|
+
...children.map((child) => child.closure.semantics),
|
|
114
|
+
]);
|
|
115
|
+
return {
|
|
116
|
+
closure: { path: node.path, tag: node.tag, structure, semantics, nodes },
|
|
117
|
+
children,
|
|
118
|
+
defined,
|
|
119
|
+
unbound,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
/** A relative path one level down: the child's index, then the path beneath it. */
|
|
123
|
+
function under(index, path) {
|
|
124
|
+
return path === '' ? String(index) : `${index}/${path}`;
|
|
125
|
+
}
|
|
126
|
+
const REFERENCE = new Set(ID_REFERENCE_ATTRIBUTES);
|
|
127
|
+
const REFERENCE_LIST = new Set(ID_REFERENCE_LIST_ATTRIBUTES);
|
|
128
|
+
/** What normalization turned an id into; an `extern:` alias lies outside every subtree. */
|
|
129
|
+
const ALIAS = /^#(?:a\d+|extern:.*)$/;
|
|
130
|
+
/**
|
|
131
|
+
* What the semantics band keeps for one node, with every id reduced to
|
|
132
|
+
* presence. `null` rather than omitted where a node has nothing, for the reason
|
|
133
|
+
* every band gives: an omitted entry lets two different nodes agree by
|
|
134
|
+
* coincidence.
|
|
135
|
+
*/
|
|
136
|
+
function ownSemantics(node) {
|
|
137
|
+
const attributes = {};
|
|
138
|
+
for (const [name, value] of Object.entries(node.attributes)) {
|
|
139
|
+
attributes[name] = isReference(name) ? withoutAliases(value) : value;
|
|
140
|
+
}
|
|
141
|
+
return {
|
|
142
|
+
tag: node.tag,
|
|
143
|
+
id: node.alias !== undefined,
|
|
144
|
+
role: node.role ?? null,
|
|
145
|
+
name: node.name ?? null,
|
|
146
|
+
state: (node.state ?? null),
|
|
147
|
+
attributes,
|
|
148
|
+
text: node.text ?? null,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
/** Every alias this node refers to, each as a reference from the node itself. */
|
|
152
|
+
function referencesOf(node) {
|
|
153
|
+
const found = [];
|
|
154
|
+
for (const [attribute, value] of Object.entries(node.attributes)) {
|
|
155
|
+
if (!isReference(attribute))
|
|
156
|
+
continue;
|
|
157
|
+
for (const alias of aliasesIn(attribute, value))
|
|
158
|
+
found.push({ attribute, alias, from: '' });
|
|
159
|
+
}
|
|
160
|
+
return found;
|
|
161
|
+
}
|
|
162
|
+
function isReference(name) {
|
|
163
|
+
return REFERENCE.has(name) || name === 'href' || name === 'xlink:href';
|
|
164
|
+
}
|
|
165
|
+
function aliasesIn(name, value) {
|
|
166
|
+
const parts = REFERENCE_LIST.has(name) ? value.split(/\s+/) : [value];
|
|
167
|
+
return parts.filter((part) => ALIAS.test(part));
|
|
168
|
+
}
|
|
169
|
+
/** The attribute with each alias replaced by a marker, so the binding carries the pair and the value does not. */
|
|
170
|
+
function withoutAliases(value) {
|
|
171
|
+
return value
|
|
172
|
+
.split(/\s+/)
|
|
173
|
+
.map((part) => (ALIAS.test(part) ? '#' : part))
|
|
174
|
+
.join(' ');
|
|
175
|
+
}
|
|
176
|
+
/** The order every other list in this package is sorted by. */
|
|
177
|
+
function codeUnitOrder(left, right) {
|
|
178
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
179
|
+
}
|
|
180
|
+
//# sourceMappingURL=closure.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"closure.js","sourceRoot":"","sources":["../../src/attribute/closure.ts"],"names":[],"mappings":"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","sourcesContent":["import type { CanonicalValue } from '../format/canonical.js';\nimport { digestCombine, digestString, digestValue, type Digest } from '../format/hash.js';\nimport type { NodePath, SemanticNode } from '../format/snapshot.js';\nimport { ID_REFERENCE_ATTRIBUTES, ID_REFERENCE_LIST_ATTRIBUTES } from '../rules/ruleset.js';\n\n/**\n * A digest per node over everything under it, on the document tree.\n *\n * The component hash deliberately is not this. It covers a component's own\n * nodes and names a hole where a child sits, so that an edit stays local and\n * the page root does not move on every commit. The price is a sentence no such\n * digest can say: *this whole subtree, to the leaf, is the one on three other\n * pages*. A closure says exactly that, and it is content-addressed the way a\n * fingerprint is — position removed, so the same bytes at two paths in two\n * subjects are one key — which is what lets *where else is this* become a\n * lookup rather than a comparison.\n *\n * Two closures per node, at two grains of the same tree. `structure` is the\n * tag and the children's structure closures and nothing else: the join key,\n * because two subtrees with different shapes have nothing further to compare.\n * `semantics` is the same walk carrying what the semantics band keeps for each\n * node — role, accessible name, state, the surviving attributes, the text — and\n * it qualifies a match rather than making one.\n *\n * ## What an id contributes\n *\n * An identical widget with a different generated id is the case this tree is\n * for, so an id contributes its presence and never its value. A reference to\n * one is a fact about a *pair* of nodes, and it enters the closure of the\n * lowest node holding both — as the two paths relative to that node, which is\n * position-free with respect to everything outside it. A label and the input\n * it names read the same on every page; a label naming something outside the\n * subtree stays an unbound reference at every closure up to the one that\n * contains its target, and at the root if nothing does.\n *\n * That is the one place a closure cannot be a plain fold of its children's\n * digests: whether a reference is bound is not a property of the node carrying\n * it. So each subtree also carries up what it defines and what it has not yet\n * resolved, and a binding is hashed once, where it closes.\n *\n * ## What enters no closure\n *\n * Style, geometry, tokens, wiring, holding, provenance. A closure is a\n * statement about what was rendered, and it is computed by a run over what it\n * already collected — it enters no baseline and no stored digest.\n */\nexport interface NodeClosure {\n readonly path: NodePath;\n readonly tag: string;\n /** Tag and the children's structure closures, nothing else. */\n readonly structure: Digest;\n /** The structure walk, carrying what the semantics band keeps per node. */\n readonly semantics: Digest;\n /** Nodes under this one, itself included. */\n readonly nodes: number;\n}\n\n/** One closure per node, in document order. */\nexport function nodeClosures(root: SemanticNode): readonly NodeClosure[] {\n const found: NodeClosure[] = [];\n const flatten = (closed: Closed): void => {\n found.push(closed.closure);\n for (const child of closed.children) flatten(child);\n };\n flatten(close(root));\n return found;\n}\n\n/** One subject's document tree, named. */\nexport interface SubjectTree {\n readonly subject: string;\n readonly root: SemanticNode;\n}\n\nexport interface ClosureSite {\n readonly subject: string;\n readonly path: NodePath;\n}\n\n/**\n * One structure closure that recurs across subjects, at the shallowest node\n * where it does.\n */\nexport interface SharedClosure {\n readonly structure: Digest;\n readonly tag: string;\n /** Nodes under each site, the site included; every site has the same count. */\n readonly nodes: number;\n /** Sorted by subject, then path. Spans at least two subjects. */\n readonly sites: readonly ClosureSite[];\n /**\n * `held` when every site agrees on the semantics closure too — the same\n * shape with the same content. `parted` is the same shape carrying different\n * content, which is what the two grains exist to keep apart.\n */\n readonly semantics: 'held' | 'parted';\n}\n\nexport interface SharedClosureOptions {\n /**\n * The smallest subtree worth reporting, in nodes. A shared `<span>` is true\n * and says nothing; the floor is the report's to choose, and absent means\n * every size.\n */\n readonly floor?: number;\n}\n\n/**\n * Every structure closure that recurs in more than one subject, reported once\n * and at its widest.\n *\n * The two-subject rule is composition's: three identical rows in one list say\n * nothing, and the same rows in two subjects say that two diffs are one review.\n *\n * A site is dropped when its parent recurs over exactly the same partners — the\n * rule that makes an echo's example the shallowest boundary, applied to every\n * node. A whole page rendered twice is one entry at the root, not one per\n * element. A subtree that recurs in *more* places than its parent does is kept,\n * because the extra places are the finding.\n */\nexport function sharedClosures(\n subjects: readonly SubjectTree[],\n options: SharedClosureOptions = {},\n): readonly SharedClosure[] {\n const index = new Map<Digest, Entry>();\n\n const file = (subject: string, closed: Closed, parent: Parent | undefined): void => {\n const { closure } = closed;\n let entry = index.get(closure.structure);\n if (entry === undefined) {\n entry = { tag: closure.tag, nodes: closure.nodes, sites: [] };\n index.set(closure.structure, entry);\n }\n entry.sites.push({ subject, path: closure.path, semantics: closure.semantics, parent });\n const here = { structure: closure.structure, site: `${subject}\\u0000${closure.path}` };\n for (const child of closed.children) file(subject, child, here);\n };\n\n for (const { subject, root } of subjects) file(subject, close(root), undefined);\n\n const shared: SharedClosure[] = [];\n for (const [structure, entry] of index) {\n if (entry.nodes < (options.floor ?? 0)) continue;\n if (new Set(entry.sites.map((site) => site.subject)).size < 2) continue;\n if (subsumed(entry, index)) continue;\n\n const sites = entry.sites\n .map(({ subject, path }) => ({ subject, path }))\n .sort((left, right) => codeUnitOrder(left.subject, right.subject) || codeUnitOrder(left.path, right.path));\n const held = new Set(entry.sites.map((site) => site.semantics)).size === 1;\n\n shared.push({ structure, tag: entry.tag, nodes: entry.nodes, sites, semantics: held ? 'held' : 'parted' });\n }\n\n return shared.sort((left, right) => codeUnitOrder(left.structure, right.structure));\n}\n\ninterface Parent {\n readonly structure: Digest;\n /** Subject and path, so that two rows under one parent count that parent once. */\n readonly site: string;\n}\n\ninterface Entry {\n readonly tag: string;\n readonly nodes: number;\n readonly sites: {\n readonly subject: string;\n readonly path: NodePath;\n readonly semantics: Digest;\n readonly parent: Parent | undefined;\n }[];\n}\n\n/**\n * Whether every site sits under one recurring parent, and that parent recurs\n * nowhere else. Two identical rows under one `<tbody>` are one parent site, so\n * the parents are counted distinct: the rows are subsumed by the body, and the\n * body by the table above it, up to the node that recurs on its own.\n */\nfunction subsumed(entry: Entry, index: ReadonlyMap<Digest, Entry>): boolean {\n const first = entry.sites[0]?.parent;\n if (first === undefined) return false;\n const parents = new Set<string>();\n for (const site of entry.sites) {\n if (site.parent === undefined || site.parent.structure !== first.structure) return false;\n parents.add(site.parent.site);\n }\n return index.get(first.structure)?.sites.length === parents.size;\n}\n\ninterface Closed {\n readonly closure: NodeClosure;\n readonly children: readonly Closed[];\n /** Alias → path relative to this node, for every id defined in this subtree. */\n readonly defined: ReadonlyMap<string, string>;\n /** References made in this subtree whose target is not in it. */\n readonly unbound: readonly Reference[];\n}\n\ninterface Reference {\n readonly attribute: string;\n readonly alias: string;\n /** Path of the referring node, relative to the subtree carrying this. */\n readonly from: string;\n}\n\n/** A reference and its target, as two paths relative to the node they close under. */\ntype Binding = {\n readonly attribute: string;\n readonly from: string;\n readonly to: string;\n};\n\nfunction close(node: SemanticNode): Closed {\n const children = node.children.map((child) => close(child));\n\n const defined = new Map<string, string>();\n if (node.alias !== undefined) defined.set(node.alias, '');\n children.forEach((child, index) => {\n for (const [alias, path] of child.defined) defined.set(alias, under(index, path));\n });\n\n const bindings: Binding[] = [];\n const unbound: Reference[] = [];\n const settle = (reference: Reference): void => {\n const to = defined.get(reference.alias);\n if (to === undefined) unbound.push(reference);\n else bindings.push({ attribute: reference.attribute, from: reference.from, to });\n };\n for (const reference of referencesOf(node)) settle(reference);\n children.forEach((child, index) => {\n for (const reference of child.unbound) settle({ ...reference, from: under(index, reference.from) });\n });\n bindings.sort(\n (left, right) =>\n codeUnitOrder(left.from, right.from) ||\n codeUnitOrder(left.attribute, right.attribute) ||\n codeUnitOrder(left.to, right.to),\n );\n\n let nodes = 1;\n for (const child of children) nodes += child.closure.nodes;\n\n const structure = digestCombine('closure:structure', [\n digestString(node.tag),\n ...children.map((child) => child.closure.structure),\n ]);\n const semantics = digestCombine('closure:semantics', [\n digestValue({ own: ownSemantics(node), bindings }),\n ...children.map((child) => child.closure.semantics),\n ]);\n\n return {\n closure: { path: node.path, tag: node.tag, structure, semantics, nodes },\n children,\n defined,\n unbound,\n };\n}\n\n/** A relative path one level down: the child's index, then the path beneath it. */\nfunction under(index: number, path: string): string {\n return path === '' ? String(index) : `${index}/${path}`;\n}\n\nconst REFERENCE = new Set(ID_REFERENCE_ATTRIBUTES);\nconst REFERENCE_LIST = new Set(ID_REFERENCE_LIST_ATTRIBUTES);\n/** What normalization turned an id into; an `extern:` alias lies outside every subtree. */\nconst ALIAS = /^#(?:a\\d+|extern:.*)$/;\n\n/**\n * What the semantics band keeps for one node, with every id reduced to\n * presence. `null` rather than omitted where a node has nothing, for the reason\n * every band gives: an omitted entry lets two different nodes agree by\n * coincidence.\n */\nfunction ownSemantics(node: SemanticNode): CanonicalValue {\n const attributes: Record<string, string> = {};\n for (const [name, value] of Object.entries(node.attributes)) {\n attributes[name] = isReference(name) ? withoutAliases(value) : value;\n }\n return {\n tag: node.tag,\n id: node.alias !== undefined,\n role: node.role ?? null,\n name: node.name ?? null,\n state: (node.state ?? null) as CanonicalValue,\n attributes,\n text: node.text ?? null,\n };\n}\n\n/** Every alias this node refers to, each as a reference from the node itself. */\nfunction referencesOf(node: SemanticNode): readonly Reference[] {\n const found: Reference[] = [];\n for (const [attribute, value] of Object.entries(node.attributes)) {\n if (!isReference(attribute)) continue;\n for (const alias of aliasesIn(attribute, value)) found.push({ attribute, alias, from: '' });\n }\n return found;\n}\n\nfunction isReference(name: string): boolean {\n return REFERENCE.has(name) || name === 'href' || name === 'xlink:href';\n}\n\nfunction aliasesIn(name: string, value: string): readonly string[] {\n const parts = REFERENCE_LIST.has(name) ? value.split(/\\s+/) : [value];\n return parts.filter((part) => ALIAS.test(part));\n}\n\n/** The attribute with each alias replaced by a marker, so the binding carries the pair and the value does not. */\nfunction withoutAliases(value: string): string {\n return value\n .split(/\\s+/)\n .map((part) => (ALIAS.test(part) ? '#' : part))\n .join(' ');\n}\n\n/** The order every other list in this package is sorted by. */\nfunction codeUnitOrder(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0;\n}\n"]}
|
|
@@ -110,9 +110,11 @@ export interface ComponentEntry {
|
|
|
110
110
|
* element. They are the same thing only for a component that authors a node of
|
|
111
111
|
* its own, and a real application is full of components that do not: measured
|
|
112
112
|
* on `examples/todomvc`, every `Chip` reports `within: ['Stack']` and
|
|
113
|
-
* `createdBy: ['TodoFooter']
|
|
114
|
-
*
|
|
115
|
-
*
|
|
113
|
+
* `createdBy: ['TodoFooter']`: the `Stack` that encloses a chip is a layout
|
|
114
|
+
* primitive that knows nothing about it, and the footer that wrote the element
|
|
115
|
+
* stands one boundary further out. A component that renders nothing but other
|
|
116
|
+
* components owns no node, is a boundary nowhere, and appears in this graph
|
|
117
|
+
* *only* here — while being the file a reviewer has to open.
|
|
116
118
|
*
|
|
117
119
|
* Empty on a production build, where `_debugOwner` is absent (ADR-0007). Empty
|
|
118
120
|
* is *not* "nothing mounted it": a component with no caller is the subject root
|
|
@@ -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.
|