@ui-manifest-json/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/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -0
- package/dist/semantics.d.ts +62 -0
- package/dist/semantics.d.ts.map +1 -0
- package/dist/semantics.js +325 -0
- package/dist/types/component.d.ts +10 -0
- package/dist/types/component.d.ts.map +1 -1
- package/dist/types/dom.d.ts +67 -0
- package/dist/types/dom.d.ts.map +1 -1
- package/dist/types/manifest.d.ts +66 -2
- package/dist/types/manifest.d.ts.map +1 -1
- package/dist/types/route.d.ts +27 -2
- package/dist/types/route.d.ts.map +1 -1
- package/dist/types/source.d.ts +25 -0
- package/dist/types/source.d.ts.map +1 -0
- package/dist/types/source.js +1 -0
- package/dist/types/uncapturable.d.ts +21 -0
- package/dist/types/uncapturable.d.ts.map +1 -0
- package/dist/types/uncapturable.js +1 -0
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export * from './types/dom.js';
|
|
2
|
+
export * from './types/source.js';
|
|
3
|
+
export * from './types/uncapturable.js';
|
|
2
4
|
export * from './types/component.js';
|
|
3
5
|
export * from './types/route.js';
|
|
4
6
|
export * from './types/provenance.js';
|
|
@@ -7,6 +9,7 @@ export * from './types/manifest.js';
|
|
|
7
9
|
export { resolveFullPaths } from './full-path.js';
|
|
8
10
|
export { collectRepoProvenance, generatorProvenance } from './provenance.js';
|
|
9
11
|
export type { CollectProvenanceOptions } from './provenance.js';
|
|
12
|
+
export { collapseDom, enrichDom, controlTypeFor, selectorCandidatesFor, testidOf, TESTID_ATTRS } from './semantics.js';
|
|
10
13
|
export { resolveRouteDependencyTree } from './resolve-tree.js';
|
|
11
14
|
export type { MatchFn } from './resolve-tree.js';
|
|
12
15
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,qBAAqB,CAAC;AACpC,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAC7E,YAAY,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAC;AAChE,OAAO,EAAE,0BAA0B,EAAE,MAAM,mBAAmB,CAAC;AAC/D,YAAY,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,gBAAgB,CAAC;AAC/B,cAAc,mBAAmB,CAAC;AAClC,cAAc,yBAAyB,CAAC;AACxC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,uBAAuB,CAAC;AACtC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,qBAAqB,CAAC;AACpC,OAAO,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClD,OAAO,EAAE,qBAAqB,EAAE,mBAAmB,EAAE,MAAM,iBAAiB,CAAC;AAC7E,YAAY,EAAE,wBAAwB,EAAE,MAAM,iBAAiB,CAAC;AAChE,OAAO,EAAE,WAAW,EAAE,SAAS,EAAE,cAAc,EAAE,qBAAqB,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AACvH,OAAO,EAAE,0BAA0B,EAAE,MAAM,mBAAmB,CAAC;AAC/D,YAAY,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export * from './types/dom.js';
|
|
2
|
+
export * from './types/source.js';
|
|
3
|
+
export * from './types/uncapturable.js';
|
|
2
4
|
export * from './types/component.js';
|
|
3
5
|
export * from './types/route.js';
|
|
4
6
|
export * from './types/provenance.js';
|
|
@@ -6,4 +8,5 @@ export * from './types/dependency-graph.js';
|
|
|
6
8
|
export * from './types/manifest.js';
|
|
7
9
|
export { resolveFullPaths } from './full-path.js';
|
|
8
10
|
export { collectRepoProvenance, generatorProvenance } from './provenance.js';
|
|
11
|
+
export { collapseDom, enrichDom, controlTypeFor, selectorCandidatesFor, testidOf, TESTID_ATTRS } from './semantics.js';
|
|
9
12
|
export { resolveRouteDependencyTree } from './resolve-tree.js';
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The pass that turns a raw parse tree into something a consumer can JOIN against a live DOM.
|
|
3
|
+
*
|
|
4
|
+
* Framework-agnostic on purpose: both extractors produce the same `DomNode` union, and every rule
|
|
5
|
+
* here reads only that union. Angular's parser and React's differ entirely in how they find a
|
|
6
|
+
* conditional; by the time a tree reaches this module the conditional is a `TemplateNode` either
|
|
7
|
+
* way.
|
|
8
|
+
*
|
|
9
|
+
* Three things happen here, and each removes work a consumer would otherwise have to do — wrongly,
|
|
10
|
+
* and once per consumer:
|
|
11
|
+
*
|
|
12
|
+
* **Static text is folded into its element.** A `<button>Sign In</button>` parses as an element
|
|
13
|
+
* with a text child. Keyed on its own, that button has no handle at all: no id, no name, no
|
|
14
|
+
* testid — which describes most buttons in most apps. Folding the text up is what makes it
|
|
15
|
+
* addressable. An interpolation is deliberately NOT folded: a key built from `{{ user.name }}`
|
|
16
|
+
* changes with the test data.
|
|
17
|
+
*
|
|
18
|
+
* **Ancestry is denormalized.** Every structural branch between the root and an element is copied
|
|
19
|
+
* onto that element as `conditionChain`. This is what lets the tree drop presentational wrappers
|
|
20
|
+
* without losing anything, and it is what turns "why is this field not on the page?" from a
|
|
21
|
+
* repository search into a field read.
|
|
22
|
+
*
|
|
23
|
+
* **Handles are enumerated, and their absence is reported.** `tokenStability: "none"` on a control
|
|
24
|
+
* means the source offers no stable way to address it. With a file and a line attached, that is
|
|
25
|
+
* the single most actionable line this whole format produces: *add a `data-testid` here*.
|
|
26
|
+
*/
|
|
27
|
+
import type { ControlType, DomNode, ElementNode, SelectorCandidate } from './types/dom.js';
|
|
28
|
+
/**
|
|
29
|
+
* Drop presentational wrappers, splicing their children into their place.
|
|
30
|
+
*
|
|
31
|
+
* Safe only because `conditionChain` is denormalized onto every element: nothing downstream needs
|
|
32
|
+
* the ancestors this removes. Run BEFORE enrichment so text folding and uniqueness both see the
|
|
33
|
+
* final tree. Returns how many nodes went, because a consumer must be able to tell that the tree
|
|
34
|
+
* it is reading is not the DOM.
|
|
35
|
+
*/
|
|
36
|
+
export declare function collapseDom(nodes: DomNode[]): {
|
|
37
|
+
dom: DomNode[];
|
|
38
|
+
collapsed: number;
|
|
39
|
+
};
|
|
40
|
+
/** The attribute names a test-id may be spelled with, in the order a consumer should prefer. */
|
|
41
|
+
export declare const TESTID_ATTRS: readonly ["data-testid", "data-test-id", "data-test", "data-qa", "data-cy"];
|
|
42
|
+
export declare function testidOf(attrs: Record<string, string>): string | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* What a person can DO with this element.
|
|
45
|
+
*
|
|
46
|
+
* Driven off the tag and `type` rather than the role attribute, because the role is usually
|
|
47
|
+
* absent and the tag almost never is. `<input type="submit">` is a BUTTON — it is spelled as an
|
|
48
|
+
* input but it submits, and treating it as a textbox is how a submit control ends up expected to
|
|
49
|
+
* accept typing.
|
|
50
|
+
*/
|
|
51
|
+
export declare function controlTypeFor(el: string, attrs: Record<string, string>): ControlType | undefined;
|
|
52
|
+
/** Every handle the SOURCE offers for this element, best first. Audit material, not a locator. */
|
|
53
|
+
export declare function selectorCandidatesFor(node: ElementNode, staticText?: string): SelectorCandidate[];
|
|
54
|
+
/**
|
|
55
|
+
* Walk a parsed tree and enrich every element in place.
|
|
56
|
+
*
|
|
57
|
+
* Returns every element it visited, in template order, so the caller can run whole-template passes
|
|
58
|
+
* (uniqueness) without walking again. Order is template order because two elements sharing a
|
|
59
|
+
* handle should collide in a stable order rather than whichever the walk happened to reach first.
|
|
60
|
+
*/
|
|
61
|
+
export declare function enrichDom(nodes: DomNode[]): ElementNode[];
|
|
62
|
+
//# sourceMappingURL=semantics.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"semantics.d.ts","sourceRoot":"","sources":["../src/semantics.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,KAAK,EACK,WAAW,EAAE,OAAO,EAAE,WAAW,EAAE,iBAAiB,EACpE,MAAM,gBAAgB,CAAC;AAoCxB;;;;;;;GAOG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG;IAAE,GAAG,EAAE,OAAO,EAAE,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CA4BnF;AAED,gGAAgG;AAChG,eAAO,MAAM,YAAY,6EAA8E,CAAC;AAExG,wBAAgB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,GAAG,SAAS,CAM1E;AAKD;;;;;;;GAOG;AACH,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,WAAW,GAAG,SAAS,CAwBjG;AAUD,kGAAkG;AAClG,wBAAgB,qBAAqB,CAAC,IAAI,EAAE,WAAW,EAAE,UAAU,CAAC,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAejG;AA2FD;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,GAAG,WAAW,EAAE,CAqEzD"}
|
|
@@ -0,0 +1,325 @@
|
|
|
1
|
+
/** Attributes that make an element addressable, and so make it worth keeping. */
|
|
2
|
+
const MEANINGFUL_ATTRS = [
|
|
3
|
+
'id', 'name', 'placeholder', 'role', 'title', 'href', 'src', 'type', 'value', 'for',
|
|
4
|
+
'routerLink', 'formControlName', 'formGroupName', 'formArrayName',
|
|
5
|
+
];
|
|
6
|
+
/**
|
|
7
|
+
* A node that exists only to position other nodes.
|
|
8
|
+
*
|
|
9
|
+
* The rule is narrow on purpose. A `div` holding nothing but a class and OTHER ELEMENTS is
|
|
10
|
+
* layout; a `div` holding text is the text's element, and dropping it would leave the text with
|
|
11
|
+
* nothing to hang on — including, decisively, nothing for a `conditionChain` to be attached to.
|
|
12
|
+
* `<div class="error-message">{{ errorMessage }}</div>` under an `*ngIf` is the whole answer to
|
|
13
|
+
* "why is this not on the page?", and it looks exactly like a wrapper until you notice it wraps
|
|
14
|
+
* no element.
|
|
15
|
+
*/
|
|
16
|
+
function isPresentationalWrapper(node) {
|
|
17
|
+
const tag = node.el.toLowerCase();
|
|
18
|
+
if (tag !== 'div' && tag !== 'span')
|
|
19
|
+
return false;
|
|
20
|
+
if (node.el.includes('-'))
|
|
21
|
+
return false;
|
|
22
|
+
if (node.events.length || node.props.length)
|
|
23
|
+
return false;
|
|
24
|
+
if (node.refs?.length)
|
|
25
|
+
return false;
|
|
26
|
+
for (const key of Object.keys(node.attrs)) {
|
|
27
|
+
if (key === 'class' || key === 'style')
|
|
28
|
+
continue;
|
|
29
|
+
if (key.startsWith('aria-') || key.startsWith('data-'))
|
|
30
|
+
return false;
|
|
31
|
+
if (MEANINGFUL_ATTRS.includes(key))
|
|
32
|
+
return false;
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
// It must wrap at least one ELEMENT, and carry no text of its own.
|
|
36
|
+
const hasElementChild = node.children.some(c => c.type === 'element' || c.type === 'template');
|
|
37
|
+
const hasOwnText = node.children.some(c => c.type === 'text' || c.type === 'interpolation');
|
|
38
|
+
return hasElementChild && !hasOwnText;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Drop presentational wrappers, splicing their children into their place.
|
|
42
|
+
*
|
|
43
|
+
* Safe only because `conditionChain` is denormalized onto every element: nothing downstream needs
|
|
44
|
+
* the ancestors this removes. Run BEFORE enrichment so text folding and uniqueness both see the
|
|
45
|
+
* final tree. Returns how many nodes went, because a consumer must be able to tell that the tree
|
|
46
|
+
* it is reading is not the DOM.
|
|
47
|
+
*/
|
|
48
|
+
export function collapseDom(nodes) {
|
|
49
|
+
let collapsed = 0;
|
|
50
|
+
const rewrite = (list) => {
|
|
51
|
+
const out = [];
|
|
52
|
+
for (const node of list) {
|
|
53
|
+
if (node.type === 'template') {
|
|
54
|
+
node.children = rewrite(node.children);
|
|
55
|
+
for (const branch of node.branches ?? [])
|
|
56
|
+
branch.children = rewrite(branch.children);
|
|
57
|
+
out.push(node);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (node.type !== 'element') {
|
|
61
|
+
out.push(node);
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
node.children = rewrite(node.children);
|
|
65
|
+
if (isPresentationalWrapper(node)) {
|
|
66
|
+
collapsed += 1;
|
|
67
|
+
out.push(...node.children);
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
out.push(node);
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
};
|
|
74
|
+
return { dom: rewrite(nodes), collapsed };
|
|
75
|
+
}
|
|
76
|
+
/** The attribute names a test-id may be spelled with, in the order a consumer should prefer. */
|
|
77
|
+
export const TESTID_ATTRS = ['data-testid', 'data-test-id', 'data-test', 'data-qa', 'data-cy'];
|
|
78
|
+
export function testidOf(attrs) {
|
|
79
|
+
for (const name of TESTID_ATTRS) {
|
|
80
|
+
const value = attrs[name];
|
|
81
|
+
if (typeof value === 'string' && value.trim())
|
|
82
|
+
return value.trim();
|
|
83
|
+
}
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
/** Tags that are interactive by nature, so an element bearing one is a control even with no role. */
|
|
87
|
+
const CONTROL_TAGS = new Set(['input', 'select', 'textarea', 'button', 'a', 'option']);
|
|
88
|
+
/**
|
|
89
|
+
* What a person can DO with this element.
|
|
90
|
+
*
|
|
91
|
+
* Driven off the tag and `type` rather than the role attribute, because the role is usually
|
|
92
|
+
* absent and the tag almost never is. `<input type="submit">` is a BUTTON — it is spelled as an
|
|
93
|
+
* input but it submits, and treating it as a textbox is how a submit control ends up expected to
|
|
94
|
+
* accept typing.
|
|
95
|
+
*/
|
|
96
|
+
export function controlTypeFor(el, attrs) {
|
|
97
|
+
const tag = el.toLowerCase();
|
|
98
|
+
const type = (attrs.type ?? '').toLowerCase();
|
|
99
|
+
if (tag === 'a')
|
|
100
|
+
return 'link';
|
|
101
|
+
if (tag === 'button')
|
|
102
|
+
return 'button';
|
|
103
|
+
if (tag === 'textarea')
|
|
104
|
+
return 'textbox';
|
|
105
|
+
if (tag === 'select')
|
|
106
|
+
return attrs.multiple !== undefined ? 'listbox' : 'combobox';
|
|
107
|
+
if (tag === 'input') {
|
|
108
|
+
if (type === 'checkbox')
|
|
109
|
+
return 'checkbox';
|
|
110
|
+
if (type === 'radio')
|
|
111
|
+
return 'radio';
|
|
112
|
+
if (type === 'file')
|
|
113
|
+
return 'fileinput';
|
|
114
|
+
if (type === 'range')
|
|
115
|
+
return 'slider';
|
|
116
|
+
if (type === 'date' || type === 'datetime-local' || type === 'month' || type === 'week')
|
|
117
|
+
return 'datepicker';
|
|
118
|
+
// submit/button/reset/image are buttons wearing an input's tag.
|
|
119
|
+
if (type === 'submit' || type === 'button' || type === 'reset' || type === 'image')
|
|
120
|
+
return 'button';
|
|
121
|
+
return 'textbox';
|
|
122
|
+
}
|
|
123
|
+
const role = (attrs.role ?? '').toLowerCase();
|
|
124
|
+
if (role === 'button' || role === 'link' || role === 'checkbox' || role === 'radio'
|
|
125
|
+
|| role === 'combobox' || role === 'listbox' || role === 'textbox' || role === 'tab'
|
|
126
|
+
|| role === 'grid' || role === 'slider') {
|
|
127
|
+
return role;
|
|
128
|
+
}
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
function isControl(node) {
|
|
132
|
+
const tag = node.el.toLowerCase();
|
|
133
|
+
if (CONTROL_TAGS.has(tag))
|
|
134
|
+
return true;
|
|
135
|
+
if (node.attrs.role)
|
|
136
|
+
return true;
|
|
137
|
+
// A custom element wired to a click is a control however it is spelled.
|
|
138
|
+
return node.events.some(e => e.kind !== 'twoWayWriteback');
|
|
139
|
+
}
|
|
140
|
+
/** Every handle the SOURCE offers for this element, best first. Audit material, not a locator. */
|
|
141
|
+
export function selectorCandidatesFor(node, staticText) {
|
|
142
|
+
const out = [];
|
|
143
|
+
const { attrs } = node;
|
|
144
|
+
const tag = node.el.toLowerCase();
|
|
145
|
+
const push = (by, value) => out.push({ by, value, unique: false, uniqueScope: 'template' });
|
|
146
|
+
const testid = testidOf(attrs);
|
|
147
|
+
if (testid)
|
|
148
|
+
push('testid', `[data-testid="${testid}"]`);
|
|
149
|
+
if (attrs.id)
|
|
150
|
+
push('id', `#${attrs.id}`);
|
|
151
|
+
if (attrs.name)
|
|
152
|
+
push('name', `${tag}[name="${attrs.name}"]`);
|
|
153
|
+
if (attrs['aria-label'])
|
|
154
|
+
push('aria', `${tag}[aria-label="${attrs['aria-label']}"]`);
|
|
155
|
+
if (attrs.placeholder)
|
|
156
|
+
push('placeholder', `${tag}[placeholder="${attrs.placeholder}"]`);
|
|
157
|
+
if (staticText)
|
|
158
|
+
push('text', staticText);
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Mark which candidates are unique WITHIN THIS TEMPLATE.
|
|
163
|
+
*
|
|
164
|
+
* Scoped to the template and labelled as such, because that is the only claim a static pass can
|
|
165
|
+
* honestly make: one page is composed of a shell plus a route component, so a value unique in its
|
|
166
|
+
* own file can still collide once rendered. Saying `uniqueScope: "template"` is the difference
|
|
167
|
+
* between a weaker claim and a wrong one.
|
|
168
|
+
*/
|
|
169
|
+
function markUniqueness(elements) {
|
|
170
|
+
const counts = new Map();
|
|
171
|
+
for (const el of elements) {
|
|
172
|
+
for (const c of el.selectorCandidates ?? []) {
|
|
173
|
+
counts.set(`${c.by} ${c.value}`, (counts.get(`${c.by} ${c.value}`) ?? 0) + 1);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
for (const el of elements) {
|
|
177
|
+
for (const c of el.selectorCandidates ?? []) {
|
|
178
|
+
c.unique = counts.get(`${c.by} ${c.value}`) === 1;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Does this element carry a stable handle, an expression-derived one, or none at all?
|
|
184
|
+
*
|
|
185
|
+
* `none` is the interesting answer and the reason this field exists: reported with a file and a
|
|
186
|
+
* line it becomes an actionable finding, where the same element simply missing from a join reads
|
|
187
|
+
* as noise.
|
|
188
|
+
*/
|
|
189
|
+
function tokenStabilityFor(node, staticText) {
|
|
190
|
+
const { attrs } = node;
|
|
191
|
+
if (testidOf(attrs) || attrs.id || attrs.name || attrs['aria-label'] || attrs.placeholder || staticText) {
|
|
192
|
+
return 'static';
|
|
193
|
+
}
|
|
194
|
+
const dynamic = node.props.find(p => p.name === 'id' || p.name === 'attr.id' || p.name === 'name' || p.name === 'attr.name'
|
|
195
|
+
|| TESTID_ATTRS.some(t => p.name === t || p.name === `attr.${t}`));
|
|
196
|
+
if (dynamic)
|
|
197
|
+
return 'dynamic';
|
|
198
|
+
return isControl(node) ? 'none' : undefined;
|
|
199
|
+
}
|
|
200
|
+
/** The literal prefix of a dynamic handle, e.g. `[attr.id]="'tx-' + tx.id"`. */
|
|
201
|
+
function tokenTemplateFor(node) {
|
|
202
|
+
const dynamic = node.props.find(p => p.name === 'id' || p.name === 'attr.id'
|
|
203
|
+
|| TESTID_ATTRS.some(t => p.name === t || p.name === `attr.${t}`));
|
|
204
|
+
if (!dynamic)
|
|
205
|
+
return undefined;
|
|
206
|
+
const literal = /^\s*['"]([^'"]+)['"]\s*\+/.exec(dynamic.expr);
|
|
207
|
+
return literal ? `${literal[1]}{{*}}` : undefined;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* An accessible name, and ONLY when the source actually declares one.
|
|
211
|
+
*
|
|
212
|
+
* Deliberately not falling back to `staticText`, which is the obvious version and is wrong. A
|
|
213
|
+
* consumer keys an element on the best handle it has, and an accessible name outranks raw text —
|
|
214
|
+
* so synthesising one from a caption turns `<button>Sign In</button>` into an `aria:Sign In` key,
|
|
215
|
+
* while a browser recording the same button (it has no `aria-label`) produces a `text:Sign In`
|
|
216
|
+
* one. The two never join, and they fail to join on exactly the elements whose only handle is
|
|
217
|
+
* their caption. `staticText` carries the caption; let the consumer decide what it is worth.
|
|
218
|
+
*/
|
|
219
|
+
function accessibleNameFor(node) {
|
|
220
|
+
return node.attrs['aria-label'] || node.attrs.title || undefined;
|
|
221
|
+
}
|
|
222
|
+
function isTruthyAttr(value) {
|
|
223
|
+
return value !== undefined && value !== 'false';
|
|
224
|
+
}
|
|
225
|
+
/** The gate a `TemplateNode` represents, as one link in a chain. */
|
|
226
|
+
function linkFor(node, branch) {
|
|
227
|
+
return {
|
|
228
|
+
directive: node.structural,
|
|
229
|
+
expr: node.condition ?? '',
|
|
230
|
+
...(branch ? { branch } : {}),
|
|
231
|
+
...(node.source ? { source: node.source } : {}),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
/** `@for`'s condition is written `item of items; track id` — split it back apart. */
|
|
235
|
+
function repeatPartsOf(condition) {
|
|
236
|
+
if (!condition)
|
|
237
|
+
return {};
|
|
238
|
+
const match = /^\s*(\S+)\s+of\s+([^;]+?)\s*(?:;\s*track\s+(.+))?\s*$/.exec(condition);
|
|
239
|
+
if (!match)
|
|
240
|
+
return { over: condition.trim() || undefined };
|
|
241
|
+
return { varName: match[1], over: match[2]?.trim(), trackBy: match[3]?.trim() };
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Walk a parsed tree and enrich every element in place.
|
|
245
|
+
*
|
|
246
|
+
* Returns every element it visited, in template order, so the caller can run whole-template passes
|
|
247
|
+
* (uniqueness) without walking again. Order is template order because two elements sharing a
|
|
248
|
+
* handle should collide in a stable order rather than whichever the walk happened to reach first.
|
|
249
|
+
*/
|
|
250
|
+
export function enrichDom(nodes) {
|
|
251
|
+
const seen = [];
|
|
252
|
+
const walk = (list, chain, repeat) => {
|
|
253
|
+
for (const node of list) {
|
|
254
|
+
if (node.type === 'template') {
|
|
255
|
+
const t = node;
|
|
256
|
+
const isFor = t.structural === '@for' || t.structural === '*ngFor' || t.structural === '.map()';
|
|
257
|
+
const parts = isFor ? repeatPartsOf(t.condition) : {};
|
|
258
|
+
const nextRepeat = isFor ? { ...parts, on: true } : repeat;
|
|
259
|
+
// The primary branch's children hang off `children`; every other branch off `branches`.
|
|
260
|
+
// `branches[0]` IS `children` for an @if/@switch, so walking both would visit the primary
|
|
261
|
+
// twice and give its elements a duplicated chain link.
|
|
262
|
+
walk(t.children, [...chain, linkFor(t)], nextRepeat);
|
|
263
|
+
for (const branch of (t.branches ?? []).slice(1)) {
|
|
264
|
+
walk(branch.children, [...chain, linkFor(t, branch.label)], nextRepeat);
|
|
265
|
+
}
|
|
266
|
+
continue;
|
|
267
|
+
}
|
|
268
|
+
if (node.type !== 'element')
|
|
269
|
+
continue;
|
|
270
|
+
const el = node;
|
|
271
|
+
const staticParts = [];
|
|
272
|
+
let hasDynamicText = false;
|
|
273
|
+
for (const child of el.children) {
|
|
274
|
+
if (child.type === 'text' && child.value.trim())
|
|
275
|
+
staticParts.push(child.value.trim());
|
|
276
|
+
if (child.type === 'interpolation')
|
|
277
|
+
hasDynamicText = true;
|
|
278
|
+
}
|
|
279
|
+
const staticText = staticParts.join(' ').trim() || undefined;
|
|
280
|
+
if (staticText)
|
|
281
|
+
el.staticText = staticText;
|
|
282
|
+
if (hasDynamicText)
|
|
283
|
+
el.hasDynamicText = true;
|
|
284
|
+
const controlType = controlTypeFor(el.el, el.attrs);
|
|
285
|
+
if (controlType)
|
|
286
|
+
el.controlType = controlType;
|
|
287
|
+
const accessibleName = accessibleNameFor(el);
|
|
288
|
+
if (accessibleName)
|
|
289
|
+
el.accessibleName = accessibleName;
|
|
290
|
+
if (el.attrs.role)
|
|
291
|
+
el.role = el.attrs.role;
|
|
292
|
+
if (isTruthyAttr(el.attrs.required))
|
|
293
|
+
el.required = true;
|
|
294
|
+
if (el.el.includes('-'))
|
|
295
|
+
el.sourceRepresentation = el.el;
|
|
296
|
+
const stability = tokenStabilityFor(el, staticText);
|
|
297
|
+
if (stability)
|
|
298
|
+
el.tokenStability = stability;
|
|
299
|
+
const template = stability === 'dynamic' ? tokenTemplateFor(el) : undefined;
|
|
300
|
+
if (template)
|
|
301
|
+
el.tokenTemplate = template;
|
|
302
|
+
const candidates = selectorCandidatesFor(el, staticText);
|
|
303
|
+
if (candidates.length)
|
|
304
|
+
el.selectorCandidates = candidates;
|
|
305
|
+
if (chain.length) {
|
|
306
|
+
el.conditional = true;
|
|
307
|
+
el.conditionChain = chain;
|
|
308
|
+
}
|
|
309
|
+
if (repeat.on) {
|
|
310
|
+
el.repeated = true;
|
|
311
|
+
if (repeat.over)
|
|
312
|
+
el.repeatOver = repeat.over;
|
|
313
|
+
if (repeat.varName)
|
|
314
|
+
el.repeatVar = repeat.varName;
|
|
315
|
+
if (repeat.trackBy)
|
|
316
|
+
el.repeatTrackBy = repeat.trackBy;
|
|
317
|
+
}
|
|
318
|
+
seen.push(el);
|
|
319
|
+
walk(el.children, chain, repeat);
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
walk(nodes, [], { on: false });
|
|
323
|
+
markUniqueness(seen);
|
|
324
|
+
return seen;
|
|
325
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { DomNode } from './dom.js';
|
|
2
|
+
import type { SourcePointer } from './source.js';
|
|
2
3
|
export type PropertyBindingKind = 'decorator' | 'signal';
|
|
3
4
|
/** An Angular @Input()/@Output() or input()/output() signal. */
|
|
4
5
|
export interface PropertyBinding {
|
|
@@ -21,6 +22,15 @@ export interface ComponentNode {
|
|
|
21
22
|
className: string;
|
|
22
23
|
/** Repo-relative path to the file the component is defined in. */
|
|
23
24
|
filePath: string;
|
|
25
|
+
/** The class declaration, with lines. `filePath` says which file; this says where in it. */
|
|
26
|
+
source?: SourcePointer;
|
|
27
|
+
/** The TEMPLATE, which for an external `templateUrl` is a different file entirely — and is
|
|
28
|
+
* where every element's own pointer lands. Without this a consumer reading a component's
|
|
29
|
+
* location gets the `.ts`, and every element in it points somewhere else. */
|
|
30
|
+
template?: {
|
|
31
|
+
source: SourcePointer;
|
|
32
|
+
inline: boolean;
|
|
33
|
+
};
|
|
24
34
|
/** Angular only. */
|
|
25
35
|
selector?: string;
|
|
26
36
|
/** Angular only. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../../src/types/component.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;
|
|
1
|
+
{"version":3,"file":"component.d.ts","sourceRoot":"","sources":["../../src/types/component.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,UAAU,CAAC;AACxC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,MAAM,MAAM,mBAAmB,GAAG,WAAW,GAAG,QAAQ,CAAC;AAEzD,gEAAgE;AAChE,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,IAAI,EAAE,mBAAmB,CAAC;IAC1B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,YAAY,GAAG,SAAS,CAAC;AAE9D,8BAA8B;AAC9B,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,UAAU,CAAC;IACnB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED,MAAM,WAAW,aAAa;IAC5B,SAAS,EAAE,MAAM,CAAC;IAClB,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAC;IACjB,4FAA4F;IAC5F,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB;;kFAE8E;IAC9E,QAAQ,CAAC,EAAE;QAAE,MAAM,EAAE,aAAa,CAAC;QAAC,MAAM,EAAE,OAAO,CAAA;KAAE,CAAC;IACtD,oBAAoB;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,oBAAoB;IACpB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,oDAAoD;IACpD,MAAM,EAAE,eAAe,EAAE,CAAC;IAC1B,oDAAoD;IACpD,OAAO,EAAE,eAAe,EAAE,CAAC;IAC3B,oDAAoD;IACpD,KAAK,CAAC,EAAE,cAAc,EAAE,CAAC;IACzB,iFAAiF;IACjF,GAAG,CAAC,EAAE,OAAO,EAAE,CAAC;CACjB"}
|
package/dist/types/dom.d.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* control-flow detection) rather than a grammar built for the purpose. Best-effort — the
|
|
10
10
|
* underlying JS could always route around the pattern in a way that isn't detected.
|
|
11
11
|
*/
|
|
12
|
+
import type { SourcePointer } from './source.js';
|
|
12
13
|
export type Extraction = 'compiler' | 'heuristic';
|
|
13
14
|
export interface BaseNode {
|
|
14
15
|
extraction: Extraction;
|
|
@@ -17,6 +18,17 @@ export interface BaseNode {
|
|
|
17
18
|
export interface BoundExpr {
|
|
18
19
|
name: string;
|
|
19
20
|
expr: string;
|
|
21
|
+
/**
|
|
22
|
+
* What kind of binding this is.
|
|
23
|
+
*
|
|
24
|
+
* `[(ngModel)]` desugars into a property AND an event, and in v2 the write-back was
|
|
25
|
+
* indistinguishable from a real handler — so a consumer counting handlers got 20 where the app
|
|
26
|
+
* had 7, and every two-way-bound input looked interactive twice. `twoWayWriteback` marks the
|
|
27
|
+
* generated half.
|
|
28
|
+
*/
|
|
29
|
+
kind?: 'dom' | 'output' | 'twoWay' | 'twoWayWriteback';
|
|
30
|
+
/** Where the handler METHOD is declared — the `.ts` file, not the template. */
|
|
31
|
+
handler?: SourcePointer;
|
|
20
32
|
}
|
|
21
33
|
/**
|
|
22
34
|
* `Child` is generic so the resolved dependency-graph tree (packages/core/src/types/
|
|
@@ -36,7 +48,61 @@ export interface ElementNode<Child = DomNode> extends BaseNode {
|
|
|
36
48
|
/** Template reference variables (#ref). Angular only. */
|
|
37
49
|
refs?: string[];
|
|
38
50
|
children: Child[];
|
|
51
|
+
/** Where this element is written. */
|
|
52
|
+
source?: SourcePointer;
|
|
53
|
+
/** STATIC child text only, folded from `TextNode` children and trimmed. An interpolation is
|
|
54
|
+
* never text: a key built from one changes with the test data. */
|
|
55
|
+
staticText?: string;
|
|
56
|
+
/** True when any child was an interpolation — so "no staticText" can be told apart from
|
|
57
|
+
* "the text is dynamic". */
|
|
58
|
+
hasDynamicText?: boolean;
|
|
59
|
+
/** `static`: a stable handle exists. `dynamic`: a token-bearing attribute is an expression
|
|
60
|
+
* (`[attr.id]="'tx-' + tx.id"`), and `tokenTemplate` carries the literal prefix. `none`: this
|
|
61
|
+
* control has no stable handle at all — which is the most actionable thing this file says. */
|
|
62
|
+
tokenStability?: 'static' | 'dynamic' | 'none';
|
|
63
|
+
tokenTemplate?: string;
|
|
64
|
+
controlType?: ControlType;
|
|
65
|
+
/** The tag as WRITTEN, when it differs from what renders (`mat-select` -> a listbox). */
|
|
66
|
+
sourceRepresentation?: string;
|
|
67
|
+
role?: string;
|
|
68
|
+
accessibleName?: string;
|
|
69
|
+
required?: boolean;
|
|
70
|
+
/** True when any ancestor is a structural branch. */
|
|
71
|
+
conditional?: boolean;
|
|
72
|
+
/** Every gate between the template root and this element, outermost first. */
|
|
73
|
+
conditionChain?: ConditionLink[];
|
|
74
|
+
repeated?: boolean;
|
|
75
|
+
repeatOver?: string;
|
|
76
|
+
repeatVar?: string;
|
|
77
|
+
repeatTrackBy?: string;
|
|
78
|
+
/**
|
|
79
|
+
* Handles the SOURCE offers for this element — audit material, never a locator to drive.
|
|
80
|
+
*
|
|
81
|
+
* `unique` computed within one template is a weaker claim than a live DOM's uniqueness: the
|
|
82
|
+
* shell and the route component both render into one page, so a token unique in its own file
|
|
83
|
+
* can still collide once composed. `uniqueScope` says which claim is being made.
|
|
84
|
+
*/
|
|
85
|
+
selectorCandidates?: SelectorCandidate[];
|
|
39
86
|
}
|
|
87
|
+
/** One structural gate, as written in the template. */
|
|
88
|
+
export interface ConditionLink {
|
|
89
|
+
/** `*ngIf`, `@if`, `@for`, `@switch`, `@defer`, ... */
|
|
90
|
+
directive: string;
|
|
91
|
+
/** The guiding expression. Free text with no grammar — treat it as data. */
|
|
92
|
+
expr: string;
|
|
93
|
+
/** Which branch of a multi-branch construct this element sits in (`if`, `else if`, `else`,
|
|
94
|
+
* a `@switch` case label, `empty`, `placeholder`). */
|
|
95
|
+
branch?: string;
|
|
96
|
+
source?: SourcePointer;
|
|
97
|
+
}
|
|
98
|
+
export interface SelectorCandidate {
|
|
99
|
+
by: 'testid' | 'id' | 'name' | 'aria' | 'role' | 'placeholder' | 'text' | 'css';
|
|
100
|
+
value: string;
|
|
101
|
+
unique: boolean;
|
|
102
|
+
uniqueScope: 'template' | 'route';
|
|
103
|
+
}
|
|
104
|
+
/** What a person can DO with this element, independent of how it is spelled. */
|
|
105
|
+
export type ControlType = 'textbox' | 'combobox' | 'listbox' | 'checkbox' | 'radio' | 'button' | 'link' | 'datepicker' | 'fileinput' | 'slider' | 'grid' | 'tab' | 'other';
|
|
40
106
|
export interface TextNode extends BaseNode {
|
|
41
107
|
type: 'text';
|
|
42
108
|
value: string;
|
|
@@ -61,6 +127,7 @@ export interface TemplateNode<Child = DomNode> extends BaseNode {
|
|
|
61
127
|
branches?: TemplateBranch<Child>[];
|
|
62
128
|
/** The primary/consequent branch's children, kept so every DomNode has a `children` array. */
|
|
63
129
|
children: Child[];
|
|
130
|
+
source?: SourcePointer;
|
|
64
131
|
}
|
|
65
132
|
export type DomNode = ElementNode | TextNode | InterpolationNode | TemplateNode;
|
|
66
133
|
//# sourceMappingURL=dom.d.ts.map
|
package/dist/types/dom.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dom.d.ts","sourceRoot":"","sources":["../../src/types/dom.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,MAAM,UAAU,GAAG,UAAU,GAAG,WAAW,CAAC;AAElD,MAAM,WAAW,QAAQ;IACvB,UAAU,EAAE,UAAU,CAAC;CACxB;AAED,mHAAmH;AACnH,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"dom.d.ts","sourceRoot":"","sources":["../../src/types/dom.ts"],"names":[],"mappings":"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"}
|
package/dist/types/manifest.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { ComponentNode } from './component.js';
|
|
2
2
|
import type { RouteNode } from './route.js';
|
|
3
3
|
import type { RouteDependencyTree } from './dependency-graph.js';
|
|
4
|
-
import type { AppIdentity, Coverage, CoverageScope, Provenance } from './provenance.js';
|
|
4
|
+
import type { AppIdentity, Coverage, CoverageScope, GeneratorProvenance, Provenance, RepoProvenance } from './provenance.js';
|
|
5
|
+
import type { Uncapturable } from './uncapturable.js';
|
|
6
|
+
import type { SourcePointer } from './source.js';
|
|
5
7
|
/**
|
|
6
8
|
* Bumped to "2.0" for the `app` block, whose two fields are REQUIRED — see `AppIdentity`. A
|
|
7
9
|
* consumer needs exactly one field to test to know whether the routes it is about to read can be
|
|
@@ -16,16 +18,78 @@ export interface UiManifest {
|
|
|
16
18
|
* against a real URL, silently. See {@link AppIdentity}. */
|
|
17
19
|
app: AppIdentity;
|
|
18
20
|
/** Which commit, which extractor, which passes. Non-diffable — ignore it when diffing, the same
|
|
19
|
-
* as `generatedAt`.
|
|
21
|
+
* as `generatedAt`.
|
|
22
|
+
*
|
|
23
|
+
* Kept alongside the top-level `repo`/`generator` below rather than replaced by them: a
|
|
24
|
+
* consumer written against the first 2.0 release reads this block, and breaking it would make
|
|
25
|
+
* a field addition into a coordinated release. The two are the same objects. */
|
|
20
26
|
provenance: Provenance;
|
|
27
|
+
/**
|
|
28
|
+
* The same `provenance.repo` / `provenance.generator`, lifted to the top level.
|
|
29
|
+
*
|
|
30
|
+
* Both spellings exist on purpose. Nesting keeps the non-diffable data in one block a
|
|
31
|
+
* `jq 'del(.provenance)'` can drop whole; lifting is what consumers that require these fields
|
|
32
|
+
* read. `repo.remoteUrl` and `repo.appRoot` are optional on the nested copy (they are absent
|
|
33
|
+
* outside a git tree, which is information) and REQUIRED here — outside a git tree this pair is
|
|
34
|
+
* simply not emitted, so "present but hollow" never occurs.
|
|
35
|
+
*/
|
|
36
|
+
repo?: RepoProvenance & {
|
|
37
|
+
remoteUrl: string;
|
|
38
|
+
appRoot: string;
|
|
39
|
+
};
|
|
40
|
+
generator?: GeneratorProvenance & {
|
|
41
|
+
generatedAt: string;
|
|
42
|
+
};
|
|
21
43
|
/** Whether a missing route means "deleted" or "not looked at". See {@link Coverage}. */
|
|
22
44
|
coverage: Coverage;
|
|
23
45
|
coverageScope?: CoverageScope;
|
|
24
46
|
/** ISO timestamp of generation. Not diff-relevant on its own — consumers diffing two
|
|
25
47
|
* manifests should ignore this field, since it changes on every run even with no UI change. */
|
|
26
48
|
generatedAt: string;
|
|
49
|
+
/**
|
|
50
|
+
* Whether presentational nodes were dropped.
|
|
51
|
+
*
|
|
52
|
+
* `"semantic"` means the tree is NOT the DOM: a wrapper carrying nothing but a class, with no
|
|
53
|
+
* events, no props and no handle, is folded away and its static text folded into its parent.
|
|
54
|
+
* That is a large reduction with no loss to any join, precisely because `conditionChain` is
|
|
55
|
+
* denormalized onto each element — nothing downstream needs the ancestors that were dropped.
|
|
56
|
+
*/
|
|
57
|
+
nodePolicy?: 'semantic' | 'verbatim';
|
|
58
|
+
/** How many nodes `nodePolicy` removed, so a consumer knows the tree is not the DOM. */
|
|
59
|
+
collapsedNodeCount?: number;
|
|
27
60
|
routes: RouteNode[];
|
|
61
|
+
/**
|
|
62
|
+
* Wildcard routes.
|
|
63
|
+
*
|
|
64
|
+
* Kept out of `routes[]` because a `**` matches every URL and so identifies none: given a page
|
|
65
|
+
* key it would fold every unmatched screen onto one node, which is worse than a miss.
|
|
66
|
+
*/
|
|
67
|
+
fallbacks?: {
|
|
68
|
+
pattern: string;
|
|
69
|
+
redirectTo?: string;
|
|
70
|
+
source?: SourcePointer;
|
|
71
|
+
}[];
|
|
28
72
|
components: ComponentNode[];
|
|
73
|
+
/**
|
|
74
|
+
* Which components render under each route — the shell above the router outlet included.
|
|
75
|
+
*
|
|
76
|
+
* References, never DOM: the elements live in `components[]` and are resolved once. Without
|
|
77
|
+
* this a consumer keying elements by page attributes the shell's navigation to no page at
|
|
78
|
+
* all, and "where is the sign-out button on this screen" answers "this screen has none".
|
|
79
|
+
*/
|
|
80
|
+
routeTrees?: {
|
|
81
|
+
routePath: string;
|
|
82
|
+
rootComponent: string;
|
|
83
|
+
nodes: {
|
|
84
|
+
component: string;
|
|
85
|
+
via?: SourcePointer;
|
|
86
|
+
conditional: boolean;
|
|
87
|
+
repeated: boolean;
|
|
88
|
+
children: unknown[];
|
|
89
|
+
}[];
|
|
90
|
+
}[];
|
|
91
|
+
/** What the extractor could not statically resolve. See {@link Uncapturable}. */
|
|
92
|
+
uncapturable?: Uncapturable[];
|
|
29
93
|
/** Present only when the extractor was run with dependency-graph resolution enabled. */
|
|
30
94
|
dependencyGraph?: RouteDependencyTree[];
|
|
31
95
|
/** Soft-failure notices, e.g. "routing pattern unresolved for src/App.tsx". Never used in
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/types/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"manifest.d.ts","sourceRoot":"","sources":["../../src/types/manifest.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAC5C,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,uBAAuB,CAAC;AACjE,OAAO,KAAK,EACV,WAAW,EAAE,QAAQ,EAAE,aAAa,EAAE,mBAAmB,EAAE,UAAU,EAAE,cAAc,EACtF,MAAM,iBAAiB,CAAC;AACzB,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD;;;;GAIG;AACH,eAAO,MAAM,cAAc,QAAQ,CAAC;AAEpC,MAAM,MAAM,SAAS,GAAG,SAAS,GAAG,OAAO,CAAC;AAE5C,MAAM,WAAW,UAAU;IACzB,aAAa,EAAE,OAAO,cAAc,CAAC;IACrC,SAAS,EAAE,SAAS,CAAC;IACrB;iEAC6D;IAC7D,GAAG,EAAE,WAAW,CAAC;IACjB;;;;;qFAKiF;IACjF,UAAU,EAAE,UAAU,CAAC;IACvB;;;;;;;;OAQG;IACH,IAAI,CAAC,EAAE,cAAc,GAAG;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC;IAC/D,SAAS,CAAC,EAAE,mBAAmB,GAAG;QAAE,WAAW,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1D,wFAAwF;IACxF,QAAQ,EAAE,QAAQ,CAAC;IACnB,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;oGACgG;IAChG,WAAW,EAAE,MAAM,CAAC;IACpB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,UAAU,GAAG,UAAU,CAAC;IACrC,wFAAwF;IACxF,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB;;;;;OAKG;IACH,SAAS,CAAC,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,CAAC;QAAC,MAAM,CAAC,EAAE,aAAa,CAAA;KAAE,EAAE,CAAC;IAC/E,UAAU,EAAE,aAAa,EAAE,CAAC;IAC5B;;;;;;OAMG;IACH,UAAU,CAAC,EAAE;QACX,SAAS,EAAE,MAAM,CAAC;QAClB,aAAa,EAAE,MAAM,CAAC;QACtB,KAAK,EAAE;YAAE,SAAS,EAAE,MAAM,CAAC;YAAC,GAAG,CAAC,EAAE,aAAa,CAAC;YAAC,WAAW,EAAE,OAAO,CAAC;YAAC,QAAQ,EAAE,OAAO,CAAC;YAChF,QAAQ,EAAE,OAAO,EAAE,CAAA;SAAE,EAAE,CAAC;KAClC,EAAE,CAAC;IACJ,iFAAiF;IACjF,YAAY,CAAC,EAAE,YAAY,EAAE,CAAC;IAC9B,wFAAwF;IACxF,eAAe,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACxC;mGAC+F;IAC/F,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB"}
|
package/dist/types/route.d.ts
CHANGED
|
@@ -1,6 +1,21 @@
|
|
|
1
|
+
import type { SourcePointer } from './source.js';
|
|
2
|
+
/**
|
|
3
|
+
* One guard on a route.
|
|
4
|
+
*
|
|
5
|
+
* v2 emitted a bare name. A name cannot be opened, so "what gates this route?" was answerable
|
|
6
|
+
* only as far as "something called authGuard" — and the follow-up, which is the one that matters
|
|
7
|
+
* when a test cannot reach a screen, needed a repository search to answer.
|
|
8
|
+
*/
|
|
9
|
+
export interface RouteGuard {
|
|
10
|
+
name: string;
|
|
11
|
+
kind: 'function' | 'class';
|
|
12
|
+
source?: SourcePointer;
|
|
13
|
+
}
|
|
1
14
|
export interface RouteGuards {
|
|
2
|
-
canActivate?:
|
|
3
|
-
|
|
15
|
+
canActivate?: RouteGuard[];
|
|
16
|
+
canActivateChild?: RouteGuard[];
|
|
17
|
+
canDeactivate?: RouteGuard[];
|
|
18
|
+
canMatch?: RouteGuard[];
|
|
4
19
|
}
|
|
5
20
|
export type ReactRoutingPattern = 'jsx-routes' | 'router-config' | 'file-based';
|
|
6
21
|
export interface RouteNode {
|
|
@@ -23,10 +38,20 @@ export interface RouteNode {
|
|
|
23
38
|
component?: {
|
|
24
39
|
module: string;
|
|
25
40
|
export: string;
|
|
41
|
+
source?: SourcePointer;
|
|
42
|
+
};
|
|
43
|
+
loadComponent?: {
|
|
44
|
+
module: string;
|
|
45
|
+
export: string;
|
|
46
|
+
source?: SourcePointer;
|
|
26
47
|
};
|
|
27
48
|
redirectTo?: string;
|
|
28
49
|
pathMatch?: string;
|
|
29
50
|
guards?: RouteGuards;
|
|
51
|
+
/** Query parameters this route reads. */
|
|
52
|
+
queryParamKeys?: string[];
|
|
53
|
+
/** Where the route object literal is written. */
|
|
54
|
+
source?: SourcePointer;
|
|
30
55
|
children?: RouteNode[];
|
|
31
56
|
/** React only, set at the tree root: which detection strategy produced this tree. */
|
|
32
57
|
routingPattern?: ReactRoutingPattern;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../src/types/route.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,WAAW;IAC1B,WAAW,CAAC,EAAE,
|
|
1
|
+
{"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../src/types/route.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD;;;;;;GAMG;AACH,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,GAAG,OAAO,CAAC;IAC3B,MAAM,CAAC,EAAE,aAAa,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,WAAW,CAAC,EAAE,UAAU,EAAE,CAAC;IAC3B,gBAAgB,CAAC,EAAE,UAAU,EAAE,CAAC;IAChC,aAAa,CAAC,EAAE,UAAU,EAAE,CAAC;IAC7B,QAAQ,CAAC,EAAE,UAAU,EAAE,CAAC;CACzB;AAED,MAAM,MAAM,mBAAmB,GAAG,YAAY,GAAG,eAAe,GAAG,YAAY,CAAC;AAEhF,MAAM,WAAW,SAAS;IACxB,kEAAkE;IAClE,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,gGAAgG;IAChG,SAAS,CAAC,EAAE;QACV,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,aAAa,CAAC;KACxB,CAAC;IACF,aAAa,CAAC,EAAE;QACd,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,CAAC,EAAE,aAAa,CAAC;KACxB,CAAC;IACF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,yCAAyC;IACzC,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;IAC1B,iDAAiD;IACjD,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,QAAQ,CAAC,EAAE,SAAS,EAAE,CAAC;IACvB,qFAAqF;IACrF,cAAc,CAAC,EAAE,mBAAmB,CAAC;CACtC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where in the repository a thing is declared.
|
|
3
|
+
*
|
|
4
|
+
* v2 emitted routes, components and elements but never said *where they came from*, so every
|
|
5
|
+
* "where is this declared?" answer a consumer could give was "somewhere in this file" — and for a
|
|
6
|
+
* 400-line template that is not an answer, it is a re-read. Line numbers are the single biggest
|
|
7
|
+
* thing this block adds, and they cost nothing to produce: Angular's `parseTemplate()` and the
|
|
8
|
+
* TypeScript AST both carry positions on every node already.
|
|
9
|
+
*
|
|
10
|
+
* `blobOid` is the quietly valuable field. A git blob sha survives a rename, so a pointer stays
|
|
11
|
+
* resolvable after a refactor that a path alone would strand. Absent until the extractor is asked
|
|
12
|
+
* to shell out to git per file, which it is not today.
|
|
13
|
+
*/
|
|
14
|
+
export interface SourcePointer {
|
|
15
|
+
/** Repo-relative, forward slashes, no `..` segment. Relative to the repository root — NOT to
|
|
16
|
+
* `repo.appRoot` — so a consumer can hand it to a repository API without re-joining anything. */
|
|
17
|
+
path: string;
|
|
18
|
+
/** A class, method or guard name. An identifier, never free text. */
|
|
19
|
+
symbol?: string;
|
|
20
|
+
/** 1-based, like an editor shows and unlike every parser that produces it. */
|
|
21
|
+
startLine?: number;
|
|
22
|
+
endLine?: number;
|
|
23
|
+
blobOid?: string;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=source.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"source.d.ts","sourceRoot":"","sources":["../../src/types/source.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,aAAa;IAC5B;sGACkG;IAClG,IAAI,EAAE,MAAM,CAAC;IACb,qEAAqE;IACrE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,8EAA8E;IAC9E,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What the extractor could NOT statically resolve.
|
|
3
|
+
*
|
|
4
|
+
* This is not optional bookkeeping. When a consumer sees an element at runtime that no manifest
|
|
5
|
+
* declares, there are two causes — the manifest is stale, or the app injects that DOM — and they
|
|
6
|
+
* need opposite actions. This list is the only thing that tells them apart. Without it, every such
|
|
7
|
+
* finding is undiagnosable and a manifest covering 70% of an app looks complete.
|
|
8
|
+
*
|
|
9
|
+
* `diagnostics[]` is still emitted alongside and still carries the same notices as free text; this
|
|
10
|
+
* is the same information given a shape a consumer can branch on.
|
|
11
|
+
*/
|
|
12
|
+
export type UncapturableKind = 'dynamicComponentOutlet' | 'innerHTML' | 'runtimeRoute' | 'unresolvedLazyChunk' | 'thirdPartyWebComponent' | 'iframe' | 'dynamicSelector' | 'unresolvedApiUrl' | 'templateParseError' | 'unsupportedTemplateNode';
|
|
13
|
+
export interface Uncapturable {
|
|
14
|
+
kind: UncapturableKind;
|
|
15
|
+
/** The component (or route) whose extraction is incomplete because of this. */
|
|
16
|
+
affects?: string;
|
|
17
|
+
/** Free text, for a person. */
|
|
18
|
+
detail?: string;
|
|
19
|
+
source?: import('./source.js').SourcePointer;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=uncapturable.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"uncapturable.d.ts","sourceRoot":"","sources":["../../src/types/uncapturable.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AACH,MAAM,MAAM,gBAAgB,GACxB,wBAAwB,GACxB,WAAW,GACX,cAAc,GACd,qBAAqB,GACrB,wBAAwB,GACxB,QAAQ,GACR,iBAAiB,GACjB,kBAAkB,GAClB,oBAAoB,GACpB,yBAAyB,CAAC;AAE9B,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,gBAAgB,CAAC;IACvB,+EAA+E;IAC/E,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,+BAA+B;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,OAAO,aAAa,EAAE,aAAa,CAAC;CAC9C"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|