@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 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
@@ -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;AAExC,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,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"}
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"}
@@ -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
@@ -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;CACd;AAED;;;;GAIG;AACH,MAAM,WAAW,WAAW,CAAC,KAAK,GAAG,OAAO,CAAE,SAAQ,QAAQ;IAC5D,IAAI,EAAE,SAAS,CAAC;IAChB,sFAAsF;IACtF,EAAE,EAAE,MAAM,CAAC;IACX,sDAAsD;IACtD,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC9B,wEAAwE;IACxE,KAAK,EAAE,SAAS,EAAE,CAAC;IACnB,qDAAqD;IACrD,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,yDAAyD;IACzD,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,KAAK,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,QAAS,SAAQ,QAAQ;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;CACf;AAED,MAAM,WAAW,iBAAkB,SAAQ,QAAQ;IACjD,IAAI,EAAE,eAAe,CAAC;IACtB,8FAA8F;IAC9F,aAAa,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,MAAM,cAAc,GACtB,OAAO,GACP,QAAQ,GACR,KAAK,GACL,MAAM,GACN,SAAS,GACT,QAAQ,GACR,SAAS,GACT,IAAI,GACJ,QAAQ,CAAC;AAEb,MAAM,WAAW,cAAc,CAAC,KAAK,GAAG,OAAO;IAC7C,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,EAAE,KAAK,EAAE,CAAC;CACnB;AAED,MAAM,WAAW,YAAY,CAAC,KAAK,GAAG,OAAO,CAAE,SAAQ,QAAQ;IAC7D,IAAI,EAAE,UAAU,CAAC;IACjB,UAAU,EAAE,cAAc,CAAC;IAC3B,iGAAiG;IACjG,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,wFAAwF;IACxF,QAAQ,CAAC,EAAE,cAAc,CAAC,KAAK,CAAC,EAAE,CAAC;IACnC,8FAA8F;IAC9F,QAAQ,EAAE,KAAK,EAAE,CAAC;CACnB;AAED,MAAM,MAAM,OAAO,GAAG,WAAW,GAAG,QAAQ,GAAG,iBAAiB,GAAG,YAAY,CAAC"}
1
+ {"version":3,"file":"dom.d.ts","sourceRoot":"","sources":["../../src/types/dom.ts"],"names":[],"mappings":"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"}
@@ -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,EAAE,WAAW,EAAE,QAAQ,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAExF;;;;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;4BACwB;IACxB,UAAU,EAAE,UAAU,CAAC;IACvB,wFAAwF;IACxF,QAAQ,EAAE,QAAQ,CAAC;IACnB,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;oGACgG;IAChG,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,UAAU,EAAE,aAAa,EAAE,CAAC;IAC5B,wFAAwF;IACxF,eAAe,CAAC,EAAE,mBAAmB,EAAE,CAAC;IACxC;mGAC+F;IAC/F,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB"}
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"}
@@ -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?: string[];
3
- canDeactivate?: string[];
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,MAAM,EAAE,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;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;KAChB,CAAC;IACF,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,QAAQ,CAAC,EAAE,SAAS,EAAE,CAAC;IACvB,qFAAqF;IACrF,cAAc,CAAC,EAAE,mBAAmB,CAAC;CACtC"}
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 {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ui-manifest-json/core",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Shared JSON schema types and framework-agnostic helpers for ui-manifest extractors.",
5
5
  "keywords": [
6
6
  "ui-manifest",