@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.
@@ -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. The shape the report carries verbatim. */
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
- return subjects.map((subject) => {
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
- const terms = {};
70
- const elided = {};
71
- for (const field of FIELDS) {
72
- const values = fields.get(field);
73
- if (values === undefined || values.size === 0)
74
- continue;
75
- const sorted = [...values].sort(codeUnit);
76
- terms[field] = sorted.slice(0, LEXICON_CAP);
77
- if (sorted.length > LEXICON_CAP)
78
- elided[field] = sorted.length - LEXICON_CAP;
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
- return {
81
- subject: subject.subject,
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
@@ -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
- // No map is not the same as a vendor frame. An unmapped application module
97
- // is one a build served as written — which a dev server does for plain `.js`
98
- // — and its own coordinates are already the answer.
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
- return { file: frame.url, line: frame.line, column: frame.column };
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"]}