@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
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { SubjectComposition } from './composition.js';
|
|
2
|
+
import { type Landmark } from './landmark.js';
|
|
3
|
+
/**
|
|
4
|
+
* The subject-first folds: every name a subject carries, and its tree as rows.
|
|
5
|
+
*
|
|
6
|
+
* `composeSubjects` folds the suite component-first, which is the axis a
|
|
7
|
+
* *finding* wants: which component, in how many subjects, mounted by whom. The
|
|
8
|
+
* question an agent asks on arrival runs the other way: *which subject is the
|
|
9
|
+
* one I mean*, and *what is that subject made of*. Both are one pass over the
|
|
10
|
+
* same instance lists, done here so that whoever holds the lists, the run or a
|
|
11
|
+
* test holding snapshots, gets the same rows the report carries.
|
|
12
|
+
*
|
|
13
|
+
* Neither fold ranks anything. The lexicon is values per field and the structure
|
|
14
|
+
* is rows in document order; what a reader does with them is the reader's rule,
|
|
15
|
+
* and it is printed where it is applied.
|
|
16
|
+
*/
|
|
17
|
+
/** The vocabularies a subject is indexed under. Mirrors the report's `LexiconField`. */
|
|
18
|
+
export type LexiconField = 'example' | 'names' | 'text' | 'components' | 'createdBy' | 'regions' | 'files' | 'roles' | 'tokens';
|
|
19
|
+
/** One subject's names, per field, and where each thing was. */
|
|
20
|
+
export interface SubjectLexicon {
|
|
21
|
+
readonly subject: string;
|
|
22
|
+
/** Attributed boundaries. The only number a rank may read. */
|
|
23
|
+
readonly boundaries: number;
|
|
24
|
+
/** Distinct values per field, code-unit sorted; a field found empty is absent. */
|
|
25
|
+
readonly terms: Partial<Record<LexiconField, readonly string[]>>;
|
|
26
|
+
/** Distinct values a field's cap left out, where it cut. */
|
|
27
|
+
readonly elided?: Partial<Record<LexiconField, number>>;
|
|
28
|
+
/**
|
|
29
|
+
* The same reading in the arrangement it was read in, document order.
|
|
30
|
+
*
|
|
31
|
+
* The fields above are what the subject *says*; this is where it said it. A
|
|
32
|
+
* bag can answer that the subject holds the word `carrier` and the word
|
|
33
|
+
* `contract` and can never answer that the second sits under the first, which
|
|
34
|
+
* is the question somebody actually arrives with. Both come out of one walk
|
|
35
|
+
* over one tree, because two walks eventually disagree about what was there.
|
|
36
|
+
*
|
|
37
|
+
* Absent on a subject with no snapshot. There is a real difference between a
|
|
38
|
+
* dialog with nothing on it and a run that never looked.
|
|
39
|
+
*/
|
|
40
|
+
readonly landmarks?: readonly Landmark[];
|
|
41
|
+
/** Landmarks the cap left out. */
|
|
42
|
+
readonly elidedLandmarks?: number;
|
|
43
|
+
}
|
|
44
|
+
/** What the fold cannot read off the instances and the snapshot. */
|
|
45
|
+
export interface LexiconOptions {
|
|
46
|
+
/**
|
|
47
|
+
* Subject → the components it is the narrow example of, from the composition.
|
|
48
|
+
* Taken rather than re-derived so the lexicon and the census cannot disagree
|
|
49
|
+
* about which story shows what.
|
|
50
|
+
*/
|
|
51
|
+
readonly examples?: ReadonlyMap<string, readonly string[]>;
|
|
52
|
+
/** Component → files declaring it, from the source index. */
|
|
53
|
+
readonly declaredIn?: ReadonlyMap<string, readonly string[]>;
|
|
54
|
+
/** Subject → lexical names of the regions its journey entered, from the journal. */
|
|
55
|
+
readonly regions?: ReadonlyMap<string, readonly string[]>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Distinct values kept per field. Generous, and what it cuts is counted in
|
|
59
|
+
* `elided`, because a cap that says nothing reads as coverage.
|
|
60
|
+
*
|
|
61
|
+
* What it keeps is the narrower promise. Where a cap fires it keeps the values
|
|
62
|
+
* the fewest other subjects hold, not the ones that sort first, because the
|
|
63
|
+
* alternative is a rule about spelling: a subject whose tree is six hundred
|
|
64
|
+
* boundaries deep under an application's worth of wrappers would keep
|
|
65
|
+
* `Anonymous` and `Connect(Account)` and drop the one component it is about,
|
|
66
|
+
* on no better ground than the alphabet. Ties keep code-unit order, so equal
|
|
67
|
+
* values still land in the same place on every machine.
|
|
68
|
+
*/
|
|
69
|
+
export declare const LEXICON_CAP = 200;
|
|
70
|
+
/**
|
|
71
|
+
* Every name each subject carries, per field.
|
|
72
|
+
*
|
|
73
|
+
* Fields come from three places and the option that supplies each says which:
|
|
74
|
+
* the instances give `components`, `createdBy` and `tokens`; the snapshot, when
|
|
75
|
+
* the subject carries one, gives `roles`, `names`, `text` and the call-site
|
|
76
|
+
* `files`; the options give `example`, declared `files` and `regions`. A subject
|
|
77
|
+
* with no snapshot still indexes under what its instances hold, which is the
|
|
78
|
+
* honest shape for a sidecar read back without its document.
|
|
79
|
+
*
|
|
80
|
+
* Digests are never values. A text the policy declared volatile reaches the
|
|
81
|
+
* snapshot as `v1:…`, and a reader that matched on it would be matching a
|
|
82
|
+
* coordinate, not a word.
|
|
83
|
+
*
|
|
84
|
+
* Values are kept as they were read and never split. Splitting is a rule, and a
|
|
85
|
+
* rule applied here would be applied to one side of a later comparison only —
|
|
86
|
+
* the reader's query would meet tokens it did not produce. So the fold stores
|
|
87
|
+
* words and the reader owns the rule, which is how a term and a value are
|
|
88
|
+
* always measured by the same instrument.
|
|
89
|
+
*/
|
|
90
|
+
export declare function lexiconOf(subjects: readonly SubjectComposition[], options?: LexiconOptions): readonly SubjectLexicon[];
|
|
91
|
+
/** Boundaries of one component at one place in a subject's tree. */
|
|
92
|
+
export interface BoundaryRow {
|
|
93
|
+
readonly component: string;
|
|
94
|
+
readonly depth: number;
|
|
95
|
+
readonly within?: string;
|
|
96
|
+
readonly createdBy?: string;
|
|
97
|
+
/** Boundaries folded into this row. */
|
|
98
|
+
readonly count: number;
|
|
99
|
+
/** Distinct props digests among them; unknown props count as one. */
|
|
100
|
+
readonly variants: number;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* One subject's boundaries as rows, document order, identical rows folded.
|
|
104
|
+
*
|
|
105
|
+
* A row's key is where it sits and who put it there: component, depth,
|
|
106
|
+
* enclosing boundary, creator. Not what it rendered, because the question this
|
|
107
|
+
* answers is *what is this subject made of* and three chips under one stack are
|
|
108
|
+
* one answer to it. What differed among them survives as `variants`, so the
|
|
109
|
+
* fold loses the digests and keeps the fact that there were several.
|
|
110
|
+
*/
|
|
111
|
+
export declare function structureOf(subject: SubjectComposition): readonly BoundaryRow[];
|
|
112
|
+
//# sourceMappingURL=lexicon.d.ts.map
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { attributed } from './instances.js';
|
|
2
|
+
import { LANDMARK_CAP, capLandmarks, isDigest, landmarkFor, walk, } from './landmark.js';
|
|
3
|
+
/**
|
|
4
|
+
* Distinct values kept per field. Generous, and what it cuts is counted in
|
|
5
|
+
* `elided`, because a cap that says nothing reads as coverage.
|
|
6
|
+
*
|
|
7
|
+
* What it keeps is the narrower promise. Where a cap fires it keeps the values
|
|
8
|
+
* the fewest other subjects hold, not the ones that sort first, because the
|
|
9
|
+
* alternative is a rule about spelling: a subject whose tree is six hundred
|
|
10
|
+
* boundaries deep under an application's worth of wrappers would keep
|
|
11
|
+
* `Anonymous` and `Connect(Account)` and drop the one component it is about,
|
|
12
|
+
* on no better ground than the alphabet. Ties keep code-unit order, so equal
|
|
13
|
+
* values still land in the same place on every machine.
|
|
14
|
+
*/
|
|
15
|
+
export const LEXICON_CAP = 200;
|
|
16
|
+
/**
|
|
17
|
+
* Every name each subject carries, per field.
|
|
18
|
+
*
|
|
19
|
+
* Fields come from three places and the option that supplies each says which:
|
|
20
|
+
* the instances give `components`, `createdBy` and `tokens`; the snapshot, when
|
|
21
|
+
* the subject carries one, gives `roles`, `names`, `text` and the call-site
|
|
22
|
+
* `files`; the options give `example`, declared `files` and `regions`. A subject
|
|
23
|
+
* with no snapshot still indexes under what its instances hold, which is the
|
|
24
|
+
* honest shape for a sidecar read back without its document.
|
|
25
|
+
*
|
|
26
|
+
* Digests are never values. A text the policy declared volatile reaches the
|
|
27
|
+
* snapshot as `v1:…`, and a reader that matched on it would be matching a
|
|
28
|
+
* coordinate, not a word.
|
|
29
|
+
*
|
|
30
|
+
* Values are kept as they were read and never split. Splitting is a rule, and a
|
|
31
|
+
* rule applied here would be applied to one side of a later comparison only —
|
|
32
|
+
* the reader's query would meet tokens it did not produce. So the fold stores
|
|
33
|
+
* words and the reader owns the rule, which is how a term and a value are
|
|
34
|
+
* always measured by the same instrument.
|
|
35
|
+
*/
|
|
36
|
+
export function lexiconOf(subjects, options = {}) {
|
|
37
|
+
const rows = subjects.map((subject) => valuesOf(subject, options));
|
|
38
|
+
const holders = holdersOf(rows);
|
|
39
|
+
return rows.map((row, at) => {
|
|
40
|
+
const subject = subjects[at];
|
|
41
|
+
const fields = row.fields;
|
|
42
|
+
const terms = {};
|
|
43
|
+
const elided = {};
|
|
44
|
+
for (const field of FIELDS) {
|
|
45
|
+
const values = fields.get(field);
|
|
46
|
+
if (values === undefined || values.size === 0)
|
|
47
|
+
continue;
|
|
48
|
+
// Code-unit first, so the result is byte-stable, and only then by how
|
|
49
|
+
// widely the suite holds the value — a `sort` that keeps the order of
|
|
50
|
+
// equal elements, which every engine's does since ES2019.
|
|
51
|
+
const sorted = [...values].sort(codeUnit);
|
|
52
|
+
const kept = sorted.length <= LEXICON_CAP
|
|
53
|
+
? sorted
|
|
54
|
+
: [...sorted]
|
|
55
|
+
.sort((left, right) => (holders.get(`${field} ${left}`) ?? 0) - (holders.get(`${field} ${right}`) ?? 0))
|
|
56
|
+
.slice(0, LEXICON_CAP)
|
|
57
|
+
.sort(codeUnit);
|
|
58
|
+
terms[field] = kept;
|
|
59
|
+
if (sorted.length > LEXICON_CAP)
|
|
60
|
+
elided[field] = sorted.length - LEXICON_CAP;
|
|
61
|
+
}
|
|
62
|
+
const found = row.landmarks;
|
|
63
|
+
const landmarks = found.length <= LANDMARK_CAP ? found : capLandmarks(found);
|
|
64
|
+
return {
|
|
65
|
+
subject: subject.subject,
|
|
66
|
+
boundaries: rows[at].boundaries,
|
|
67
|
+
terms,
|
|
68
|
+
...(Object.keys(elided).length === 0 ? {} : { elided }),
|
|
69
|
+
...(landmarks.length === 0 ? {} : { landmarks }),
|
|
70
|
+
...(found.length > LANDMARK_CAP ? { elidedLandmarks: found.length - LANDMARK_CAP } : {}),
|
|
71
|
+
};
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Every value a subject holds, per field, with nothing cut.
|
|
76
|
+
*
|
|
77
|
+
* Split from the fold above because a cap that keeps the values worth keeping
|
|
78
|
+
* has to know how widely the *suite* holds each one, and that is not a fact any
|
|
79
|
+
* single subject carries. So the pass that reads a subject and the pass that
|
|
80
|
+
* decides what survives are two passes, with the counting in between.
|
|
81
|
+
*/
|
|
82
|
+
function valuesOf(subject, options) {
|
|
83
|
+
{
|
|
84
|
+
const fields = new Map();
|
|
85
|
+
const add = (field, value) => {
|
|
86
|
+
if (value === undefined)
|
|
87
|
+
return;
|
|
88
|
+
const trimmed = value.trim();
|
|
89
|
+
if (trimmed === '' || isDigest(trimmed))
|
|
90
|
+
return;
|
|
91
|
+
let set = fields.get(field);
|
|
92
|
+
if (set === undefined)
|
|
93
|
+
fields.set(field, (set = new Set()));
|
|
94
|
+
set.add(trimmed);
|
|
95
|
+
};
|
|
96
|
+
const held = subject.instances.filter(attributed);
|
|
97
|
+
for (const instance of held) {
|
|
98
|
+
add('components', instance.component);
|
|
99
|
+
add('createdBy', instance.createdBy);
|
|
100
|
+
for (const token of instance.tokens)
|
|
101
|
+
add('tokens', token);
|
|
102
|
+
for (const file of options.declaredIn?.get(instance.component) ?? [])
|
|
103
|
+
add('files', file);
|
|
104
|
+
}
|
|
105
|
+
for (const component of options.examples?.get(subject.subject) ?? [])
|
|
106
|
+
add('example', component);
|
|
107
|
+
for (const region of options.regions?.get(subject.subject) ?? [])
|
|
108
|
+
add('regions', region);
|
|
109
|
+
const landmarks = [];
|
|
110
|
+
if (subject.snapshot !== undefined) {
|
|
111
|
+
walk(subject.snapshot.root, undefined, (node, within) => {
|
|
112
|
+
add('roles', node.role);
|
|
113
|
+
add('names', node.name);
|
|
114
|
+
add('names', node.description);
|
|
115
|
+
// The three attributes that are a label for a person rather than a
|
|
116
|
+
// value for a machine. `value` is state, `href` is a coordinate, and
|
|
117
|
+
// `class` never survives normalization at all.
|
|
118
|
+
add('names', node.attributes['placeholder']);
|
|
119
|
+
add('names', node.attributes['alt']);
|
|
120
|
+
add('names', node.attributes['title']);
|
|
121
|
+
add('text', node.text);
|
|
122
|
+
add('files', node.provenance?.source?.file);
|
|
123
|
+
// The same visit, keeping what the bags drop. See `landmarkFor`.
|
|
124
|
+
const landmark = landmarkFor(node, within);
|
|
125
|
+
if (landmark === undefined)
|
|
126
|
+
return within;
|
|
127
|
+
const at = landmarks.length;
|
|
128
|
+
landmarks.push(landmark);
|
|
129
|
+
return at;
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return { fields, landmarks, boundaries: held.length };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* How many subjects hold each value, per field.
|
|
137
|
+
*
|
|
138
|
+
* The one number that separates a name from a fixture. A value nearly every
|
|
139
|
+
* subject carries is structure — a harness wrapper, a provider, an HOC, a
|
|
140
|
+
* chunk every page imports — and a value one subject carries is what
|
|
141
|
+
* distinguishes it. Counted rather than listed, so it costs nothing to be
|
|
142
|
+
* wrong about which framework wrote the wrapper.
|
|
143
|
+
*/
|
|
144
|
+
function holdersOf(rows) {
|
|
145
|
+
const holders = new Map();
|
|
146
|
+
for (const row of rows) {
|
|
147
|
+
for (const [field, values] of row.fields) {
|
|
148
|
+
for (const value of values) {
|
|
149
|
+
const key = `${field} ${value}`;
|
|
150
|
+
holders.set(key, (holders.get(key) ?? 0) + 1);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return holders;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* One subject's boundaries as rows, document order, identical rows folded.
|
|
158
|
+
*
|
|
159
|
+
* A row's key is where it sits and who put it there: component, depth,
|
|
160
|
+
* enclosing boundary, creator. Not what it rendered, because the question this
|
|
161
|
+
* answers is *what is this subject made of* and three chips under one stack are
|
|
162
|
+
* one answer to it. What differed among them survives as `variants`, so the
|
|
163
|
+
* fold loses the digests and keeps the fact that there were several.
|
|
164
|
+
*/
|
|
165
|
+
export function structureOf(subject) {
|
|
166
|
+
const rows = new Map();
|
|
167
|
+
for (const instance of subject.instances) {
|
|
168
|
+
if (!attributed(instance))
|
|
169
|
+
continue;
|
|
170
|
+
const key = [instance.component, instance.depth, instance.within ?? '', instance.createdBy ?? '']
|
|
171
|
+
.join(' ');
|
|
172
|
+
const props = instance.props ?? UNKNOWN;
|
|
173
|
+
const held = rows.get(key);
|
|
174
|
+
if (held === undefined) {
|
|
175
|
+
rows.set(key, { row: rowOf(instance), props: new Set([props]) });
|
|
176
|
+
}
|
|
177
|
+
else {
|
|
178
|
+
held.props.add(props);
|
|
179
|
+
held.row = { ...held.row, count: held.row.count + 1 };
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return [...rows.values()].map(({ row, props }) => ({ ...row, variants: props.size }));
|
|
183
|
+
}
|
|
184
|
+
function rowOf(instance) {
|
|
185
|
+
return {
|
|
186
|
+
component: instance.component,
|
|
187
|
+
depth: instance.depth,
|
|
188
|
+
...(instance.within === undefined ? {} : { within: instance.within }),
|
|
189
|
+
...(instance.createdBy === undefined ? {} : { createdBy: instance.createdBy }),
|
|
190
|
+
count: 1,
|
|
191
|
+
variants: 1,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
const FIELDS = [
|
|
195
|
+
'example',
|
|
196
|
+
'names',
|
|
197
|
+
'text',
|
|
198
|
+
'components',
|
|
199
|
+
'createdBy',
|
|
200
|
+
'regions',
|
|
201
|
+
'files',
|
|
202
|
+
'roles',
|
|
203
|
+
'tokens',
|
|
204
|
+
];
|
|
205
|
+
const UNKNOWN = '(unknown props)';
|
|
206
|
+
/** Code-unit order: the same bytes on every machine, whatever `LANG` says. */
|
|
207
|
+
function codeUnit(left, right) {
|
|
208
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
209
|
+
}
|
|
210
|
+
//# sourceMappingURL=lexicon.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"lexicon.js","sourceRoot":"","sources":["../../src/attribute/lexicon.ts"],"names":[],"mappings":"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","sourcesContent":["import { attributed, type ComponentInstance } from './instances.js';\nimport type { SubjectComposition } from './composition.js';\nimport {\n LANDMARK_CAP,\n capLandmarks,\n isDigest,\n landmarkFor,\n walk,\n type Landmark,\n} from './landmark.js';\n\n/**\n * The subject-first folds: every name a subject carries, and its tree as rows.\n *\n * `composeSubjects` folds the suite component-first, which is the axis a\n * *finding* wants: which component, in how many subjects, mounted by whom. The\n * question an agent asks on arrival runs the other way: *which subject is the\n * one I mean*, and *what is that subject made of*. Both are one pass over the\n * same instance lists, done here so that whoever holds the lists, the run or a\n * test holding snapshots, gets the same rows the report carries.\n *\n * Neither fold ranks anything. The lexicon is values per field and the structure\n * is rows in document order; what a reader does with them is the reader's rule,\n * and it is printed where it is applied.\n */\n\n/** The vocabularies a subject is indexed under. Mirrors the report's `LexiconField`. */\nexport type LexiconField =\n | 'example'\n | 'names'\n | 'text'\n | 'components'\n | 'createdBy'\n | 'regions'\n | 'files'\n | 'roles'\n | 'tokens';\n\n\n/** One subject's names, per field, and where each thing was. */\nexport interface SubjectLexicon {\n readonly subject: string;\n /** Attributed boundaries. The only number a rank may read. */\n readonly boundaries: number;\n /** Distinct values per field, code-unit sorted; a field found empty is absent. */\n readonly terms: Partial<Record<LexiconField, readonly string[]>>;\n /** Distinct values a field's cap left out, where it cut. */\n readonly elided?: Partial<Record<LexiconField, number>>;\n\n /**\n * The same reading in the arrangement it was read in, document order.\n *\n * The fields above are what the subject *says*; this is where it said it. A\n * bag can answer that the subject holds the word `carrier` and the word\n * `contract` and can never answer that the second sits under the first, which\n * is the question somebody actually arrives with. Both come out of one walk\n * over one tree, because two walks eventually disagree about what was there.\n *\n * Absent on a subject with no snapshot. There is a real difference between a\n * dialog with nothing on it and a run that never looked.\n */\n readonly landmarks?: readonly Landmark[];\n /** Landmarks the cap left out. */\n readonly elidedLandmarks?: number;\n}\n\n/** What the fold cannot read off the instances and the snapshot. */\nexport interface LexiconOptions {\n /**\n * Subject → the components it is the narrow example of, from the composition.\n * Taken rather than re-derived so the lexicon and the census cannot disagree\n * about which story shows what.\n */\n readonly examples?: ReadonlyMap<string, readonly string[]>;\n /** Component → files declaring it, from the source index. */\n readonly declaredIn?: ReadonlyMap<string, readonly string[]>;\n /** Subject → lexical names of the regions its journey entered, from the journal. */\n readonly regions?: ReadonlyMap<string, readonly string[]>;\n}\n\n/**\n * Distinct values kept per field. Generous, and what it cuts is counted in\n * `elided`, because a cap that says nothing reads as coverage.\n *\n * What it keeps is the narrower promise. Where a cap fires it keeps the values\n * the fewest other subjects hold, not the ones that sort first, because the\n * alternative is a rule about spelling: a subject whose tree is six hundred\n * boundaries deep under an application's worth of wrappers would keep\n * `Anonymous` and `Connect(Account)` and drop the one component it is about,\n * on no better ground than the alphabet. Ties keep code-unit order, so equal\n * values still land in the same place on every machine.\n */\nexport const LEXICON_CAP = 200;\n\n\n/**\n * Every name each subject carries, per field.\n *\n * Fields come from three places and the option that supplies each says which:\n * the instances give `components`, `createdBy` and `tokens`; the snapshot, when\n * the subject carries one, gives `roles`, `names`, `text` and the call-site\n * `files`; the options give `example`, declared `files` and `regions`. A subject\n * with no snapshot still indexes under what its instances hold, which is the\n * honest shape for a sidecar read back without its document.\n *\n * Digests are never values. A text the policy declared volatile reaches the\n * snapshot as `v1:…`, and a reader that matched on it would be matching a\n * coordinate, not a word.\n *\n * Values are kept as they were read and never split. Splitting is a rule, and a\n * rule applied here would be applied to one side of a later comparison only —\n * the reader's query would meet tokens it did not produce. So the fold stores\n * words and the reader owns the rule, which is how a term and a value are\n * always measured by the same instrument.\n */\nexport function lexiconOf(\n subjects: readonly SubjectComposition[],\n options: LexiconOptions = {},\n): readonly SubjectLexicon[] {\n const rows = subjects.map((subject) => valuesOf(subject, options));\n const holders = holdersOf(rows);\n\n return rows.map((row, at) => {\n const subject = subjects[at]!;\n const fields = row.fields;\n const terms: Partial<Record<LexiconField, readonly string[]>> = {};\n const elided: Partial<Record<LexiconField, number>> = {};\n for (const field of FIELDS) {\n const values = fields.get(field);\n if (values === undefined || values.size === 0) continue;\n\n // Code-unit first, so the result is byte-stable, and only then by how\n // widely the suite holds the value — a `sort` that keeps the order of\n // equal elements, which every engine's does since ES2019.\n const sorted = [...values].sort(codeUnit);\n const kept = sorted.length <= LEXICON_CAP\n ? sorted\n : [...sorted]\n .sort((left, right) => (holders.get(`${field} ${left}`) ?? 0) - (holders.get(`${field} ${right}`) ?? 0))\n .slice(0, LEXICON_CAP)\n .sort(codeUnit);\n\n terms[field] = kept;\n if (sorted.length > LEXICON_CAP) elided[field] = sorted.length - LEXICON_CAP;\n }\n\n const found = row.landmarks;\n const landmarks = found.length <= LANDMARK_CAP ? found : capLandmarks(found);\n\n return {\n subject: subject.subject,\n boundaries: rows[at]!.boundaries,\n terms,\n ...(Object.keys(elided).length === 0 ? {} : { elided }),\n ...(landmarks.length === 0 ? {} : { landmarks }),\n ...(found.length > LANDMARK_CAP ? { elidedLandmarks: found.length - LANDMARK_CAP } : {}),\n };\n });\n}\n\n/** One subject's values per field before any cap, its landmarks, its boundary count. */\ninterface SubjectValues {\n readonly fields: ReadonlyMap<LexiconField, Set<string>>;\n readonly landmarks: readonly Landmark[];\n readonly boundaries: number;\n}\n\n/**\n * Every value a subject holds, per field, with nothing cut.\n *\n * Split from the fold above because a cap that keeps the values worth keeping\n * has to know how widely the *suite* holds each one, and that is not a fact any\n * single subject carries. So the pass that reads a subject and the pass that\n * decides what survives are two passes, with the counting in between.\n */\nfunction valuesOf(subject: SubjectComposition, options: LexiconOptions): SubjectValues {\n {\n const fields = new Map<LexiconField, Set<string>>();\n const add = (field: LexiconField, value: string | undefined) => {\n if (value === undefined) return;\n const trimmed = value.trim();\n if (trimmed === '' || isDigest(trimmed)) return;\n let set = fields.get(field);\n if (set === undefined) fields.set(field, (set = new Set()));\n set.add(trimmed);\n };\n\n const held = subject.instances.filter(attributed);\n for (const instance of held) {\n add('components', instance.component);\n add('createdBy', instance.createdBy);\n for (const token of instance.tokens) add('tokens', token);\n for (const file of options.declaredIn?.get(instance.component) ?? []) add('files', file);\n }\n for (const component of options.examples?.get(subject.subject) ?? []) add('example', component);\n for (const region of options.regions?.get(subject.subject) ?? []) add('regions', region);\n\n const landmarks: Landmark[] = [];\n if (subject.snapshot !== undefined) {\n walk(subject.snapshot.root, undefined, (node, within) => {\n add('roles', node.role);\n add('names', node.name);\n add('names', node.description);\n // The three attributes that are a label for a person rather than a\n // value for a machine. `value` is state, `href` is a coordinate, and\n // `class` never survives normalization at all.\n add('names', node.attributes['placeholder']);\n add('names', node.attributes['alt']);\n add('names', node.attributes['title']);\n add('text', node.text);\n add('files', node.provenance?.source?.file);\n\n // The same visit, keeping what the bags drop. See `landmarkFor`.\n const landmark = landmarkFor(node, within);\n if (landmark === undefined) return within;\n const at = landmarks.length;\n landmarks.push(landmark);\n return at;\n });\n }\n\n return { fields, landmarks, boundaries: held.length };\n }\n}\n\n/**\n * How many subjects hold each value, per field.\n *\n * The one number that separates a name from a fixture. A value nearly every\n * subject carries is structure — a harness wrapper, a provider, an HOC, a\n * chunk every page imports — and a value one subject carries is what\n * distinguishes it. Counted rather than listed, so it costs nothing to be\n * wrong about which framework wrote the wrapper.\n */\nfunction holdersOf(rows: readonly SubjectValues[]): ReadonlyMap<string, number> {\n const holders = new Map<string, number>();\n for (const row of rows) {\n for (const [field, values] of row.fields) {\n for (const value of values) {\n const key = `${field} ${value}`;\n holders.set(key, (holders.get(key) ?? 0) + 1);\n }\n }\n }\n return holders;\n}\n\n/** Boundaries of one component at one place in a subject's tree. */\nexport interface BoundaryRow {\n readonly component: string;\n readonly depth: number;\n readonly within?: string;\n readonly createdBy?: string;\n /** Boundaries folded into this row. */\n readonly count: number;\n /** Distinct props digests among them; unknown props count as one. */\n readonly variants: number;\n}\n\n/**\n * One subject's boundaries as rows, document order, identical rows folded.\n *\n * A row's key is where it sits and who put it there: component, depth,\n * enclosing boundary, creator. Not what it rendered, because the question this\n * answers is *what is this subject made of* and three chips under one stack are\n * one answer to it. What differed among them survives as `variants`, so the\n * fold loses the digests and keeps the fact that there were several.\n */\nexport function structureOf(subject: SubjectComposition): readonly BoundaryRow[] {\n const rows = new Map<string, { row: BoundaryRow; props: Set<string> }>();\n\n for (const instance of subject.instances) {\n if (!attributed(instance)) continue;\n const key = [instance.component, instance.depth, instance.within ?? '', instance.createdBy ?? '']\n .join(' ');\n const props = instance.props ?? UNKNOWN;\n const held = rows.get(key);\n if (held === undefined) {\n rows.set(key, { row: rowOf(instance), props: new Set([props]) });\n } else {\n held.props.add(props);\n held.row = { ...held.row, count: held.row.count + 1 };\n }\n }\n\n return [...rows.values()].map(({ row, props }) => ({ ...row, variants: props.size }));\n}\n\nfunction rowOf(instance: ComponentInstance): BoundaryRow {\n return {\n component: instance.component,\n depth: instance.depth,\n ...(instance.within === undefined ? {} : { within: instance.within }),\n ...(instance.createdBy === undefined ? {} : { createdBy: instance.createdBy }),\n count: 1,\n variants: 1,\n };\n}\n\nconst FIELDS: readonly LexiconField[] = [\n 'example',\n 'names',\n 'text',\n 'components',\n 'createdBy',\n 'regions',\n 'files',\n 'roles',\n 'tokens',\n];\n\nconst UNKNOWN = '(unknown props)';\n\n\n\n\n\n\n\n/** Code-unit order: the same bytes on every machine, whatever `LANG` says. */\nfunction codeUnit(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0;\n}\n"]}
|
|
@@ -28,8 +28,15 @@ export interface SourceRef {
|
|
|
28
28
|
/** Repository-relative, so a report is portable between machines and CI. */
|
|
29
29
|
readonly file: string;
|
|
30
30
|
readonly line: number;
|
|
31
|
-
/**
|
|
32
|
-
|
|
31
|
+
/**
|
|
32
|
+
* How the declaration was recognised. Carried so a bad match is debuggable.
|
|
33
|
+
*
|
|
34
|
+
* The first four are shapes a scan of the source text matched by name.
|
|
35
|
+
* `engine` is the one that is not a match at all: the browser was asked where
|
|
36
|
+
* the function it rendered was compiled from, and that position was mapped
|
|
37
|
+
* back to the file. It names the component that ran, whatever it was called.
|
|
38
|
+
*/
|
|
39
|
+
readonly via: 'function' | 'const' | 'class' | 'declared' | 'engine';
|
|
33
40
|
}
|
|
34
41
|
/**
|
|
35
42
|
* Component name → where it is declared.
|
|
@@ -74,4 +81,14 @@ export declare function formatSource(resolution: Resolution): string;
|
|
|
74
81
|
export declare function indexSource(file: string, contents: string): SourceIndex;
|
|
75
82
|
/** Merge per-file indexes. A name declared in several files keeps every ref. */
|
|
76
83
|
export declare function mergeSourceIndexes(indexes: readonly SourceIndex[]): SourceIndex;
|
|
84
|
+
/**
|
|
85
|
+
* One index laid over another: a name the overlay holds replaces the base's refs.
|
|
86
|
+
*
|
|
87
|
+
* Not a merge. A scan finds every file that declares a `Button`, and reports the
|
|
88
|
+
* pair as ambiguous because the text cannot say which one a page rendered. The
|
|
89
|
+
* engine can, and when it has answered for a name the scan's guesses for that
|
|
90
|
+
* name are no longer the question. A name the overlay never saw keeps what the
|
|
91
|
+
* base had, so a component the page did not mount still has a file.
|
|
92
|
+
*/
|
|
93
|
+
export declare function overlaySourceIndex(base: SourceIndex, overlay: SourceIndex): SourceIndex;
|
|
77
94
|
//# sourceMappingURL=source.d.ts.map
|
package/dist/attribute/source.js
CHANGED
|
@@ -92,4 +92,16 @@ export function mergeSourceIndexes(indexes) {
|
|
|
92
92
|
}
|
|
93
93
|
return merged;
|
|
94
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* One index laid over another: a name the overlay holds replaces the base's refs.
|
|
97
|
+
*
|
|
98
|
+
* Not a merge. A scan finds every file that declares a `Button`, and reports the
|
|
99
|
+
* pair as ambiguous because the text cannot say which one a page rendered. The
|
|
100
|
+
* engine can, and when it has answered for a name the scan's guesses for that
|
|
101
|
+
* name are no longer the question. A name the overlay never saw keeps what the
|
|
102
|
+
* base had, so a component the page did not mount still has a file.
|
|
103
|
+
*/
|
|
104
|
+
export function overlaySourceIndex(base, overlay) {
|
|
105
|
+
return { ...base, ...overlay };
|
|
106
|
+
}
|
|
95
107
|
//# sourceMappingURL=source.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"source.js","sourceRoot":"","sources":["../../src/attribute/source.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;
|
|
1
|
+
{"version":3,"file":"source.js","sourceRoot":"","sources":["../../src/attribute/source.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Connecting a change to a file.\n *\n * A report that says `Button` is one step short of useful. An agent asked to fix\n * it still has to find `Button`, and a reviewer still has to guess whether the\n * `Button` in question is the design-system one or the local one in checkout.\n * The last hop — **which file** — is what turns a finding into an edit.\n *\n * There are two mechanisms, and this file is the second one.\n *\n * The first is exact. Spec §6.1 assumes the build tells us: every JSX transform\n * already computes each element's file, line and column, and\n * `@variance-authority/jsx-source` is the `jsxImportSource` setting that keeps\n * that location as far as the fiber — React 19 drops it otherwise, on every path.\n * When it is on, a finding names the element's own line and this index is not\n * consulted.\n *\n * The second is this one, and it is what a repository that has changed nothing\n * gets. Attribution names *components*, so a **component → file** index answers\n * the question attribution asks. It is built by reading source rather than by\n * configuring a build, and it answers with where a component is *declared* —\n * coarser than a call site, and enough to open the right file.\n *\n * The index is plain data. `core` performs no I/O (ADR-0006), so building one is\n * a caller's job; resolving against one is here.\n */\n\nexport interface SourceRef {\n /** Repository-relative, so a report is portable between machines and CI. */\n readonly file: string;\n readonly line: number;\n /**\n * How the declaration was recognised. Carried so a bad match is debuggable.\n *\n * The first four are shapes a scan of the source text matched by name.\n * `engine` is the one that is not a match at all: the browser was asked where\n * the function it rendered was compiled from, and that position was mapped\n * back to the file. It names the component that ran, whatever it was called.\n */\n readonly via: 'function' | 'const' | 'class' | 'declared' | 'engine';\n}\n\n/**\n * Component name → where it is declared.\n *\n * A name may map to several files. That is not an error to be resolved by\n * picking one: two components genuinely can share a name, and silently choosing\n * the first would send an agent to edit the wrong file with full confidence.\n * Ambiguity is reported (ADR-0003's rule for overlapping projections, applied to\n * source).\n */\nexport type SourceIndex = Readonly<Record<string, readonly SourceRef[]>>;\n\nexport interface Resolution {\n readonly name: string;\n readonly refs: readonly SourceRef[];\n /** `true` when more than one file declares this name. */\n readonly ambiguous: boolean;\n}\n\nexport function resolveSource(name: string, index: SourceIndex): Resolution | null {\n const refs = index[name];\n if (refs === undefined || refs.length === 0) return null;\n\n return { name, refs, ambiguous: refs.length > 1 };\n}\n\n/**\n * `file:line`, or a disambiguating list when a name is declared more than once.\n *\n * Formatted as `path:line` because that is the form an editor, a terminal, and\n * every agent harness already know how to open.\n */\nexport function formatSource(resolution: Resolution): string {\n const [first] = resolution.refs;\n if (first === undefined) return resolution.name;\n\n const primary = `${first.file}:${first.line}`;\n if (!resolution.ambiguous) return primary;\n\n const others = resolution.refs.slice(1).map((ref) => `${ref.file}:${ref.line}`);\n return `${primary} (ambiguous — also ${others.join(', ')})`;\n}\n\n/**\n * Extract component declarations from one file's source.\n *\n * A regex scan, and the limits are worth stating rather than discovering. It\n * finds exported and local declarations in the three shapes React components are\n * written in, and it will miss a component produced by a factory, assigned\n * dynamically, or re-exported under another name. It can also match a function\n * that merely looks like a component — capitalised, declared at top level — and\n * name a non-component in a report.\n *\n * Both failures are survivable in a way a wrong file path would not be: a miss\n * degrades the report to the component name, which is what it said before, and a\n * false positive can only appear if attribution already named that identifier.\n * Parsing properly is the right answer eventually; it is not worth a parser\n * dependency to find out whether the link is useful.\n */\nexport function indexSource(file: string, contents: string): SourceIndex {\n const found: Record<string, SourceRef[]> = {};\n const lines = contents.split('\\n');\n\n const patterns: readonly { readonly re: RegExp; readonly via: SourceRef['via'] }[] = [\n { re: /^\\s*(?:export\\s+)?(?:default\\s+)?function\\s+([A-Z][A-Za-z0-9_]*)/, via: 'function' },\n { re: /^\\s*(?:export\\s+)?(?:const|let)\\s+([A-Z][A-Za-z0-9_]*)\\s*[:=]/, via: 'const' },\n { re: /^\\s*(?:export\\s+)?(?:default\\s+)?class\\s+([A-Z][A-Za-z0-9_]*)/, via: 'class' },\n ];\n\n for (const [index, line] of lines.entries()) {\n for (const { re, via } of patterns) {\n const match = re.exec(line);\n if (!match) continue;\n\n const name = match[1]!;\n (found[name] ??= []).push({ file, line: index + 1, via });\n break;\n }\n }\n\n return found;\n}\n\n/** Merge per-file indexes. A name declared in several files keeps every ref. */\nexport function mergeSourceIndexes(indexes: readonly SourceIndex[]): SourceIndex {\n const merged: Record<string, SourceRef[]> = {};\n\n for (const index of indexes) {\n for (const [name, refs] of Object.entries(index)) {\n (merged[name] ??= []).push(...refs);\n }\n }\n\n return merged;\n}\n\n/**\n * One index laid over another: a name the overlay holds replaces the base's refs.\n *\n * Not a merge. A scan finds every file that declares a `Button`, and reports the\n * pair as ambiguous because the text cannot say which one a page rendered. The\n * engine can, and when it has answered for a name the scan's guesses for that\n * name are no longer the question. A name the overlay never saw keeps what the\n * base had, so a component the page did not mount still has a file.\n */\nexport function overlaySourceIndex(base: SourceIndex, overlay: SourceIndex): SourceIndex {\n return { ...base, ...overlay };\n}\n"]}
|
|
@@ -56,6 +56,65 @@ export declare function parseStackFrames(stack: string): StackFrame[];
|
|
|
56
56
|
* the answer to "what changed".
|
|
57
57
|
*/
|
|
58
58
|
export declare function isVendorPath(path: string): boolean;
|
|
59
|
+
/**
|
|
60
|
+
* A served URL as a path: the origin dropped, the rest kept.
|
|
61
|
+
*
|
|
62
|
+
* An origin is a fact about the machine that ran the capture rather than about
|
|
63
|
+
* the code. A dev server binds an ephemeral port and a preview server binds
|
|
64
|
+
* another, so two runs of one suite would name every location differently, and
|
|
65
|
+
* a cache-busting query would move under the same hand. What is left is what
|
|
66
|
+
* the source index and `relativizeSource` already speak.
|
|
67
|
+
*
|
|
68
|
+
* Anything that does not parse as a URL is left exactly as it is — a bundler
|
|
69
|
+
* that wrote an absolute filesystem path, or a `webpack://` specifier — because
|
|
70
|
+
* inventing a shape for those would be a guess.
|
|
71
|
+
*/
|
|
72
|
+
export declare function servedPath(url: string): string;
|
|
73
|
+
/**
|
|
74
|
+
* The frame's own position on a disk, or `undefined` if a server sent it.
|
|
75
|
+
*
|
|
76
|
+
* The two arrive in the same field and mean different things when no map
|
|
77
|
+
* answers for them. A browser frame is a position in whatever text the build
|
|
78
|
+
* served, and only a map can say where that text came from. A Node frame is
|
|
79
|
+
* already a position in a file: Node applies maps to `Error.stack` itself, so a
|
|
80
|
+
* Vitest or Jest frame has been resolved before anything here sees it.
|
|
81
|
+
*
|
|
82
|
+
* Decided by the frame's own shape, because that is a property of the value in
|
|
83
|
+
* hand. Nothing here asks a filesystem whether a path is real; a coordinate is
|
|
84
|
+
* ours because of how it was obtained, and a disk cannot be asked about how.
|
|
85
|
+
*
|
|
86
|
+
* Four shapes are a disk. A bare path does not parse as a URL at all. A
|
|
87
|
+
* `file:` URL is a path spelled formally, so it is decoded back into one —
|
|
88
|
+
* whole, including the leading separator a served path sheds, because an
|
|
89
|
+
* absolute path that arrives looking repository-relative is a wrong answer that
|
|
90
|
+
* resolves. A Windows path is checked before either, because `C:\src\probe.ts`
|
|
91
|
+
* parses as a URL whose scheme is the drive letter: a test that asked only
|
|
92
|
+
* whether a frame parsed would call every frame on Windows served, and attribute
|
|
93
|
+
* nothing at all there.
|
|
94
|
+
*
|
|
95
|
+
* **The fourth shape is React's, and it is refused — measured, not assumed.**
|
|
96
|
+
* A component that ran on the server has no fiber in the browser and no module
|
|
97
|
+
* the browser was served, so React manufactures a frame on the client out of
|
|
98
|
+
* coordinates the server put on the wire:
|
|
99
|
+
* `about://React/<environment>/<url>?<n>`. It *looks* like the third shape — a
|
|
100
|
+
* `file:` URL naming a file in the repository — and it is not one. The
|
|
101
|
+
* coordinates inside it are positions in the module the **server** compiled,
|
|
102
|
+
* and that module is not the one the browser can fetch.
|
|
103
|
+
*
|
|
104
|
+
* Reading it as a path was tried against a real RSC route, and the answer was
|
|
105
|
+
* wrong in the way that is worst: plausibly. React's frame for one `<aside>`
|
|
106
|
+
* read `DocsPage.tsx:200`, in a file 146 lines long; the browser's own build of
|
|
107
|
+
* the same source put that element at line 196 of a 268-line module, and
|
|
108
|
+
* resolving *that* through its map landed on line 126 — a different element,
|
|
109
|
+
* four lines from the right answer, with nothing about it to say so.
|
|
110
|
+
*
|
|
111
|
+
* So it falls to the `file:` test below and yields nothing, which is the rule
|
|
112
|
+
* {@link writerLocationOf} already states for every served frame: a position in
|
|
113
|
+
* text the build made is a position in this repository only if a map says so.
|
|
114
|
+
* The component is still named — that comes from the owner chain, not from
|
|
115
|
+
* here — and the source index still answers where it was declared.
|
|
116
|
+
*/
|
|
117
|
+
export declare function writtenPath(url: string): string | undefined;
|
|
59
118
|
/**
|
|
60
119
|
* The location that wrote this element, chosen from its stack.
|
|
61
120
|
*
|
|
@@ -68,7 +127,9 @@ export declare function isVendorPath(path: string): boolean;
|
|
|
68
127
|
*
|
|
69
128
|
* That is the same answer the recorded-symbol path gives, arrived at from the
|
|
70
129
|
* other side. Where `jsx-source` is installed it wins, because it is exact
|
|
71
|
-
* without a map; this is what a project that installed nothing still gets
|
|
130
|
+
* without a map; this is what a project that installed nothing still gets — as
|
|
131
|
+
* long as the build serving those frames emits maps. Where it does not, this
|
|
132
|
+
* answers `null`, and a caller with no location is a caller that says so.
|
|
72
133
|
*
|
|
73
134
|
* `originalFor` is supplied rather than performed because resolving a frame
|
|
74
135
|
* means fetching the module the browser was served, and this package may not
|