@variance-authority/core 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +81 -36
- package/README.md +255 -128
- package/dist/attribute/call-site.d.ts +20 -2
- package/dist/attribute/call-site.js +17 -20
- package/dist/attribute/call-site.js.map +1 -1
- package/dist/attribute/composition.js +67 -11
- package/dist/attribute/composition.js.map +1 -1
- package/dist/attribute/index.d.ts +2 -0
- package/dist/attribute/index.js +1 -0
- package/dist/attribute/index.js.map +1 -1
- package/dist/attribute/landmark.d.ts +97 -0
- package/dist/attribute/landmark.js +144 -0
- package/dist/attribute/landmark.js.map +1 -0
- package/dist/attribute/lexicon.d.ts +25 -1
- package/dist/attribute/lexicon.js +87 -27
- package/dist/attribute/lexicon.js.map +1 -1
- package/dist/attribute/stack.d.ts +62 -1
- package/dist/attribute/stack.js +116 -5
- package/dist/attribute/stack.js.map +1 -1
- package/dist/judge/inspect.js +57 -4
- package/dist/judge/inspect.js.map +1 -1
- package/dist/relate/before.d.ts +137 -0
- package/dist/relate/before.js +155 -0
- package/dist/relate/before.js.map +1 -0
- package/dist/relate/graph.d.ts +28 -11
- package/dist/relate/graph.js +20 -9
- package/dist/relate/graph.js.map +1 -1
- package/dist/relate/index.d.ts +2 -1
- package/dist/relate/index.js +1 -0
- package/dist/relate/index.js.map +1 -1
- package/dist/relate/merkle.d.ts +6 -0
- package/dist/relate/merkle.js +6 -0
- package/dist/relate/merkle.js.map +1 -1
- package/dist/relate/records.d.ts +67 -1
- package/dist/relate/records.js +34 -7
- package/dist/relate/records.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { SubjectComposition } from './composition.js';
|
|
2
|
+
import { type Landmark } from './landmark.js';
|
|
2
3
|
/**
|
|
3
4
|
* The subject-first folds: every name a subject carries, and its tree as rows.
|
|
4
5
|
*
|
|
@@ -15,7 +16,7 @@ import type { SubjectComposition } from './composition.js';
|
|
|
15
16
|
*/
|
|
16
17
|
/** The vocabularies a subject is indexed under. Mirrors the report's `LexiconField`. */
|
|
17
18
|
export type LexiconField = 'example' | 'names' | 'text' | 'components' | 'createdBy' | 'regions' | 'files' | 'roles' | 'tokens';
|
|
18
|
-
/** One subject's names, per field
|
|
19
|
+
/** One subject's names, per field, and where each thing was. */
|
|
19
20
|
export interface SubjectLexicon {
|
|
20
21
|
readonly subject: string;
|
|
21
22
|
/** Attributed boundaries. The only number a rank may read. */
|
|
@@ -24,6 +25,21 @@ export interface SubjectLexicon {
|
|
|
24
25
|
readonly terms: Partial<Record<LexiconField, readonly string[]>>;
|
|
25
26
|
/** Distinct values a field's cap left out, where it cut. */
|
|
26
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;
|
|
27
43
|
}
|
|
28
44
|
/** What the fold cannot read off the instances and the snapshot. */
|
|
29
45
|
export interface LexiconOptions {
|
|
@@ -41,6 +57,14 @@ export interface LexiconOptions {
|
|
|
41
57
|
/**
|
|
42
58
|
* Distinct values kept per field. Generous, and what it cuts is counted in
|
|
43
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.
|
|
44
68
|
*/
|
|
45
69
|
export declare const LEXICON_CAP = 200;
|
|
46
70
|
/**
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { attributed } from './instances.js';
|
|
2
|
+
import { LANDMARK_CAP, capLandmarks, isDigest, landmarkFor, walk, } from './landmark.js';
|
|
2
3
|
/**
|
|
3
4
|
* Distinct values kept per field. Generous, and what it cuts is counted in
|
|
4
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.
|
|
5
14
|
*/
|
|
6
15
|
export const LEXICON_CAP = 200;
|
|
7
16
|
/**
|
|
@@ -25,7 +34,53 @@ export const LEXICON_CAP = 200;
|
|
|
25
34
|
* always measured by the same instrument.
|
|
26
35
|
*/
|
|
27
36
|
export function lexiconOf(subjects, options = {}) {
|
|
28
|
-
|
|
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
|
+
{
|
|
29
84
|
const fields = new Map();
|
|
30
85
|
const add = (field, value) => {
|
|
31
86
|
if (value === undefined)
|
|
@@ -51,8 +106,9 @@ export function lexiconOf(subjects, options = {}) {
|
|
|
51
106
|
add('example', component);
|
|
52
107
|
for (const region of options.regions?.get(subject.subject) ?? [])
|
|
53
108
|
add('regions', region);
|
|
109
|
+
const landmarks = [];
|
|
54
110
|
if (subject.snapshot !== undefined) {
|
|
55
|
-
walk(subject.snapshot.root, (node) => {
|
|
111
|
+
walk(subject.snapshot.root, undefined, (node, within) => {
|
|
56
112
|
add('roles', node.role);
|
|
57
113
|
add('names', node.name);
|
|
58
114
|
add('names', node.description);
|
|
@@ -64,26 +120,38 @@ export function lexiconOf(subjects, options = {}) {
|
|
|
64
120
|
add('names', node.attributes['title']);
|
|
65
121
|
add('text', node.text);
|
|
66
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;
|
|
67
130
|
});
|
|
68
131
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
+
}
|
|
79
152
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
boundaries: held.length,
|
|
83
|
-
terms,
|
|
84
|
-
...(Object.keys(elided).length === 0 ? {} : { elided }),
|
|
85
|
-
};
|
|
86
|
-
});
|
|
153
|
+
}
|
|
154
|
+
return holders;
|
|
87
155
|
}
|
|
88
156
|
/**
|
|
89
157
|
* One subject's boundaries as rows, document order, identical rows folded.
|
|
@@ -135,14 +203,6 @@ const FIELDS = [
|
|
|
135
203
|
'tokens',
|
|
136
204
|
];
|
|
137
205
|
const UNKNOWN = '(unknown props)';
|
|
138
|
-
function isDigest(value) {
|
|
139
|
-
return /^v\d+:[0-9a-f]+$/.test(value);
|
|
140
|
-
}
|
|
141
|
-
function walk(node, visit) {
|
|
142
|
-
visit(node);
|
|
143
|
-
for (const child of node.children)
|
|
144
|
-
walk(child, visit);
|
|
145
|
-
}
|
|
146
206
|
/** Code-unit order: the same bytes on every machine, whatever `LANG` says. */
|
|
147
207
|
function codeUnit(left, right) {
|
|
148
208
|
return left < right ? -1 : left > right ? 1 : 0;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lexicon.js","sourceRoot":"","sources":["../../src/attribute/lexicon.ts"],"names":[],"mappings":"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","sourcesContent":["import type { SemanticNode } from '../format/snapshot.js';\nimport { attributed, type ComponentInstance } from './instances.js';\nimport type { SubjectComposition } from './composition.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/** One subject's names, per field. The shape the report carries verbatim. */\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/** 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 */\nexport const LEXICON_CAP = 200;\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 return subjects.map((subject) => {\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 if (subject.snapshot !== undefined) {\n walk(subject.snapshot.root, (node) => {\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 }\n\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 const sorted = [...values].sort(codeUnit);\n terms[field] = sorted.slice(0, LEXICON_CAP);\n if (sorted.length > LEXICON_CAP) elided[field] = sorted.length - LEXICON_CAP;\n }\n\n return {\n subject: subject.subject,\n boundaries: held.length,\n terms,\n ...(Object.keys(elided).length === 0 ? {} : { elided }),\n };\n });\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\nfunction isDigest(value: string): boolean {\n return /^v\\d+:[0-9a-f]+$/.test(value);\n}\n\nfunction walk(node: SemanticNode, visit: (node: SemanticNode) => void): void {\n visit(node);\n for (const child of node.children) walk(child, visit);\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"]}
|
|
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"]}
|
|
@@ -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
|
package/dist/attribute/stack.js
CHANGED
|
@@ -68,6 +68,106 @@ export function isVendorPath(path) {
|
|
|
68
68
|
path.includes('/.yarn/') ||
|
|
69
69
|
path.startsWith('webpack-internal:'));
|
|
70
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* A served URL as a path: the origin dropped, the rest kept.
|
|
73
|
+
*
|
|
74
|
+
* An origin is a fact about the machine that ran the capture rather than about
|
|
75
|
+
* the code. A dev server binds an ephemeral port and a preview server binds
|
|
76
|
+
* another, so two runs of one suite would name every location differently, and
|
|
77
|
+
* a cache-busting query would move under the same hand. What is left is what
|
|
78
|
+
* the source index and `relativizeSource` already speak.
|
|
79
|
+
*
|
|
80
|
+
* Anything that does not parse as a URL is left exactly as it is — a bundler
|
|
81
|
+
* that wrote an absolute filesystem path, or a `webpack://` specifier — because
|
|
82
|
+
* inventing a shape for those would be a guess.
|
|
83
|
+
*/
|
|
84
|
+
export function servedPath(url) {
|
|
85
|
+
if (URL_OF === undefined)
|
|
86
|
+
return url;
|
|
87
|
+
try {
|
|
88
|
+
return new URL_OF(url).pathname.replace(/^\/+/, '');
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return url;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The frame's own position on a disk, or `undefined` if a server sent it.
|
|
96
|
+
*
|
|
97
|
+
* The two arrive in the same field and mean different things when no map
|
|
98
|
+
* answers for them. A browser frame is a position in whatever text the build
|
|
99
|
+
* served, and only a map can say where that text came from. A Node frame is
|
|
100
|
+
* already a position in a file: Node applies maps to `Error.stack` itself, so a
|
|
101
|
+
* Vitest or Jest frame has been resolved before anything here sees it.
|
|
102
|
+
*
|
|
103
|
+
* Decided by the frame's own shape, because that is a property of the value in
|
|
104
|
+
* hand. Nothing here asks a filesystem whether a path is real; a coordinate is
|
|
105
|
+
* ours because of how it was obtained, and a disk cannot be asked about how.
|
|
106
|
+
*
|
|
107
|
+
* Four shapes are a disk. A bare path does not parse as a URL at all. A
|
|
108
|
+
* `file:` URL is a path spelled formally, so it is decoded back into one —
|
|
109
|
+
* whole, including the leading separator a served path sheds, because an
|
|
110
|
+
* absolute path that arrives looking repository-relative is a wrong answer that
|
|
111
|
+
* resolves. A Windows path is checked before either, because `C:\src\probe.ts`
|
|
112
|
+
* parses as a URL whose scheme is the drive letter: a test that asked only
|
|
113
|
+
* whether a frame parsed would call every frame on Windows served, and attribute
|
|
114
|
+
* nothing at all there.
|
|
115
|
+
*
|
|
116
|
+
* **The fourth shape is React's, and it is refused — measured, not assumed.**
|
|
117
|
+
* A component that ran on the server has no fiber in the browser and no module
|
|
118
|
+
* the browser was served, so React manufactures a frame on the client out of
|
|
119
|
+
* coordinates the server put on the wire:
|
|
120
|
+
* `about://React/<environment>/<url>?<n>`. It *looks* like the third shape — a
|
|
121
|
+
* `file:` URL naming a file in the repository — and it is not one. The
|
|
122
|
+
* coordinates inside it are positions in the module the **server** compiled,
|
|
123
|
+
* and that module is not the one the browser can fetch.
|
|
124
|
+
*
|
|
125
|
+
* Reading it as a path was tried against a real RSC route, and the answer was
|
|
126
|
+
* wrong in the way that is worst: plausibly. React's frame for one `<aside>`
|
|
127
|
+
* read `DocsPage.tsx:200`, in a file 146 lines long; the browser's own build of
|
|
128
|
+
* the same source put that element at line 196 of a 268-line module, and
|
|
129
|
+
* resolving *that* through its map landed on line 126 — a different element,
|
|
130
|
+
* four lines from the right answer, with nothing about it to say so.
|
|
131
|
+
*
|
|
132
|
+
* So it falls to the `file:` test below and yields nothing, which is the rule
|
|
133
|
+
* {@link writerLocationOf} already states for every served frame: a position in
|
|
134
|
+
* text the build made is a position in this repository only if a map says so.
|
|
135
|
+
* The component is still named — that comes from the owner chain, not from
|
|
136
|
+
* here — and the source index still answers where it was declared.
|
|
137
|
+
*/
|
|
138
|
+
export function writtenPath(url) {
|
|
139
|
+
if (DRIVE.test(url))
|
|
140
|
+
return url;
|
|
141
|
+
if (URL_OF === undefined)
|
|
142
|
+
return url;
|
|
143
|
+
let parsed;
|
|
144
|
+
try {
|
|
145
|
+
parsed = new URL_OF(url);
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return url;
|
|
149
|
+
}
|
|
150
|
+
if (parsed.protocol !== 'file:')
|
|
151
|
+
return undefined;
|
|
152
|
+
const path = decoded(parsed.pathname);
|
|
153
|
+
return DRIVE.test(path.slice(1)) ? path.slice(1) : path;
|
|
154
|
+
}
|
|
155
|
+
/** `C:\src\probe.ts` and `C:/src/probe.ts`, and inside a `file:` URL's path. */
|
|
156
|
+
const DRIVE = /^[A-Za-z]:[\\/]/;
|
|
157
|
+
function decoded(pathname) {
|
|
158
|
+
try {
|
|
159
|
+
return decodeURIComponent(pathname);
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return pathname;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* The one host global this file needs, asked for rather than assumed. `core`
|
|
167
|
+
* types no host library (ADR-0001), and taking a URL apart by hand is a bug
|
|
168
|
+
* farm.
|
|
169
|
+
*/
|
|
170
|
+
const URL_OF = globalThis.URL;
|
|
71
171
|
/**
|
|
72
172
|
* The location that wrote this element, chosen from its stack.
|
|
73
173
|
*
|
|
@@ -80,7 +180,9 @@ export function isVendorPath(path) {
|
|
|
80
180
|
*
|
|
81
181
|
* That is the same answer the recorded-symbol path gives, arrived at from the
|
|
82
182
|
* other side. Where `jsx-source` is installed it wins, because it is exact
|
|
83
|
-
* without a map; this is what a project that installed nothing still gets
|
|
183
|
+
* without a map; this is what a project that installed nothing still gets — as
|
|
184
|
+
* long as the build serving those frames emits maps. Where it does not, this
|
|
185
|
+
* answers `null`, and a caller with no location is a caller that says so.
|
|
84
186
|
*
|
|
85
187
|
* `originalFor` is supplied rather than performed because resolving a frame
|
|
86
188
|
* means fetching the module the browser was served, and this package may not
|
|
@@ -93,11 +195,20 @@ export function writerLocationOf(frames, originalFor) {
|
|
|
93
195
|
if (isVendorPath(frame.url))
|
|
94
196
|
continue;
|
|
95
197
|
const original = originalFor(frame);
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
198
|
+
// A served frame with no map is a position in text the build made, and only
|
|
199
|
+
// a map can say where that text came from. It is one fewer candidate rather
|
|
200
|
+
// than a location: naming it anyway would put a coordinate in this
|
|
201
|
+
// repository's basis that nothing in this repository was measured in, which
|
|
202
|
+
// reads exactly like a real one and sends a reviewer to a line at random.
|
|
203
|
+
//
|
|
204
|
+
// A frame that is a path needs no map to have been read. Node applies them
|
|
205
|
+
// to `Error.stack` before a runner ever sees it, so the position arrived
|
|
206
|
+
// already resolved and the only thing left to take off it is the origin.
|
|
99
207
|
if (original === null) {
|
|
100
|
-
|
|
208
|
+
const written = writtenPath(frame.url);
|
|
209
|
+
if (written === undefined)
|
|
210
|
+
continue;
|
|
211
|
+
return { file: written, line: frame.line, column: frame.column };
|
|
101
212
|
}
|
|
102
213
|
if (isVendorPath(original.source))
|
|
103
214
|
continue;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"stack.js","sourceRoot":"","sources":["../../src/attribute/stack.ts"],"names":[],"mappings":"AA+BA;;;;;;;GAOG;AACH,MAAM,QAAQ,GAAG,+CAA+C,CAAC;AAEjE,8EAA8E;AAC9E,MAAM,QAAQ,GAAG,kCAAkC,CAAC;AAEpD;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,gBAAgB,CAAC,KAAa;IAC5C,MAAM,MAAM,GAAiB,EAAE,CAAC;IAEhC,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACpC,MAAM,IAAI,GAAG,GAAG,CAAC,OAAO,EAAE,CAAC;QAC3B,IAAI,IAAI,KAAK,EAAE,IAAI,IAAI,CAAC,QAAQ,CAAC,WAAW,CAAC;YAAE,SAAS;QAExD,MAAM,KAAK,GAAG,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACzD,IAAI,KAAK,KAAK,IAAI;YAAE,SAAS;QAE7B,MAAM,CAAC,EAAE,IAAI,EAAE,GAAG,GAAG,EAAE,EAAE,UAAU,GAAG,EAAE,EAAE,YAAY,GAAG,EAAE,CAAC,GAAG,KAAK,CAAC;QACrE,IAAI,GAAG,KAAK,EAAE,IAAI,GAAG,CAAC,UAAU,CAAC,OAAO,CAAC;YAAE,SAAS;QAEpD,MAAM,EAAE,GAAG,MAAM,CAAC,UAAU,CAAC,CAAC;QAC9B,MAAM,MAAM,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC;QACpC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,CAAC;YAAE,SAAS;QAEzE,MAAM,CAAC,IAAI,CAAC;YACV,GAAG;YACH,IAAI,EAAE,EAAE;YACR,MAAM;YACN,GAAG,CAAC,IAAI,KAAK,SAAS,IAAI,IAAI,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACjE,CAAC,CAAC;IACL,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,UAAU,YAAY,CAAC,IAAY;IACvC,OAAO,CACL,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC;QAC7B,IAAI,CAAC,QAAQ,CAAC,cAAc,CAAC;QAC7B,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC;QACxB,IAAI,CAAC,UAAU,CAAC,mBAAmB,CAAC,CACrC,CAAC;AACJ,CAAC;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,gBAAgB,CAC9B,MAA6B,EAC7B,WAA2D;IAE3D,KAAK,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QAC3B,IAAI,YAAY,CAAC,KAAK,CAAC,GAAG,CAAC;YAAE,SAAS;QAEtC,MAAM,QAAQ,GAAG,WAAW,CAAC,KAAK,CAAC,CAAC;QAEpC,2EAA2E;QAC3E,6EAA6E;QAC7E,oDAAoD;QACpD,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;YACtB,OAAO,EAAE,IAAI,EAAE,KAAK,CAAC,GAAG,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,MAAM,EAAE,CAAC;QACrE,CAAC;QAED,IAAI,YAAY,CAAC,QAAQ,CAAC,MAAM,CAAC;YAAE,SAAS;QAE5C,OAAO,EAAE,IAAI,EAAE,QAAQ,CAAC,MAAM,EAAE,IAAI,EAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,CAAC;IACjF,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["import type { SourceLocation, StackFrame } from '../format/provenance.js';\nimport type { OriginalPosition } from './source-map.js';\n\n/**\n * Reading a call site out of a stack, and deciding which frame is the author.\n *\n * React 19 constructs an `Error` inside its own `jsx`/`jsxDEV` and keeps it on\n * every fiber. That error is a complete answer to \"which line wrote this\n * element\" that no build has to opt into: the runtime that captured it is frame\n * zero, and whoever called it is frame one. Nothing is configured, nothing is\n * installed, and the location is present in any build where React is the\n * development build — which is every dev server, every Vitest run and every Jest\n * run, because all three are what \"not production\" means.\n *\n * Two jobs live here and they are separable on purpose. Parsing a stack is a\n * fact about a JavaScript engine. Choosing *which* frame is the author is a\n * policy, and it is the part with a defensible rule rather than a heuristic:\n * the author is the first frame that maps back to code the project wrote. A\n * custom JSX runtime — Emotion, theme-ui — sits between React and the author and\n * is skipped not because it is on a list but because it resolves into\n * `node_modules`.\n */\n\n/**\n * A frame is a wire value, not a local one: the page reads it, the collector\n * resolves it, and it travels between them on `Provenance`. It is declared where\n * the rest of provenance is declared, and re-exported here because this is the\n * group that does something with it.\n */\nexport type { StackFrame };\n\n/**\n * V8: ` at App (http://host/src/probe.jsx:23:26)` — and the same without a\n * name for top-level code, which is why the parenthesised half is optional.\n *\n * `async`, `new` and `Object.<anonymous>` all land in the name, which is fine:\n * nothing here reads the name, and a name that is slightly wrong costs nothing\n * where a *position* that is slightly wrong costs a reviewer an open file.\n */\nconst V8_FRAME = /^\\s*at\\s+(?:(.+?)\\s+\\()?(.+?):(\\d+):(\\d+)\\)?$/;\n\n/** SpiderMonkey and JavaScriptCore: `App@http://host/src/probe.jsx:23:26`. */\nconst AT_FRAME = /^\\s*(?:(.*?)@)(.+?):(\\d+):(\\d+)$/;\n\n/**\n * The frames of a stack, in order, outermost call last.\n *\n * The message line is dropped, and so is any line neither engine format\n * recognises — a stack is diagnostic output rather than a data format, and a\n * frame that cannot be read is one fewer candidate rather than a failure.\n *\n * `eval` frames are deliberately not unwrapped. V8 writes them as\n * `at eval (eval at fn (http://host/a.js:1:1), <anonymous>:2:3)`, where two\n * positions are present and the inner one is meaningless outside the eval.\n * Taking the outer one would name the line that *called* `eval` as the line that\n * wrote the element, which is a confident wrong answer.\n */\nexport function parseStackFrames(stack: string): StackFrame[] {\n const frames: StackFrame[] = [];\n\n for (const raw of stack.split('\\n')) {\n const line = raw.trimEnd();\n if (line === '' || line.includes('(eval at ')) continue;\n\n const match = V8_FRAME.exec(line) ?? AT_FRAME.exec(line);\n if (match === null) continue;\n\n const [, name, url = '', lineDigits = '', columnDigits = ''] = match;\n if (url === '' || url.startsWith('data:')) continue;\n\n const at = Number(lineDigits);\n const column = Number(columnDigits);\n if (!Number.isFinite(at) || !Number.isFinite(column) || at < 1) continue;\n\n frames.push({\n url,\n line: at,\n column,\n ...(name !== undefined && name !== '' ? { function: name } : {}),\n });\n }\n\n return frames;\n}\n\n/**\n * Whether a path belongs to a dependency rather than to the project.\n *\n * Three spellings of the same fact, because three build tools spell it\n * differently: the directory itself, Vite's prebundled dependency cache, and the\n * `webpack-internal:` scheme wrapping the same directory. The test runs against\n * both the served URL — which saves fetching a map for a megabyte of vendor\n * code — and against the path the map resolves to, which is what actually\n * decides it.\n *\n * A dependency that *is* the subject cannot be attributed by this, and that is\n * the intended reading: a project reviewing its own components wants the line in\n * its own repository, and the line inside `react-dom` that rendered it is never\n * the answer to \"what changed\".\n */\nexport function isVendorPath(path: string): boolean {\n return (\n path.includes('node_modules') ||\n path.includes('/.vite/deps/') ||\n path.includes('/.yarn/') ||\n path.startsWith('webpack-internal:')\n );\n}\n\n/**\n * The location that wrote this element, chosen from its stack.\n *\n * The rule is one sentence: **the first frame that resolves to a file the\n * project wrote.** Frame zero is always React's own runtime, because React is\n * what constructed the error. Frame one is the author in an ordinary build and\n * is a custom JSX runtime in a build that has one — and this does not need to\n * know which, because Emotion resolves into `node_modules` and the component\n * does not.\n *\n * That is the same answer the recorded-symbol path gives, arrived at from the\n * other side. Where `jsx-source` is installed it wins, because it is exact\n * without a map; this is what a project that installed nothing still gets.\n *\n * `originalFor` is supplied rather than performed because resolving a frame\n * means fetching the module the browser was served, and this package may not\n * assume a network (ADR-0013). The caller owns the fetch, the cache, and the\n * decision about how long to wait; this owns which frame to ask about and when\n * to stop asking.\n */\nexport function writerLocationOf(\n frames: readonly StackFrame[],\n originalFor: (frame: StackFrame) => OriginalPosition | null,\n): SourceLocation | null {\n for (const frame of frames) {\n if (isVendorPath(frame.url)) continue;\n\n const original = originalFor(frame);\n\n // No map is not the same as a vendor frame. An unmapped application module\n // is one a build served as written — which a dev server does for plain `.js`\n // — and its own coordinates are already the answer.\n if (original === null) {\n return { file: frame.url, line: frame.line, column: frame.column };\n }\n\n if (isVendorPath(original.source)) continue;\n\n return { file: original.source, line: original.line, column: original.column };\n }\n\n return null;\n}\n"]}
|
|
1
|
+
{"version":3,"file":"stack.js","sourceRoot":"","sources":["../../src/attribute/stack.ts"],"names":[],"mappings":"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","sourcesContent":["import type { SourceLocation, StackFrame } from '../format/provenance.js';\nimport type { OriginalPosition } from './source-map.js';\n\n/**\n * Reading a call site out of a stack, and deciding which frame is the author.\n *\n * React 19 constructs an `Error` inside its own `jsx`/`jsxDEV` and keeps it on\n * every fiber. That error is a complete answer to \"which line wrote this\n * element\" that no build has to opt into: the runtime that captured it is frame\n * zero, and whoever called it is frame one. Nothing is configured, nothing is\n * installed, and the location is present in any build where React is the\n * development build — which is every dev server, every Vitest run and every Jest\n * run, because all three are what \"not production\" means.\n *\n * Two jobs live here and they are separable on purpose. Parsing a stack is a\n * fact about a JavaScript engine. Choosing *which* frame is the author is a\n * policy, and it is the part with a defensible rule rather than a heuristic:\n * the author is the first frame that maps back to code the project wrote. A\n * custom JSX runtime — Emotion, theme-ui — sits between React and the author and\n * is skipped not because it is on a list but because it resolves into\n * `node_modules`.\n */\n\n/**\n * A frame is a wire value, not a local one: the page reads it, the collector\n * resolves it, and it travels between them on `Provenance`. It is declared where\n * the rest of provenance is declared, and re-exported here because this is the\n * group that does something with it.\n */\nexport type { StackFrame };\n\n/**\n * V8: ` at App (http://host/src/probe.jsx:23:26)` — and the same without a\n * name for top-level code, which is why the parenthesised half is optional.\n *\n * `async`, `new` and `Object.<anonymous>` all land in the name, which is fine:\n * nothing here reads the name, and a name that is slightly wrong costs nothing\n * where a *position* that is slightly wrong costs a reviewer an open file.\n */\nconst V8_FRAME = /^\\s*at\\s+(?:(.+?)\\s+\\()?(.+?):(\\d+):(\\d+)\\)?$/;\n\n/** SpiderMonkey and JavaScriptCore: `App@http://host/src/probe.jsx:23:26`. */\nconst AT_FRAME = /^\\s*(?:(.*?)@)(.+?):(\\d+):(\\d+)$/;\n\n/**\n * The frames of a stack, in order, outermost call last.\n *\n * The message line is dropped, and so is any line neither engine format\n * recognises — a stack is diagnostic output rather than a data format, and a\n * frame that cannot be read is one fewer candidate rather than a failure.\n *\n * `eval` frames are deliberately not unwrapped. V8 writes them as\n * `at eval (eval at fn (http://host/a.js:1:1), <anonymous>:2:3)`, where two\n * positions are present and the inner one is meaningless outside the eval.\n * Taking the outer one would name the line that *called* `eval` as the line that\n * wrote the element, which is a confident wrong answer.\n */\nexport function parseStackFrames(stack: string): StackFrame[] {\n const frames: StackFrame[] = [];\n\n for (const raw of stack.split('\\n')) {\n const line = raw.trimEnd();\n if (line === '' || line.includes('(eval at ')) continue;\n\n const match = V8_FRAME.exec(line) ?? AT_FRAME.exec(line);\n if (match === null) continue;\n\n const [, name, url = '', lineDigits = '', columnDigits = ''] = match;\n if (url === '' || url.startsWith('data:')) continue;\n\n const at = Number(lineDigits);\n const column = Number(columnDigits);\n if (!Number.isFinite(at) || !Number.isFinite(column) || at < 1) continue;\n\n frames.push({\n url,\n line: at,\n column,\n ...(name !== undefined && name !== '' ? { function: name } : {}),\n });\n }\n\n return frames;\n}\n\n/**\n * Whether a path belongs to a dependency rather than to the project.\n *\n * Three spellings of the same fact, because three build tools spell it\n * differently: the directory itself, Vite's prebundled dependency cache, and the\n * `webpack-internal:` scheme wrapping the same directory. The test runs against\n * both the served URL — which saves fetching a map for a megabyte of vendor\n * code — and against the path the map resolves to, which is what actually\n * decides it.\n *\n * A dependency that *is* the subject cannot be attributed by this, and that is\n * the intended reading: a project reviewing its own components wants the line in\n * its own repository, and the line inside `react-dom` that rendered it is never\n * the answer to \"what changed\".\n */\nexport function isVendorPath(path: string): boolean {\n return (\n path.includes('node_modules') ||\n path.includes('/.vite/deps/') ||\n path.includes('/.yarn/') ||\n path.startsWith('webpack-internal:')\n );\n}\n\n/**\n * A served URL as a path: the origin dropped, the rest kept.\n *\n * An origin is a fact about the machine that ran the capture rather than about\n * the code. A dev server binds an ephemeral port and a preview server binds\n * another, so two runs of one suite would name every location differently, and\n * a cache-busting query would move under the same hand. What is left is what\n * the source index and `relativizeSource` already speak.\n *\n * Anything that does not parse as a URL is left exactly as it is — a bundler\n * that wrote an absolute filesystem path, or a `webpack://` specifier — because\n * inventing a shape for those would be a guess.\n */\nexport function servedPath(url: string): string {\n if (URL_OF === undefined) return url;\n try {\n return new URL_OF(url).pathname.replace(/^\\/+/, '');\n } catch {\n return url;\n }\n}\n\n/**\n * The frame's own position on a disk, or `undefined` if a server sent it.\n *\n * The two arrive in the same field and mean different things when no map\n * answers for them. A browser frame is a position in whatever text the build\n * served, and only a map can say where that text came from. A Node frame is\n * already a position in a file: Node applies maps to `Error.stack` itself, so a\n * Vitest or Jest frame has been resolved before anything here sees it.\n *\n * Decided by the frame's own shape, because that is a property of the value in\n * hand. Nothing here asks a filesystem whether a path is real; a coordinate is\n * ours because of how it was obtained, and a disk cannot be asked about how.\n *\n * Four shapes are a disk. A bare path does not parse as a URL at all. A\n * `file:` URL is a path spelled formally, so it is decoded back into one —\n * whole, including the leading separator a served path sheds, because an\n * absolute path that arrives looking repository-relative is a wrong answer that\n * resolves. A Windows path is checked before either, because `C:\\src\\probe.ts`\n * parses as a URL whose scheme is the drive letter: a test that asked only\n * whether a frame parsed would call every frame on Windows served, and attribute\n * nothing at all there.\n *\n * **The fourth shape is React's, and it is refused — measured, not assumed.**\n * A component that ran on the server has no fiber in the browser and no module\n * the browser was served, so React manufactures a frame on the client out of\n * coordinates the server put on the wire:\n * `about://React/<environment>/<url>?<n>`. It *looks* like the third shape — a\n * `file:` URL naming a file in the repository — and it is not one. The\n * coordinates inside it are positions in the module the **server** compiled,\n * and that module is not the one the browser can fetch.\n *\n * Reading it as a path was tried against a real RSC route, and the answer was\n * wrong in the way that is worst: plausibly. React's frame for one `<aside>`\n * read `DocsPage.tsx:200`, in a file 146 lines long; the browser's own build of\n * the same source put that element at line 196 of a 268-line module, and\n * resolving *that* through its map landed on line 126 — a different element,\n * four lines from the right answer, with nothing about it to say so.\n *\n * So it falls to the `file:` test below and yields nothing, which is the rule\n * {@link writerLocationOf} already states for every served frame: a position in\n * text the build made is a position in this repository only if a map says so.\n * The component is still named — that comes from the owner chain, not from\n * here — and the source index still answers where it was declared.\n */\nexport function writtenPath(url: string): string | undefined {\n if (DRIVE.test(url)) return url;\n if (URL_OF === undefined) return url;\n\n let parsed;\n try {\n parsed = new URL_OF(url);\n } catch {\n return url;\n }\n\n if (parsed.protocol !== 'file:') return undefined;\n const path = decoded(parsed.pathname);\n return DRIVE.test(path.slice(1)) ? path.slice(1) : path;\n}\n\n/** `C:\\src\\probe.ts` and `C:/src/probe.ts`, and inside a `file:` URL's path. */\nconst DRIVE = /^[A-Za-z]:[\\\\/]/;\n\nfunction decoded(pathname: string): string {\n try {\n return decodeURIComponent(pathname);\n } catch {\n return pathname;\n }\n}\n\n/**\n * The one host global this file needs, asked for rather than assumed. `core`\n * types no host library (ADR-0001), and taking a URL apart by hand is a bug\n * farm.\n */\nconst URL_OF = (globalThis as { URL?: new (url: string) => ParsedUrl }).URL;\n\n/** As much of a parsed URL as this file reads. See {@link URL_OF}. */\ninterface ParsedUrl {\n readonly pathname: string;\n readonly protocol: string;\n}\n\n/**\n * The location that wrote this element, chosen from its stack.\n *\n * The rule is one sentence: **the first frame that resolves to a file the\n * project wrote.** Frame zero is always React's own runtime, because React is\n * what constructed the error. Frame one is the author in an ordinary build and\n * is a custom JSX runtime in a build that has one — and this does not need to\n * know which, because Emotion resolves into `node_modules` and the component\n * does not.\n *\n * That is the same answer the recorded-symbol path gives, arrived at from the\n * other side. Where `jsx-source` is installed it wins, because it is exact\n * without a map; this is what a project that installed nothing still gets — as\n * long as the build serving those frames emits maps. Where it does not, this\n * answers `null`, and a caller with no location is a caller that says so.\n *\n * `originalFor` is supplied rather than performed because resolving a frame\n * means fetching the module the browser was served, and this package may not\n * assume a network (ADR-0013). The caller owns the fetch, the cache, and the\n * decision about how long to wait; this owns which frame to ask about and when\n * to stop asking.\n */\nexport function writerLocationOf(\n frames: readonly StackFrame[],\n originalFor: (frame: StackFrame) => OriginalPosition | null,\n): SourceLocation | null {\n for (const frame of frames) {\n if (isVendorPath(frame.url)) continue;\n\n const original = originalFor(frame);\n\n // A served frame with no map is a position in text the build made, and only\n // a map can say where that text came from. It is one fewer candidate rather\n // than a location: naming it anyway would put a coordinate in this\n // repository's basis that nothing in this repository was measured in, which\n // reads exactly like a real one and sends a reviewer to a line at random.\n //\n // A frame that is a path needs no map to have been read. Node applies them\n // to `Error.stack` before a runner ever sees it, so the position arrived\n // already resolved and the only thing left to take off it is the origin.\n if (original === null) {\n const written = writtenPath(frame.url);\n if (written === undefined) continue;\n return { file: written, line: frame.line, column: frame.column };\n }\n\n if (isVendorPath(original.source)) continue;\n\n return { file: original.source, line: original.line, column: original.column };\n }\n\n return null;\n}\n"]}
|