@cleya/contributor-react-navigation 0.0.0-stage → 1.0.0-next.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/README.md +136 -2
- package/dist/ast.d.ts +64 -0
- package/dist/blank.d.ts +97 -0
- package/dist/census.d.ts +43 -0
- package/dist/collect.d.ts +113 -0
- package/dist/dialect.d.ts +58 -0
- package/dist/graph-reader.d.ts +55 -0
- package/dist/nav-calls.d.ts +93 -0
- package/dist/payload.d.ts +21 -0
- package/dist/react-navigation.contributor.d.ts +49 -0
- package/dist/react-navigation.index.d.ts +2 -0
- package/dist/react-navigation.index.js +1522 -0
- package/dist/react-navigation.types.d.ts +42 -0
- package/dist/registrations.d.ts +95 -0
- package/dist/route-names.d.ts +135 -0
- package/dist/type-index.d.ts +92 -0
- package/package.json +37 -5
|
@@ -0,0 +1,1522 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// src/react-navigation.contributor.ts
|
|
3
|
+
import { regionNavigation } from "@cleya/region-navigation";
|
|
4
|
+
|
|
5
|
+
// src/blank.ts
|
|
6
|
+
var stripNoise = (src) => {
|
|
7
|
+
let out = "";
|
|
8
|
+
let i = 0;
|
|
9
|
+
while (i < src.length) {
|
|
10
|
+
const c = src[i];
|
|
11
|
+
if (c === "/" && src[i + 1] === "/") {
|
|
12
|
+
const j = src.indexOf(`
|
|
13
|
+
`, i);
|
|
14
|
+
const end = j < 0 ? src.length : j;
|
|
15
|
+
out += " ".repeat(end - i);
|
|
16
|
+
i = end;
|
|
17
|
+
} else if (c === "/" && src[i + 1] === "*") {
|
|
18
|
+
const j = src.indexOf("*/", i + 2);
|
|
19
|
+
const end = j < 0 ? src.length : j + 2;
|
|
20
|
+
out += " ".repeat(end - i);
|
|
21
|
+
i = end;
|
|
22
|
+
} else if (c === '"' || c === "'" || c === "`") {
|
|
23
|
+
let j = i + 1;
|
|
24
|
+
while (j < src.length && src[j] !== c)
|
|
25
|
+
j += src[j] === "\\" ? 2 : 1;
|
|
26
|
+
const end = Math.min(j + 1, src.length);
|
|
27
|
+
out += " ".repeat(end - i);
|
|
28
|
+
i = end;
|
|
29
|
+
} else {
|
|
30
|
+
out += c;
|
|
31
|
+
i += 1;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return out;
|
|
35
|
+
};
|
|
36
|
+
var lineAt = (src, index) => src.slice(0, index).split(`
|
|
37
|
+
`).length - 1;
|
|
38
|
+
var depthAt = (blanked, index) => {
|
|
39
|
+
let depth = 0;
|
|
40
|
+
for (let i = 0;i < index; i++) {
|
|
41
|
+
const ch = blanked[i];
|
|
42
|
+
if (ch === "{")
|
|
43
|
+
depth += 1;
|
|
44
|
+
else if (ch === "}")
|
|
45
|
+
depth -= 1;
|
|
46
|
+
}
|
|
47
|
+
return depth;
|
|
48
|
+
};
|
|
49
|
+
var topLevelAttr = (attrsSrc, attrsBlanked, attr) => {
|
|
50
|
+
for (const m of attrsBlanked.matchAll(new RegExp(`\\b${attr}\\s*=`, "g"))) {
|
|
51
|
+
if (depthAt(attrsBlanked, m.index) !== 0)
|
|
52
|
+
continue;
|
|
53
|
+
let at = m.index + m[0].length;
|
|
54
|
+
while (at < attrsSrc.length && /\s/.test(attrsSrc[at]))
|
|
55
|
+
at += 1;
|
|
56
|
+
const quote = attrsSrc[at];
|
|
57
|
+
if (quote === '"' || quote === "'") {
|
|
58
|
+
const end = attrsSrc.indexOf(quote, at + 1);
|
|
59
|
+
if (end > 0)
|
|
60
|
+
return { literal: attrsSrc.slice(at + 1, end), expr: null, at: at + 1 };
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (attrsBlanked[at] !== "{")
|
|
64
|
+
continue;
|
|
65
|
+
let depth = 0;
|
|
66
|
+
for (let i = at;i < attrsBlanked.length; i++) {
|
|
67
|
+
const ch = attrsBlanked[i];
|
|
68
|
+
if (ch === "{")
|
|
69
|
+
depth += 1;
|
|
70
|
+
else if (ch === "}") {
|
|
71
|
+
depth -= 1;
|
|
72
|
+
if (depth === 0)
|
|
73
|
+
return { literal: null, expr: attrsSrc.slice(at + 1, i), at: at + 1 };
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { literal: null, expr: null, at: null };
|
|
78
|
+
};
|
|
79
|
+
var spanOf = (blanked, symbol) => {
|
|
80
|
+
const decl = new RegExp(`\\b(?:const|let|var)\\s+${symbol}\\b`).exec(blanked);
|
|
81
|
+
if (!decl)
|
|
82
|
+
return null;
|
|
83
|
+
const eq = /=(?!=|>)/.exec(blanked.slice(decl.index + decl[0].length));
|
|
84
|
+
if (!eq)
|
|
85
|
+
return null;
|
|
86
|
+
const from = decl.index + decl[0].length + eq.index + 1;
|
|
87
|
+
let open = -1;
|
|
88
|
+
for (let i = from;i < blanked.length; i++) {
|
|
89
|
+
if (blanked[i] === "{" || blanked[i] === "[") {
|
|
90
|
+
open = i;
|
|
91
|
+
break;
|
|
92
|
+
}
|
|
93
|
+
if (blanked[i] === ";")
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
if (open < 0)
|
|
97
|
+
return null;
|
|
98
|
+
let depth = 0;
|
|
99
|
+
for (let i = open;i < blanked.length; i++) {
|
|
100
|
+
const ch = blanked[i];
|
|
101
|
+
if (ch === "{" || ch === "[")
|
|
102
|
+
depth += 1;
|
|
103
|
+
else if (ch === "}" || ch === "]") {
|
|
104
|
+
depth -= 1;
|
|
105
|
+
if (depth === 0)
|
|
106
|
+
return [open, i + 1];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
};
|
|
111
|
+
var keysAtDepth1 = (blanked, objStart) => {
|
|
112
|
+
const keys = [];
|
|
113
|
+
const IDENT = /^["']?([A-Za-z_$][\w$]*)["']?\s*:/;
|
|
114
|
+
const COMPUTED = /^\[\s*([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\s*\]\s*:/;
|
|
115
|
+
let depth = 0;
|
|
116
|
+
let i = objStart;
|
|
117
|
+
let pending = null;
|
|
118
|
+
while (i < blanked.length) {
|
|
119
|
+
const ch = blanked[i];
|
|
120
|
+
if (ch === "{" || ch === "[" || ch === "(") {
|
|
121
|
+
const computed = ch === "[" && depth === 1 ? COMPUTED.exec(blanked.slice(i, i + 120)) : null;
|
|
122
|
+
if (computed) {
|
|
123
|
+
if (pending)
|
|
124
|
+
keys.push({ ...pending, valueEnd: i });
|
|
125
|
+
pending = {
|
|
126
|
+
key: computed[1],
|
|
127
|
+
at: i,
|
|
128
|
+
keyAt: i + computed[0].indexOf(computed[1]),
|
|
129
|
+
valueAt: i + computed[0].length
|
|
130
|
+
};
|
|
131
|
+
i += computed[0].length;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
depth += 1;
|
|
135
|
+
if (depth === 1) {
|
|
136
|
+
i += 1;
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
} else if (ch === "}" || ch === "]" || ch === ")") {
|
|
140
|
+
depth -= 1;
|
|
141
|
+
if (depth === 0) {
|
|
142
|
+
if (pending)
|
|
143
|
+
keys.push({ ...pending, valueEnd: i });
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
} else if (depth === 1) {
|
|
147
|
+
if (ch === "," && pending) {
|
|
148
|
+
keys.push({ ...pending, valueEnd: i });
|
|
149
|
+
pending = null;
|
|
150
|
+
}
|
|
151
|
+
const m = IDENT.exec(blanked.slice(i, i + 80));
|
|
152
|
+
const prev = blanked[i - 1] ?? "";
|
|
153
|
+
if (m && (i === objStart + 1 || `,{
|
|
154
|
+
\r`.includes(prev))) {
|
|
155
|
+
pending = { key: m[1], at: i, keyAt: i, valueAt: i + m[0].length };
|
|
156
|
+
i += m[0].length;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
i += 1;
|
|
161
|
+
}
|
|
162
|
+
return keys;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
// src/census.ts
|
|
166
|
+
var newCensus = () => ({
|
|
167
|
+
jsxTags: 0,
|
|
168
|
+
jsxMissed: [],
|
|
169
|
+
callsSeen: 0,
|
|
170
|
+
callsResolved: 0,
|
|
171
|
+
unknown: [],
|
|
172
|
+
patterned: [],
|
|
173
|
+
patternedCount: 0,
|
|
174
|
+
dynamic: []
|
|
175
|
+
});
|
|
176
|
+
var SAMPLES = 5;
|
|
177
|
+
var sample = (into, line) => {
|
|
178
|
+
if (into.length < SAMPLES)
|
|
179
|
+
into.push(line);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// src/dialect.ts
|
|
183
|
+
var DEFAULTS = {
|
|
184
|
+
helperCalls: [],
|
|
185
|
+
configKeys: ["screen"],
|
|
186
|
+
labelProps: ["label", "title"],
|
|
187
|
+
registryEntry: { name: "name", component: "component" },
|
|
188
|
+
guardKey: "if",
|
|
189
|
+
predicates: [],
|
|
190
|
+
exclude: [".test.", ".spec.", ".stories.", "__mocks__/"],
|
|
191
|
+
routeAliasFiles: [],
|
|
192
|
+
closureDepth: 6,
|
|
193
|
+
graphLabel: (path) => {
|
|
194
|
+
const folder = path.split("/").slice(-2, -1)[0] ?? "root";
|
|
195
|
+
const name = folder.replace(/(Stack)?Navigator$/, "") || folder;
|
|
196
|
+
return name.charAt(0).toLowerCase() + name.slice(1);
|
|
197
|
+
}
|
|
198
|
+
};
|
|
199
|
+
var checkPattern = (axis, re) => {
|
|
200
|
+
if (!re.global)
|
|
201
|
+
throw new Error(`[react-navigation] ${axis}: la regex doit porter le drapeau /g`);
|
|
202
|
+
if (new RegExp(`${re.source}|`).exec("").length - 1 < 1)
|
|
203
|
+
throw new Error(`[react-navigation] ${axis}: la regex doit avoir un groupe de capture`);
|
|
204
|
+
return re;
|
|
205
|
+
};
|
|
206
|
+
var resolveDialect = (dialect = {}) => {
|
|
207
|
+
const configured = new Set(Object.entries(dialect).filter(([, value]) => value !== undefined).map(([key]) => key));
|
|
208
|
+
for (const re of dialect.helperCalls ?? [])
|
|
209
|
+
checkPattern("helperCalls", re);
|
|
210
|
+
for (const p of dialect.predicates ?? [])
|
|
211
|
+
checkPattern(`predicates:${p.kind}`, p.pattern);
|
|
212
|
+
return {
|
|
213
|
+
helperCalls: dialect.helperCalls ?? DEFAULTS.helperCalls,
|
|
214
|
+
configKeys: dialect.configKeys ?? DEFAULTS.configKeys,
|
|
215
|
+
labelProps: dialect.labelProps ?? DEFAULTS.labelProps,
|
|
216
|
+
registryEntry: dialect.registryEntry ?? DEFAULTS.registryEntry,
|
|
217
|
+
guardKey: dialect.guardKey ?? DEFAULTS.guardKey,
|
|
218
|
+
predicates: dialect.predicates ?? DEFAULTS.predicates,
|
|
219
|
+
exclude: dialect.exclude ?? DEFAULTS.exclude,
|
|
220
|
+
routeAliasFiles: dialect.routeAliasFiles ?? DEFAULTS.routeAliasFiles,
|
|
221
|
+
closureDepth: dialect.closureDepth ?? DEFAULTS.closureDepth,
|
|
222
|
+
graphLabel: dialect.graphLabel ?? DEFAULTS.graphLabel,
|
|
223
|
+
configured
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
var bump = (counters, axis, n = 1) => {
|
|
227
|
+
counters[axis] = (counters[axis] ?? 0) + n;
|
|
228
|
+
};
|
|
229
|
+
var FACTORY = /\bcreate[A-Za-z]*Navigator\s*[(<]/;
|
|
230
|
+
|
|
231
|
+
// src/ast.ts
|
|
232
|
+
import ts from "typescript";
|
|
233
|
+
var TYPE_POSITIONS = new Set([
|
|
234
|
+
ts.SyntaxKind.LiteralType,
|
|
235
|
+
ts.SyntaxKind.TypeAliasDeclaration,
|
|
236
|
+
ts.SyntaxKind.InterfaceDeclaration,
|
|
237
|
+
ts.SyntaxKind.EnumDeclaration
|
|
238
|
+
]);
|
|
239
|
+
var tagName = (node) => node.tagName.getText();
|
|
240
|
+
var labelOf = (node, labelProps) => {
|
|
241
|
+
for (const attr of node.attributes.properties) {
|
|
242
|
+
if (!ts.isJsxAttribute(attr) || !ts.isIdentifier(attr.name))
|
|
243
|
+
continue;
|
|
244
|
+
if (!labelProps.includes(attr.name.text))
|
|
245
|
+
continue;
|
|
246
|
+
const value = attr.initializer;
|
|
247
|
+
if (value && ts.isStringLiteral(value))
|
|
248
|
+
return value.text;
|
|
249
|
+
if (value && ts.isJsxExpression(value) && value.expression && ts.isStringLiteral(value.expression))
|
|
250
|
+
return value.expression.text;
|
|
251
|
+
}
|
|
252
|
+
return "";
|
|
253
|
+
};
|
|
254
|
+
var declaredName = (node) => {
|
|
255
|
+
if (ts.isFunctionDeclaration(node) || ts.isClassDeclaration(node))
|
|
256
|
+
return node.name ? node.name.text : null;
|
|
257
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name))
|
|
258
|
+
return node.name.text;
|
|
259
|
+
if (ts.isMethodDeclaration(node) && ts.isIdentifier(node.name))
|
|
260
|
+
return node.name.text;
|
|
261
|
+
if (ts.isPropertyAssignment(node) && (ts.isIdentifier(node.name) || ts.isStringLiteral(node.name)) && (ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer)))
|
|
262
|
+
return node.name.text;
|
|
263
|
+
return null;
|
|
264
|
+
};
|
|
265
|
+
var astIndex = (path, src) => {
|
|
266
|
+
const sourceFile = ts.createSourceFile(path, src, ts.ScriptTarget.Latest, true, path.endsWith(".tsx") ? ts.ScriptKind.TSX : ts.ScriptKind.TS);
|
|
267
|
+
const nodeAt = (offset) => {
|
|
268
|
+
let deepest = sourceFile;
|
|
269
|
+
const descend = (node) => {
|
|
270
|
+
if (offset < node.getStart(sourceFile) || offset >= node.getEnd())
|
|
271
|
+
return;
|
|
272
|
+
deepest = node;
|
|
273
|
+
node.forEachChild(descend);
|
|
274
|
+
};
|
|
275
|
+
sourceFile.forEachChild(descend);
|
|
276
|
+
return deepest;
|
|
277
|
+
};
|
|
278
|
+
return {
|
|
279
|
+
typeOnly: (offset) => {
|
|
280
|
+
for (let node = nodeAt(offset);node; node = node.parent)
|
|
281
|
+
if (TYPE_POSITIONS.has(node.kind))
|
|
282
|
+
return true;
|
|
283
|
+
return false;
|
|
284
|
+
},
|
|
285
|
+
literalAt: (offset) => {
|
|
286
|
+
const node = nodeAt(offset);
|
|
287
|
+
return ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) ? node.getStart(sourceFile) : offset;
|
|
288
|
+
},
|
|
289
|
+
context: (offset, labelProps) => {
|
|
290
|
+
for (let node = nodeAt(offset);node; node = node.parent) {
|
|
291
|
+
if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node))
|
|
292
|
+
return { trigger: tagName(node), label: labelOf(node, labelProps) };
|
|
293
|
+
const name = declaredName(node);
|
|
294
|
+
if (name)
|
|
295
|
+
return { trigger: name, label: "" };
|
|
296
|
+
}
|
|
297
|
+
return { trigger: "", label: "" };
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
// src/route-names.ts
|
|
303
|
+
var collectEnums = (src, blanked, into) => {
|
|
304
|
+
for (const m of blanked.matchAll(/\benum\s+([A-Za-z_$][\w$]*)\s*\{/g)) {
|
|
305
|
+
const open = m.index + m[0].length - 1;
|
|
306
|
+
let depth = 0;
|
|
307
|
+
let end = blanked.length;
|
|
308
|
+
for (let i = open;i < blanked.length; i++) {
|
|
309
|
+
const ch = blanked[i];
|
|
310
|
+
if (ch === "{")
|
|
311
|
+
depth += 1;
|
|
312
|
+
else if (ch === "}") {
|
|
313
|
+
depth -= 1;
|
|
314
|
+
if (depth === 0) {
|
|
315
|
+
end = i;
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
for (const member of src.slice(open + 1, end).matchAll(/([A-Za-z_$][\w$]*)\s*=\s*["']([^"']*)["']/g))
|
|
321
|
+
into.set(`${m[1]}.${member[1]}`, member[2]);
|
|
322
|
+
}
|
|
323
|
+
};
|
|
324
|
+
var REF_PATH = /^\s*([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+)/;
|
|
325
|
+
var resolveRef = (names, ref) => {
|
|
326
|
+
let path = ref;
|
|
327
|
+
for (;; ) {
|
|
328
|
+
const hit = names.get(path);
|
|
329
|
+
if (hit !== undefined)
|
|
330
|
+
return hit;
|
|
331
|
+
const cut = path.lastIndexOf(".");
|
|
332
|
+
if (cut <= 0)
|
|
333
|
+
return null;
|
|
334
|
+
path = path.slice(0, cut);
|
|
335
|
+
}
|
|
336
|
+
};
|
|
337
|
+
var keyRoute = (key, keyAt, file, resolve) => {
|
|
338
|
+
const resolved = resolve(file, keyAt, keyAt + key.length, key);
|
|
339
|
+
return resolved ?? (key.includes(".") ? null : key);
|
|
340
|
+
};
|
|
341
|
+
var routeNameOf = (value, file, base, resolve) => {
|
|
342
|
+
if (value.literal !== null)
|
|
343
|
+
return value.literal;
|
|
344
|
+
if (value.expr === null || value.at === null)
|
|
345
|
+
return null;
|
|
346
|
+
const ref = REF_PATH.exec(value.expr);
|
|
347
|
+
return resolve(file, base + value.at, base + value.at + value.expr.length, ref?.[1] ?? value.expr);
|
|
348
|
+
};
|
|
349
|
+
var REFS_IN = /\b([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+)/g;
|
|
350
|
+
var collectAliases = (src, blanked, objStart, file, resolve, into, depth) => {
|
|
351
|
+
if (depth > 12)
|
|
352
|
+
return;
|
|
353
|
+
for (const { key, keyAt, valueAt, valueEnd } of keysAtDepth1(blanked, objStart)) {
|
|
354
|
+
const valueBlanked = blanked.slice(valueAt, valueEnd);
|
|
355
|
+
const nested = /\bscreens\s*:\s*\{/.exec(valueBlanked);
|
|
356
|
+
if (nested) {
|
|
357
|
+
collectAliases(src, blanked, valueAt + nested.index + nested[0].length - 1, file, resolve, into, depth + 1);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
const route = keyRoute(key, keyAt, file, resolve);
|
|
361
|
+
if (!route)
|
|
362
|
+
continue;
|
|
363
|
+
for (const m of src.slice(valueAt, valueEnd).matchAll(REFS_IN)) {
|
|
364
|
+
const ref = m[1];
|
|
365
|
+
const at = valueAt + m.index;
|
|
366
|
+
const url = resolve(file, at, at + ref.length, ref);
|
|
367
|
+
if (url && !into.has(url))
|
|
368
|
+
into.set(url, route);
|
|
369
|
+
const cut = ref.lastIndexOf(".");
|
|
370
|
+
for (const path of cut > 0 ? [ref, ref.slice(0, cut)] : [ref])
|
|
371
|
+
if (!into.has(path))
|
|
372
|
+
into.set(path, route);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
};
|
|
376
|
+
var isNested = (id) => id.split("|").length > 4;
|
|
377
|
+
|
|
378
|
+
// src/nav-calls.ts
|
|
379
|
+
var REF = String.raw`([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+)`;
|
|
380
|
+
var callPatterns = (d) => [
|
|
381
|
+
{
|
|
382
|
+
re: /\bnavigate\s*\(\s*["']([^"']+)["']/g,
|
|
383
|
+
kind: "navigate",
|
|
384
|
+
via: "imperative-call",
|
|
385
|
+
axis: "core"
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
re: new RegExp(String.raw`\bnavigate\s*\(\s*${REF}`, "g"),
|
|
389
|
+
kind: "navigate",
|
|
390
|
+
via: "imperative-call",
|
|
391
|
+
axis: "core",
|
|
392
|
+
ref: true
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
re: /\bpush\s*\(\s*["']([^"']+)["']/g,
|
|
396
|
+
kind: "push",
|
|
397
|
+
via: "imperative-call",
|
|
398
|
+
axis: "core"
|
|
399
|
+
},
|
|
400
|
+
{
|
|
401
|
+
re: new RegExp(String.raw`\bpush\s*\(\s*${REF}`, "g"),
|
|
402
|
+
kind: "push",
|
|
403
|
+
via: "imperative-call",
|
|
404
|
+
axis: "core",
|
|
405
|
+
ref: true
|
|
406
|
+
},
|
|
407
|
+
{
|
|
408
|
+
re: /\breplace\s*\(\s*["']([^"']+)["']/g,
|
|
409
|
+
kind: "replace",
|
|
410
|
+
via: "imperative-call",
|
|
411
|
+
axis: "core"
|
|
412
|
+
},
|
|
413
|
+
{
|
|
414
|
+
re: new RegExp(String.raw`\breplace\s*\(\s*${REF}`, "g"),
|
|
415
|
+
kind: "replace",
|
|
416
|
+
via: "imperative-call",
|
|
417
|
+
axis: "core",
|
|
418
|
+
ref: true
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
re: /\breset\s*\(\s*["']([^"']+)["']/g,
|
|
422
|
+
kind: "reset",
|
|
423
|
+
via: "imperative-call",
|
|
424
|
+
axis: "core"
|
|
425
|
+
},
|
|
426
|
+
{
|
|
427
|
+
re: new RegExp(String.raw`\breset\s*\(\s*${REF}`, "g"),
|
|
428
|
+
kind: "reset",
|
|
429
|
+
via: "imperative-call",
|
|
430
|
+
axis: "core",
|
|
431
|
+
ref: true
|
|
432
|
+
},
|
|
433
|
+
{
|
|
434
|
+
re: /navigateTo\s*=\s*\{\{\s*screen\s*:\s*["']([^"']+)["']/g,
|
|
435
|
+
kind: "link",
|
|
436
|
+
via: "declarative-link",
|
|
437
|
+
axis: "core"
|
|
438
|
+
},
|
|
439
|
+
{
|
|
440
|
+
re: new RegExp(String.raw`navigateTo\s*=\s*\{\{\s*screen\s*:\s*${REF}`, "g"),
|
|
441
|
+
kind: "link",
|
|
442
|
+
via: "declarative-link",
|
|
443
|
+
axis: "core",
|
|
444
|
+
ref: true
|
|
445
|
+
},
|
|
446
|
+
...d.helperCalls.map((re) => ({
|
|
447
|
+
re,
|
|
448
|
+
kind: "navigate",
|
|
449
|
+
via: "helper",
|
|
450
|
+
axis: "helperCalls"
|
|
451
|
+
})),
|
|
452
|
+
...d.configKeys.flatMap((key) => [
|
|
453
|
+
{
|
|
454
|
+
re: new RegExp(`\\b${key}\\s*:\\s*["']([^"']+)["']`, "g"),
|
|
455
|
+
kind: "navigate",
|
|
456
|
+
via: "config",
|
|
457
|
+
axis: "configKeys"
|
|
458
|
+
},
|
|
459
|
+
{
|
|
460
|
+
re: new RegExp(String.raw`\b${key}\s*:\s*${REF}`, "g"),
|
|
461
|
+
kind: "navigate",
|
|
462
|
+
via: "config",
|
|
463
|
+
axis: "configKeys",
|
|
464
|
+
ref: true
|
|
465
|
+
}
|
|
466
|
+
])
|
|
467
|
+
];
|
|
468
|
+
var CENSUS_CALL = /\bnavigate\s*\(/g;
|
|
469
|
+
var PATTERN_LIMIT = 8;
|
|
470
|
+
var firstArgEnd = (blanked, from) => {
|
|
471
|
+
let depth = 0;
|
|
472
|
+
for (let i = from;i < blanked.length; i++) {
|
|
473
|
+
const ch = blanked[i];
|
|
474
|
+
if (ch === "(" || ch === "[" || ch === "{")
|
|
475
|
+
depth += 1;
|
|
476
|
+
else if (ch === ")" || ch === "]" || ch === "}") {
|
|
477
|
+
if (depth === 0)
|
|
478
|
+
return i;
|
|
479
|
+
depth -= 1;
|
|
480
|
+
} else if (ch === "," && depth === 0)
|
|
481
|
+
return i;
|
|
482
|
+
}
|
|
483
|
+
return blanked.length;
|
|
484
|
+
};
|
|
485
|
+
var NAV_CALL = /\b(navigate|push|replace|reset)\s*\(/g;
|
|
486
|
+
var ROUTE_ENTRY = /\bname\s*:\s*["']([^"']+)["']/g;
|
|
487
|
+
var ROUTE_TERNARY = /\?\s*["']([^"']+)["']\s*:\s*["']([^"']+)["']/g;
|
|
488
|
+
var argsEnd = (blanked, from) => {
|
|
489
|
+
let depth = 0;
|
|
490
|
+
for (let i = from;i < blanked.length; i++) {
|
|
491
|
+
const ch = blanked[i];
|
|
492
|
+
if (ch === "(")
|
|
493
|
+
depth += 1;
|
|
494
|
+
else if (ch === ")") {
|
|
495
|
+
if (depth === 0)
|
|
496
|
+
return i;
|
|
497
|
+
depth -= 1;
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
return blanked.length;
|
|
501
|
+
};
|
|
502
|
+
var spreadTargets = (src, blanked, routeNames) => {
|
|
503
|
+
const out = [];
|
|
504
|
+
for (const call of blanked.matchAll(NAV_CALL)) {
|
|
505
|
+
const from = call.index + call[0].length;
|
|
506
|
+
const region = src.slice(from, argsEnd(blanked, from));
|
|
507
|
+
for (const re of [ROUTE_ENTRY, ROUTE_TERNARY])
|
|
508
|
+
for (const m of region.matchAll(re))
|
|
509
|
+
for (const route of m.slice(1))
|
|
510
|
+
if (route && routeNames.has(route))
|
|
511
|
+
out.push({
|
|
512
|
+
route,
|
|
513
|
+
index: call.index,
|
|
514
|
+
kind: call[1]
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
return out;
|
|
518
|
+
};
|
|
519
|
+
var callSitesIn = (src, blanked, path, routeNames, d, resolve, counters, census, classify) => {
|
|
520
|
+
const out = [];
|
|
521
|
+
const seen = new Set;
|
|
522
|
+
let parsed = null;
|
|
523
|
+
const ast = () => parsed ??= astIndex(path, src);
|
|
524
|
+
for (const { re, kind, via, axis, ref } of callPatterns(d)) {
|
|
525
|
+
re.lastIndex = 0;
|
|
526
|
+
for (const m of src.matchAll(re)) {
|
|
527
|
+
const captured = m[1];
|
|
528
|
+
const at = m.index + m[0].lastIndexOf(captured);
|
|
529
|
+
const route = ref ? resolve(path, at, at + captured.length, captured) : captured;
|
|
530
|
+
if (!route || !routeNames.has(route))
|
|
531
|
+
continue;
|
|
532
|
+
if (ast().typeOnly(at))
|
|
533
|
+
continue;
|
|
534
|
+
const key = `${route}|${ast().literalAt(at)}`;
|
|
535
|
+
if (seen.has(key))
|
|
536
|
+
continue;
|
|
537
|
+
seen.add(key);
|
|
538
|
+
const line = lineAt(src, m.index);
|
|
539
|
+
bump(counters, axis);
|
|
540
|
+
out.push({
|
|
541
|
+
route,
|
|
542
|
+
file: path,
|
|
543
|
+
line,
|
|
544
|
+
kind,
|
|
545
|
+
via,
|
|
546
|
+
...ast().context(at, d.labelProps)
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
for (const hit of spreadTargets(src, blanked, routeNames)) {
|
|
551
|
+
if (ast().typeOnly(hit.index))
|
|
552
|
+
continue;
|
|
553
|
+
const key = `${hit.route}|${ast().literalAt(hit.index)}`;
|
|
554
|
+
if (seen.has(key))
|
|
555
|
+
continue;
|
|
556
|
+
seen.add(key);
|
|
557
|
+
const line = lineAt(src, hit.index);
|
|
558
|
+
bump(counters, "core");
|
|
559
|
+
out.push({
|
|
560
|
+
route: hit.route,
|
|
561
|
+
file: path,
|
|
562
|
+
line,
|
|
563
|
+
kind: hit.kind,
|
|
564
|
+
via: "imperative-call",
|
|
565
|
+
...ast().context(hit.index, d.labelProps)
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
CENSUS_CALL.lastIndex = 0;
|
|
569
|
+
for (const call of blanked.matchAll(CENSUS_CALL)) {
|
|
570
|
+
const from = call.index + call[0].length;
|
|
571
|
+
if (ast().typeOnly(from))
|
|
572
|
+
continue;
|
|
573
|
+
census.callsSeen += 1;
|
|
574
|
+
let start = from;
|
|
575
|
+
while (start < src.length && /\s/.test(src[start]))
|
|
576
|
+
start += 1;
|
|
577
|
+
const end = firstArgEnd(blanked, from);
|
|
578
|
+
const expr = src.slice(start, end).trim();
|
|
579
|
+
if (expr.length === 0)
|
|
580
|
+
continue;
|
|
581
|
+
const ref = REF_PATH.exec(expr);
|
|
582
|
+
const verdict = classify(path, start, start + expr.length, ref?.[1] ?? expr);
|
|
583
|
+
const where = `${path}:${lineAt(src, call.index) + 1} ${expr.replace(/\s+/g, " ").slice(0, 60)}`;
|
|
584
|
+
if (verdict === "resolved")
|
|
585
|
+
census.callsResolved += 1;
|
|
586
|
+
else if (verdict === "unknown")
|
|
587
|
+
sample(census.unknown, where);
|
|
588
|
+
else if (verdict === "pattern") {
|
|
589
|
+
census.patternedCount += 1;
|
|
590
|
+
sample(census.patterned, where);
|
|
591
|
+
} else
|
|
592
|
+
sample(census.dynamic, where);
|
|
593
|
+
}
|
|
594
|
+
return out;
|
|
595
|
+
};
|
|
596
|
+
var fileOfSymbolId = (id) => id.split("|")[2] ?? "";
|
|
597
|
+
|
|
598
|
+
// src/registrations.ts
|
|
599
|
+
var unwrapComponent = (expr) => {
|
|
600
|
+
let current = expr.trim();
|
|
601
|
+
for (let guard = 0;guard < 8; guard++) {
|
|
602
|
+
const call = /^([A-Za-z_$][\w$.]*)\s*\(/.exec(current);
|
|
603
|
+
if (!call)
|
|
604
|
+
break;
|
|
605
|
+
let depth = 0;
|
|
606
|
+
let start = -1;
|
|
607
|
+
let end = -1;
|
|
608
|
+
for (let i = call[0].length - 1;i < current.length; i++) {
|
|
609
|
+
const ch = current[i];
|
|
610
|
+
if (ch === "(") {
|
|
611
|
+
depth += 1;
|
|
612
|
+
if (depth === 1)
|
|
613
|
+
start = i + 1;
|
|
614
|
+
} else if (ch === ")") {
|
|
615
|
+
depth -= 1;
|
|
616
|
+
if (depth === 0) {
|
|
617
|
+
end = i;
|
|
618
|
+
break;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
if (start < 0 || end < 0)
|
|
623
|
+
break;
|
|
624
|
+
const inner = current.slice(start, end).trim();
|
|
625
|
+
if (!inner || /^\(?\s*\)/.test(inner))
|
|
626
|
+
break;
|
|
627
|
+
current = inner;
|
|
628
|
+
}
|
|
629
|
+
const ident = /^([A-Za-z_$][\w$.]*)/.exec(current);
|
|
630
|
+
return ident ? ident[1].split(".").pop() ?? null : null;
|
|
631
|
+
};
|
|
632
|
+
var LAZY_MODULE = /\brequire\s*(?:<[^>]*>)?\s*\(\s*["']([^"']+)["']/;
|
|
633
|
+
var lazyLoaders = (src, blanked) => {
|
|
634
|
+
const out = new Map;
|
|
635
|
+
for (const decl of blanked.matchAll(/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=/g)) {
|
|
636
|
+
const from = decl.index + decl[0].length;
|
|
637
|
+
const semi = blanked.indexOf(";", from);
|
|
638
|
+
const module = LAZY_MODULE.exec(src.slice(from, semi < 0 ? from + 400 : semi));
|
|
639
|
+
if (module)
|
|
640
|
+
out.set(decl[1], module[1]);
|
|
641
|
+
}
|
|
642
|
+
return out;
|
|
643
|
+
};
|
|
644
|
+
var jsxComponent = (component, getter, loaders) => {
|
|
645
|
+
if (component.expr)
|
|
646
|
+
return unwrapComponent(component.expr);
|
|
647
|
+
if (!getter.expr)
|
|
648
|
+
return null;
|
|
649
|
+
const inline = LAZY_MODULE.exec(getter.expr);
|
|
650
|
+
if (inline)
|
|
651
|
+
return `module:${inline[1]}`;
|
|
652
|
+
const ident = /^\s*([A-Za-z_$][\w$]*)\s*$/.exec(getter.expr);
|
|
653
|
+
const viaLoader = ident ? loaders.get(ident[1]) : undefined;
|
|
654
|
+
return viaLoader ? `module:${viaLoader}` : unwrapComponent(getter.expr);
|
|
655
|
+
};
|
|
656
|
+
var enclosingDecl = (blanked, before) => {
|
|
657
|
+
let last = null;
|
|
658
|
+
for (const m of blanked.slice(0, before).matchAll(/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=/g))
|
|
659
|
+
last = m[1];
|
|
660
|
+
return last;
|
|
661
|
+
};
|
|
662
|
+
var readEntry = (segment, d) => {
|
|
663
|
+
const compKey = `(?:screen|${d.registryEntry.component})`;
|
|
664
|
+
const comp = new RegExp(`\\b${compKey}\\s*:\\s*([\\s\\S]{0,200}?)(?:,\\s*\\w+\\s*:|\\}|$)`).exec(segment);
|
|
665
|
+
const guard = new RegExp(`\\b${d.guardKey}\\s*:\\s*([A-Za-z_$][\\w$.]*)`).exec(segment);
|
|
666
|
+
return {
|
|
667
|
+
component: comp ? unwrapComponent(comp[1]) : null,
|
|
668
|
+
guard: guard ? guard[1] : null,
|
|
669
|
+
hasOptions: /\boptions\s*:/.test(segment)
|
|
670
|
+
};
|
|
671
|
+
};
|
|
672
|
+
var fromRegistries = (src, blanked, d, file, resolve) => {
|
|
673
|
+
const out = [];
|
|
674
|
+
for (const m of blanked.matchAll(/\bscreens\s*:\s*\{/g)) {
|
|
675
|
+
const objStart = m.index + m[0].length - 1;
|
|
676
|
+
for (const { key, at, keyAt, valueAt, valueEnd } of keysAtDepth1(blanked, objStart)) {
|
|
677
|
+
const segment = src.slice(valueAt, valueEnd);
|
|
678
|
+
if (!new RegExp(`\\b(?:screen|${d.registryEntry.component})\\s*:\\s*[A-Za-z_$]`).test(segment))
|
|
679
|
+
continue;
|
|
680
|
+
const route = keyRoute(key, keyAt, file, resolve);
|
|
681
|
+
if (!route)
|
|
682
|
+
continue;
|
|
683
|
+
out.push({
|
|
684
|
+
route,
|
|
685
|
+
line: lineAt(src, at),
|
|
686
|
+
shape: "static-config",
|
|
687
|
+
...readEntry(segment, d)
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
for (const decl of blanked.matchAll(/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)/g)) {
|
|
692
|
+
const span = spanOf(blanked, decl[1]);
|
|
693
|
+
if (!span)
|
|
694
|
+
continue;
|
|
695
|
+
const [start, end] = span;
|
|
696
|
+
const segment = src.slice(start, end);
|
|
697
|
+
if (!new RegExp(`\\b${d.registryEntry.component}\\s*:`).test(segment))
|
|
698
|
+
continue;
|
|
699
|
+
for (const m of segment.matchAll(new RegExp(`\\b${d.registryEntry.name}\\s*:\\s*["']([^"']+)["']`, "g"))) {
|
|
700
|
+
const tail = segment.slice(m.index + m[0].length, m.index + m[0].length + 500);
|
|
701
|
+
out.push({
|
|
702
|
+
route: m[1],
|
|
703
|
+
line: lineAt(src, start + m.index),
|
|
704
|
+
shape: "array-config",
|
|
705
|
+
...readEntry(tail, d)
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
return out;
|
|
710
|
+
};
|
|
711
|
+
var fromRegistryLiterals = (src, blanked, d, file, resolve, registries) => {
|
|
712
|
+
const out = [];
|
|
713
|
+
for (const { start, viaCall } of registries) {
|
|
714
|
+
if (blanked[start] !== "{")
|
|
715
|
+
continue;
|
|
716
|
+
const graph = viaCall ? enclosingDecl(blanked, start) ?? undefined : undefined;
|
|
717
|
+
for (const { key, at, keyAt, valueAt, valueEnd } of keysAtDepth1(blanked, start)) {
|
|
718
|
+
const route = keyRoute(key, keyAt, file, resolve);
|
|
719
|
+
if (!route)
|
|
720
|
+
continue;
|
|
721
|
+
const segment = src.slice(valueAt, valueEnd);
|
|
722
|
+
const lazy = LAZY_MODULE.exec(segment);
|
|
723
|
+
const entry = readEntry(segment, d);
|
|
724
|
+
out.push({
|
|
725
|
+
route,
|
|
726
|
+
line: lineAt(src, at),
|
|
727
|
+
shape: viaCall ? "factory-arg" : "const-object",
|
|
728
|
+
component: lazy ? `module:${lazy[1]}` : entry.component ?? unwrapComponent(segment),
|
|
729
|
+
guard: entry.guard,
|
|
730
|
+
hasOptions: entry.hasOptions,
|
|
731
|
+
graph
|
|
732
|
+
});
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
return out;
|
|
736
|
+
};
|
|
737
|
+
var fromJsx = (src, blanked, d, file, resolve, census) => {
|
|
738
|
+
const out = [];
|
|
739
|
+
const loaders = lazyLoaders(src, blanked);
|
|
740
|
+
for (const m of blanked.matchAll(/<\s*[\w.]*\.Screen\b/g)) {
|
|
741
|
+
let depth = 0;
|
|
742
|
+
let i = m.index + m[0].length;
|
|
743
|
+
for (;i < blanked.length; i++) {
|
|
744
|
+
const ch = blanked[i];
|
|
745
|
+
if (ch === "{")
|
|
746
|
+
depth += 1;
|
|
747
|
+
else if (ch === "}")
|
|
748
|
+
depth -= 1;
|
|
749
|
+
else if (ch === ">" && depth === 0)
|
|
750
|
+
break;
|
|
751
|
+
}
|
|
752
|
+
const from = m.index + m[0].length;
|
|
753
|
+
const attrs = src.slice(from, i);
|
|
754
|
+
const attrsBlanked = blanked.slice(from, i);
|
|
755
|
+
census.jsxTags += 1;
|
|
756
|
+
const route = routeNameOf(topLevelAttr(attrs, attrsBlanked, "name"), file, from, resolve);
|
|
757
|
+
if (!route) {
|
|
758
|
+
const attr = topLevelAttr(attrs, attrsBlanked, "name");
|
|
759
|
+
sample(census.jsxMissed, `${file}:${lineAt(src, m.index) + 1} name={${attr.expr?.trim().slice(0, 50) ?? "?"}}`);
|
|
760
|
+
continue;
|
|
761
|
+
}
|
|
762
|
+
out.push({
|
|
763
|
+
route,
|
|
764
|
+
line: lineAt(src, m.index),
|
|
765
|
+
shape: "inline-jsx",
|
|
766
|
+
component: jsxComponent(topLevelAttr(attrs, attrsBlanked, "component"), topLevelAttr(attrs, attrsBlanked, "getComponent"), loaders),
|
|
767
|
+
guard: null,
|
|
768
|
+
hasOptions: /\boptions\s*=/.test(attrsBlanked)
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
return out;
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
// src/type-index.ts
|
|
775
|
+
import ts2 from "typescript";
|
|
776
|
+
var createTypeIndex = (projectRoot, files) => {
|
|
777
|
+
const configPath = ts2.findConfigFile(projectRoot, ts2.sys.fileExists, "tsconfig.json");
|
|
778
|
+
if (!configPath)
|
|
779
|
+
return null;
|
|
780
|
+
const started = Date.now();
|
|
781
|
+
const config = ts2.readConfigFile(configPath, ts2.sys.readFile);
|
|
782
|
+
if (config.error)
|
|
783
|
+
return null;
|
|
784
|
+
const parsed = ts2.parseJsonConfigFileContent(config.config, ts2.sys, projectRoot);
|
|
785
|
+
const rootNames = files.map((f) => `${projectRoot}/${f}`);
|
|
786
|
+
const program = ts2.createProgram({
|
|
787
|
+
rootNames,
|
|
788
|
+
options: { ...parsed.options, noEmit: true }
|
|
789
|
+
});
|
|
790
|
+
const checker = program.getTypeChecker();
|
|
791
|
+
const buildMs = Date.now() - started;
|
|
792
|
+
if (program.getSourceFiles().length === 0)
|
|
793
|
+
return null;
|
|
794
|
+
const sources = new Map;
|
|
795
|
+
const sourceOf = (file) => {
|
|
796
|
+
const key = `${projectRoot}/${file}`;
|
|
797
|
+
if (!sources.has(key))
|
|
798
|
+
sources.set(key, program.getSourceFile(key));
|
|
799
|
+
return sources.get(key);
|
|
800
|
+
};
|
|
801
|
+
const nodeIn = (sf, start, end) => {
|
|
802
|
+
const deepest = (node) => {
|
|
803
|
+
for (const child of node.getChildren(sf))
|
|
804
|
+
if (child.getFullStart() <= start && start < child.getEnd())
|
|
805
|
+
return deepest(child);
|
|
806
|
+
return node;
|
|
807
|
+
};
|
|
808
|
+
let node = deepest(sf);
|
|
809
|
+
if (node.getStart(sf) >= end)
|
|
810
|
+
return null;
|
|
811
|
+
while (node.parent && node.parent.getStart(sf) >= start && node.parent.getEnd() <= end)
|
|
812
|
+
node = node.parent;
|
|
813
|
+
return node;
|
|
814
|
+
};
|
|
815
|
+
const patternsOf = (type, out) => {
|
|
816
|
+
if (type.isUnion()) {
|
|
817
|
+
for (const member of type.types)
|
|
818
|
+
patternsOf(member, out);
|
|
819
|
+
return;
|
|
820
|
+
}
|
|
821
|
+
if (type.flags & ts2.TypeFlags.TemplateLiteral) {
|
|
822
|
+
const texts = type.texts;
|
|
823
|
+
out.push(texts.join("*"));
|
|
824
|
+
} else if (type.isStringLiteral())
|
|
825
|
+
out.push(type.value);
|
|
826
|
+
};
|
|
827
|
+
const typeAt = (file, start, end) => {
|
|
828
|
+
const sf = sourceOf(file);
|
|
829
|
+
if (!sf)
|
|
830
|
+
return null;
|
|
831
|
+
const node = nodeIn(sf, start, end);
|
|
832
|
+
return node ? checker.getTypeAtLocation(node) : null;
|
|
833
|
+
};
|
|
834
|
+
const keyLiteral = (name) => {
|
|
835
|
+
if (ts2.isComputedPropertyName(name)) {
|
|
836
|
+
const type = checker.getTypeAtLocation(name.expression);
|
|
837
|
+
return type.isStringLiteral() ? type.value : null;
|
|
838
|
+
}
|
|
839
|
+
return ts2.isStringLiteral(name) ? name.text : null;
|
|
840
|
+
};
|
|
841
|
+
const callable = (value) => checker.getTypeAtLocation(value).getCallSignatures().length > 0;
|
|
842
|
+
const isRegistry = (obj) => {
|
|
843
|
+
const props = obj.properties.filter(ts2.isPropertyAssignment);
|
|
844
|
+
if (props.length === 0 || props.length !== obj.properties.length)
|
|
845
|
+
return false;
|
|
846
|
+
let callables = 0;
|
|
847
|
+
for (const prop of props) {
|
|
848
|
+
if (keyLiteral(prop.name) === null)
|
|
849
|
+
return false;
|
|
850
|
+
if (callable(prop.initializer))
|
|
851
|
+
callables += 1;
|
|
852
|
+
}
|
|
853
|
+
return callables * 2 >= props.length;
|
|
854
|
+
};
|
|
855
|
+
const effectiveParent = (node) => {
|
|
856
|
+
let current = node.parent;
|
|
857
|
+
while (current && (ts2.isAsExpression(current) || ts2.isSatisfiesExpression(current) || ts2.isParenthesizedExpression(current)))
|
|
858
|
+
current = current.parent;
|
|
859
|
+
return current;
|
|
860
|
+
};
|
|
861
|
+
return {
|
|
862
|
+
sourceFiles: program.getSourceFiles().length,
|
|
863
|
+
buildMs,
|
|
864
|
+
registryLiterals(file) {
|
|
865
|
+
const sf = sourceOf(file);
|
|
866
|
+
if (!sf)
|
|
867
|
+
return [];
|
|
868
|
+
const out = [];
|
|
869
|
+
const visit = (node) => {
|
|
870
|
+
if (ts2.isObjectLiteralExpression(node) && isRegistry(node)) {
|
|
871
|
+
const parent = effectiveParent(node);
|
|
872
|
+
out.push({
|
|
873
|
+
start: node.getStart(sf),
|
|
874
|
+
end: node.getEnd(),
|
|
875
|
+
viaCall: !!parent && ts2.isCallExpression(parent)
|
|
876
|
+
});
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
ts2.forEachChild(node, visit);
|
|
880
|
+
};
|
|
881
|
+
visit(sf);
|
|
882
|
+
return out;
|
|
883
|
+
},
|
|
884
|
+
literalIn(file, start, end) {
|
|
885
|
+
const type = typeAt(file, start, end);
|
|
886
|
+
return type?.isStringLiteral() ? type.value : null;
|
|
887
|
+
},
|
|
888
|
+
patternIn(file, start, end) {
|
|
889
|
+
const type = typeAt(file, start, end);
|
|
890
|
+
if (!type)
|
|
891
|
+
return null;
|
|
892
|
+
const out = [];
|
|
893
|
+
patternsOf(type, out);
|
|
894
|
+
return out.length > 0 && !type.isStringLiteral() ? out : null;
|
|
895
|
+
}
|
|
896
|
+
};
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
// src/collect.ts
|
|
900
|
+
var collect = async (deps, d) => {
|
|
901
|
+
const { source, projectRoot, graph, workspace } = deps;
|
|
902
|
+
const counters = {};
|
|
903
|
+
const census = newCensus();
|
|
904
|
+
const paths = (await source.listFiles()).filter((p) => !d.exclude.some((x) => p.includes(x)));
|
|
905
|
+
const sources = new Map;
|
|
906
|
+
const factoryFiles = [];
|
|
907
|
+
const names = new Map;
|
|
908
|
+
for (const path of paths) {
|
|
909
|
+
const src = await source.read(path);
|
|
910
|
+
sources.set(path, src);
|
|
911
|
+
if (FACTORY.test(src))
|
|
912
|
+
factoryFiles.push(path);
|
|
913
|
+
if (src.includes("enum "))
|
|
914
|
+
collectEnums(src, stripNoise(src), names);
|
|
915
|
+
}
|
|
916
|
+
const types = createTypeIndex(projectRoot, paths);
|
|
917
|
+
const aliases = new Map;
|
|
918
|
+
const literalOf = (file, start, end, text) => types?.literalIn(file, start, end) ?? resolveRef(names, text);
|
|
919
|
+
const resolve = (file, start, end, text) => {
|
|
920
|
+
const byType = types?.literalIn(file, start, end) ?? null;
|
|
921
|
+
if (byType !== null) {
|
|
922
|
+
bump(counters, "types");
|
|
923
|
+
return byType;
|
|
924
|
+
}
|
|
925
|
+
const byTable = resolveRef(names, text);
|
|
926
|
+
if (byTable !== null)
|
|
927
|
+
bump(counters, "tables");
|
|
928
|
+
return byTable;
|
|
929
|
+
};
|
|
930
|
+
for (const fragment of d.routeAliasFiles) {
|
|
931
|
+
for (const path of paths) {
|
|
932
|
+
if (!path.endsWith(fragment))
|
|
933
|
+
continue;
|
|
934
|
+
const src = sources.get(path);
|
|
935
|
+
const blanked = stripNoise(src);
|
|
936
|
+
for (const m of blanked.matchAll(/\bscreens\s*:\s*\{/g))
|
|
937
|
+
collectAliases(src, blanked, m.index + m[0].length - 1, path, resolve, aliases, 0);
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
bump(counters, "routeAliasFiles", aliases.size);
|
|
941
|
+
const dirs = new Set(factoryFiles.map((p) => p.split("/").slice(0, -1).join("/")));
|
|
942
|
+
const found = [];
|
|
943
|
+
for (const path of paths) {
|
|
944
|
+
if (!dirs.has(path.split("/").slice(0, -1).join("/")))
|
|
945
|
+
continue;
|
|
946
|
+
const src = sources.get(path);
|
|
947
|
+
const blanked = stripNoise(src);
|
|
948
|
+
const regs = [
|
|
949
|
+
...fromRegistries(src, blanked, d, path, resolve),
|
|
950
|
+
...fromRegistryLiterals(src, blanked, d, path, resolve, types?.registryLiterals(path) ?? []),
|
|
951
|
+
...fromJsx(src, blanked, d, path, resolve, census)
|
|
952
|
+
];
|
|
953
|
+
if (regs.length === 0)
|
|
954
|
+
continue;
|
|
955
|
+
bump(counters, "registries", regs.filter((r) => r.shape !== "inline-jsx").length);
|
|
956
|
+
for (const reg of regs)
|
|
957
|
+
found.push({ graph: reg.graph ?? d.graphLabel(path), file: path, reg });
|
|
958
|
+
}
|
|
959
|
+
const duplicates = [];
|
|
960
|
+
{
|
|
961
|
+
const seen = new Set;
|
|
962
|
+
let write = 0;
|
|
963
|
+
for (const entry of found) {
|
|
964
|
+
const key = `${entry.graph}|${entry.reg.route}`;
|
|
965
|
+
if (seen.has(key)) {
|
|
966
|
+
duplicates.push(`${key} (${entry.file})`);
|
|
967
|
+
continue;
|
|
968
|
+
}
|
|
969
|
+
seen.add(key);
|
|
970
|
+
found[write++] = entry;
|
|
971
|
+
}
|
|
972
|
+
found.length = write;
|
|
973
|
+
}
|
|
974
|
+
const navigatorFiles = [...new Set(found.map((f) => f.file))];
|
|
975
|
+
const byFile = new Map;
|
|
976
|
+
const ambiguous = new Set;
|
|
977
|
+
for (const row of await graph.componentCandidates(navigatorFiles)) {
|
|
978
|
+
if (!row.name || !row.symbolId)
|
|
979
|
+
continue;
|
|
980
|
+
if (isNested(row.symbolId))
|
|
981
|
+
continue;
|
|
982
|
+
const index = byFile.get(row.file) ?? new Map;
|
|
983
|
+
const previous = index.get(row.name);
|
|
984
|
+
if (previous && previous !== row.symbolId)
|
|
985
|
+
ambiguous.add(`${row.file}|${row.name}`);
|
|
986
|
+
else
|
|
987
|
+
index.set(row.name, row.symbolId);
|
|
988
|
+
byFile.set(row.file, index);
|
|
989
|
+
}
|
|
990
|
+
const filePaths = await graph.filePaths();
|
|
991
|
+
const filePathSet = new Set(filePaths);
|
|
992
|
+
const normalize = (path) => {
|
|
993
|
+
const out = [];
|
|
994
|
+
for (const part of path.split("/")) {
|
|
995
|
+
if (part === "." || part === "")
|
|
996
|
+
continue;
|
|
997
|
+
if (part === "..")
|
|
998
|
+
out.pop();
|
|
999
|
+
else
|
|
1000
|
+
out.push(part);
|
|
1001
|
+
}
|
|
1002
|
+
return out.join("/");
|
|
1003
|
+
};
|
|
1004
|
+
const EXTENSIONS = [".tsx", ".ts", "/index.tsx", "/index.ts"];
|
|
1005
|
+
const fileOfModule = (fromFile, spec) => {
|
|
1006
|
+
const base = spec.startsWith(".") ? normalize(`${fromFile.split("/").slice(0, -1).join("/")}/${spec}`) : spec.replace(/^@[\w-]*\//, "");
|
|
1007
|
+
for (const ext of EXTENSIONS)
|
|
1008
|
+
if (filePathSet.has(base + ext))
|
|
1009
|
+
return base + ext;
|
|
1010
|
+
for (const ext of EXTENSIONS) {
|
|
1011
|
+
const suffix = `/${base}${ext}`;
|
|
1012
|
+
const hit = filePaths.find((p) => p.endsWith(suffix));
|
|
1013
|
+
if (hit)
|
|
1014
|
+
return hit;
|
|
1015
|
+
}
|
|
1016
|
+
return null;
|
|
1017
|
+
};
|
|
1018
|
+
const moduleFiles = new Map;
|
|
1019
|
+
for (const { file, reg } of found) {
|
|
1020
|
+
if (!reg.component?.startsWith("module:"))
|
|
1021
|
+
continue;
|
|
1022
|
+
const key = `${file}|${reg.component}`;
|
|
1023
|
+
if (moduleFiles.has(key))
|
|
1024
|
+
continue;
|
|
1025
|
+
const path = fileOfModule(file, reg.component.slice("module:".length));
|
|
1026
|
+
if (path)
|
|
1027
|
+
moduleFiles.set(key, path);
|
|
1028
|
+
}
|
|
1029
|
+
const mainSymbol = new Map;
|
|
1030
|
+
{
|
|
1031
|
+
const rows = await graph.declaredSymbols([
|
|
1032
|
+
...new Set(moduleFiles.values())
|
|
1033
|
+
]);
|
|
1034
|
+
const byPath = new Map;
|
|
1035
|
+
for (const row of rows) {
|
|
1036
|
+
const list = byPath.get(row.file) ?? [];
|
|
1037
|
+
list.push(row);
|
|
1038
|
+
byPath.set(row.file, list);
|
|
1039
|
+
}
|
|
1040
|
+
for (const [path, symbols] of byPath) {
|
|
1041
|
+
const stem = path.split("/").pop().replace(/\.(tsx|ts)$/, "");
|
|
1042
|
+
const values = symbols.filter((s) => s.type !== "type" && s.type !== "interface");
|
|
1043
|
+
const hit = values.find((s) => s.name === stem) ?? (path.endsWith("/index.tsx") || path.endsWith("/index.ts") ? values.find((s) => s.name === path.split("/").slice(-2, -1)[0]) : undefined) ?? (values.length === 1 ? values[0] : undefined);
|
|
1044
|
+
if (hit)
|
|
1045
|
+
mainSymbol.set(path, hit.symbolId);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
const screenKey = (graphName, route) => `${workspace}|${graphName}|${route}`;
|
|
1049
|
+
const routeKey = (route) => `${workspace}|${route}`;
|
|
1050
|
+
const componentOf = new Map;
|
|
1051
|
+
for (const { graph: graphName, file, reg } of found) {
|
|
1052
|
+
const key = screenKey(graphName, reg.route);
|
|
1053
|
+
const lazy = reg.component?.startsWith("module:") ? mainSymbol.get(moduleFiles.get(`${file}|${reg.component}`) ?? "") : undefined;
|
|
1054
|
+
componentOf.set(key, lazy ?? (reg.component ? byFile.get(file)?.get(reg.component) ?? null : null));
|
|
1055
|
+
}
|
|
1056
|
+
const routeNames = new Set(found.map((f) => f.reg.route));
|
|
1057
|
+
const resolveTarget = (file, start, end, text) => {
|
|
1058
|
+
const literal = resolve(file, start, end, text);
|
|
1059
|
+
if (literal !== null && routeNames.has(literal))
|
|
1060
|
+
return literal;
|
|
1061
|
+
const viaUrl = literal !== null ? aliases.get(literal) : undefined;
|
|
1062
|
+
if (viaUrl) {
|
|
1063
|
+
bump(counters, "aliasUrl");
|
|
1064
|
+
return viaUrl;
|
|
1065
|
+
}
|
|
1066
|
+
const viaRef = resolveRef(aliases, text);
|
|
1067
|
+
if (viaRef)
|
|
1068
|
+
bump(counters, "aliasRef");
|
|
1069
|
+
return viaRef;
|
|
1070
|
+
};
|
|
1071
|
+
const classify = (file, start, end, text) => {
|
|
1072
|
+
const literal = literalOf(file, start, end, text);
|
|
1073
|
+
if (literal !== null)
|
|
1074
|
+
return routeNames.has(literal) || aliases.has(literal) || resolveRef(aliases, text) ? "resolved" : "unknown";
|
|
1075
|
+
if (resolveRef(aliases, text))
|
|
1076
|
+
return "resolved";
|
|
1077
|
+
const patterns = types?.patternIn(file, start, end);
|
|
1078
|
+
return patterns && patterns.length <= PATTERN_LIMIT ? "pattern" : "dynamic";
|
|
1079
|
+
};
|
|
1080
|
+
const callSites = [];
|
|
1081
|
+
for (const [path, src] of sources) {
|
|
1082
|
+
callSites.push(...callSitesIn(src, stripNoise(src), path, routeNames, d, resolveTarget, counters, census, classify));
|
|
1083
|
+
}
|
|
1084
|
+
const adjacency = await graph.adjacency();
|
|
1085
|
+
const closureCache = new Map;
|
|
1086
|
+
const filesReachableFrom = (symbolId) => {
|
|
1087
|
+
const cached = closureCache.get(symbolId);
|
|
1088
|
+
if (cached)
|
|
1089
|
+
return cached;
|
|
1090
|
+
const files = new Set([fileOfSymbolId(symbolId)]);
|
|
1091
|
+
const seen = new Set([symbolId]);
|
|
1092
|
+
let frontier = [symbolId];
|
|
1093
|
+
for (let depth = 0;depth < d.closureDepth && frontier.length; depth++) {
|
|
1094
|
+
const next = [];
|
|
1095
|
+
for (const node of frontier) {
|
|
1096
|
+
for (const target of adjacency.get(node) ?? []) {
|
|
1097
|
+
if (seen.has(target))
|
|
1098
|
+
continue;
|
|
1099
|
+
seen.add(target);
|
|
1100
|
+
files.add(fileOfSymbolId(target));
|
|
1101
|
+
next.push(target);
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
frontier = next;
|
|
1105
|
+
}
|
|
1106
|
+
closureCache.set(symbolId, files);
|
|
1107
|
+
return files;
|
|
1108
|
+
};
|
|
1109
|
+
const callsByFile = new Map;
|
|
1110
|
+
for (const cs of callSites) {
|
|
1111
|
+
const list = callsByFile.get(cs.file);
|
|
1112
|
+
if (list)
|
|
1113
|
+
list.push(cs);
|
|
1114
|
+
else
|
|
1115
|
+
callsByFile.set(cs.file, [cs]);
|
|
1116
|
+
}
|
|
1117
|
+
const screensByRoute = new Map;
|
|
1118
|
+
for (const { graph: graphName, reg } of found) {
|
|
1119
|
+
const key = screenKey(graphName, reg.route);
|
|
1120
|
+
const list = screensByRoute.get(reg.route);
|
|
1121
|
+
if (list)
|
|
1122
|
+
list.push(key);
|
|
1123
|
+
else
|
|
1124
|
+
screensByRoute.set(reg.route, [key]);
|
|
1125
|
+
}
|
|
1126
|
+
const graphLabels = new Set(found.map((f) => f.graph));
|
|
1127
|
+
const screens = [];
|
|
1128
|
+
const renders = [];
|
|
1129
|
+
const unlinked = [];
|
|
1130
|
+
const mounts = [];
|
|
1131
|
+
for (const { graph: graphName, file, reg } of found) {
|
|
1132
|
+
const key = screenKey(graphName, reg.route);
|
|
1133
|
+
const symbolId = componentOf.get(key) ?? null;
|
|
1134
|
+
screens.push({
|
|
1135
|
+
id: key,
|
|
1136
|
+
route: reg.route,
|
|
1137
|
+
graph: graphName,
|
|
1138
|
+
file,
|
|
1139
|
+
line: reg.line,
|
|
1140
|
+
shape: reg.shape,
|
|
1141
|
+
guard: reg.guard ?? "",
|
|
1142
|
+
hasOptions: reg.hasOptions,
|
|
1143
|
+
component: symbolId ?? ""
|
|
1144
|
+
});
|
|
1145
|
+
if (symbolId)
|
|
1146
|
+
renders.push({ screen: key, symbol: symbolId });
|
|
1147
|
+
else if (reg.component && graphLabels.has(reg.component))
|
|
1148
|
+
mounts.push(reg.route);
|
|
1149
|
+
else
|
|
1150
|
+
unlinked.push(`${reg.route} (${graphName})`);
|
|
1151
|
+
}
|
|
1152
|
+
const predicateIndex = new Map;
|
|
1153
|
+
const gates = [];
|
|
1154
|
+
for (const { graph: graphName, reg } of found) {
|
|
1155
|
+
const key = screenKey(graphName, reg.route);
|
|
1156
|
+
const screenPredicates = [];
|
|
1157
|
+
if (reg.guard)
|
|
1158
|
+
screenPredicates.push({ name: reg.guard, kind: "guard" });
|
|
1159
|
+
const symbolId = componentOf.get(key);
|
|
1160
|
+
const componentFile = symbolId ? symbolId.split("|")[2] : undefined;
|
|
1161
|
+
const componentSrc = componentFile ? sources.get(componentFile) : undefined;
|
|
1162
|
+
if (componentSrc) {
|
|
1163
|
+
for (const { kind, pattern } of d.predicates) {
|
|
1164
|
+
pattern.lastIndex = 0;
|
|
1165
|
+
for (const m of componentSrc.matchAll(pattern)) {
|
|
1166
|
+
screenPredicates.push({ name: m[1], kind });
|
|
1167
|
+
bump(counters, "predicates");
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
for (const predicate of screenPredicates) {
|
|
1172
|
+
const pkey = `${workspace}|${predicate.kind}|${predicate.name}`;
|
|
1173
|
+
predicateIndex.set(pkey, predicate);
|
|
1174
|
+
gates.push({ screen: key, predicate: pkey });
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
const navRows = [];
|
|
1178
|
+
const attributedFiles = new Set;
|
|
1179
|
+
for (const { graph: graphName, reg } of found) {
|
|
1180
|
+
const key = screenKey(graphName, reg.route);
|
|
1181
|
+
const symbolId = componentOf.get(key);
|
|
1182
|
+
if (!symbolId)
|
|
1183
|
+
continue;
|
|
1184
|
+
const componentFile = symbolId.split("|")[2];
|
|
1185
|
+
for (const file of filesReachableFrom(symbolId)) {
|
|
1186
|
+
for (const cs of callsByFile.get(file) ?? []) {
|
|
1187
|
+
if (cs.route === reg.route)
|
|
1188
|
+
continue;
|
|
1189
|
+
attributedFiles.add(file);
|
|
1190
|
+
navRows.push({
|
|
1191
|
+
screen: key,
|
|
1192
|
+
route: routeKey(cs.route),
|
|
1193
|
+
kind: cs.kind,
|
|
1194
|
+
via: cs.via,
|
|
1195
|
+
file: cs.file,
|
|
1196
|
+
line: cs.line,
|
|
1197
|
+
trigger: cs.trigger,
|
|
1198
|
+
label: cs.label,
|
|
1199
|
+
own: cs.file === componentFile,
|
|
1200
|
+
owners: 0
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
const ownedSites = new Set;
|
|
1206
|
+
for (const row of navRows)
|
|
1207
|
+
if (row.own)
|
|
1208
|
+
ownedSites.add(`${row.route}|${row.file}|${row.line}`);
|
|
1209
|
+
const edges = navRows.filter((row) => row.own || !ownedSites.has(`${row.route}|${row.file}|${row.line}`));
|
|
1210
|
+
const borrowed = navRows.length - edges.length;
|
|
1211
|
+
const siteKey = (row) => `${row.file}|${row.line}|${row.route}`;
|
|
1212
|
+
const siteOwners = new Map;
|
|
1213
|
+
for (const row of edges)
|
|
1214
|
+
siteOwners.set(siteKey(row), (siteOwners.get(siteKey(row)) ?? 0) + 1);
|
|
1215
|
+
for (const row of edges)
|
|
1216
|
+
row.owners = siteOwners.get(siteKey(row)) ?? 1;
|
|
1217
|
+
let orphanCalls = 0;
|
|
1218
|
+
for (const cs of callSites)
|
|
1219
|
+
if (!attributedFiles.has(cs.file))
|
|
1220
|
+
orphanCalls += 1;
|
|
1221
|
+
return {
|
|
1222
|
+
routes: [...screensByRoute].map(([route, keys]) => ({
|
|
1223
|
+
id: routeKey(route),
|
|
1224
|
+
name: route,
|
|
1225
|
+
registrations: keys.length
|
|
1226
|
+
})),
|
|
1227
|
+
screens,
|
|
1228
|
+
renders,
|
|
1229
|
+
predicates: [...predicateIndex].map(([id, p]) => ({
|
|
1230
|
+
id,
|
|
1231
|
+
name: p.name,
|
|
1232
|
+
kind: p.kind
|
|
1233
|
+
})),
|
|
1234
|
+
gates,
|
|
1235
|
+
edges,
|
|
1236
|
+
report: {
|
|
1237
|
+
counters,
|
|
1238
|
+
census,
|
|
1239
|
+
duplicates,
|
|
1240
|
+
unlinked,
|
|
1241
|
+
mounts,
|
|
1242
|
+
ambiguous: [...ambiguous],
|
|
1243
|
+
multi: [...screensByRoute].filter(([, ids]) => ids.length > 1).map(([route]) => route),
|
|
1244
|
+
orphanCalls,
|
|
1245
|
+
borrowed,
|
|
1246
|
+
factoryFiles: factoryFiles.length,
|
|
1247
|
+
callSites: callSites.length,
|
|
1248
|
+
rendered: renders.length,
|
|
1249
|
+
gates: gates.length,
|
|
1250
|
+
typeIndex: types ? { sourceFiles: types.sourceFiles, buildMs: types.buildMs } : null,
|
|
1251
|
+
adjacencySize: adjacency.size
|
|
1252
|
+
}
|
|
1253
|
+
};
|
|
1254
|
+
};
|
|
1255
|
+
|
|
1256
|
+
// src/graph-reader.ts
|
|
1257
|
+
var cypherReader = (db, workspace) => ({
|
|
1258
|
+
async adjacency() {
|
|
1259
|
+
const out = new Map;
|
|
1260
|
+
const rows = await db.query`
|
|
1261
|
+
MATCH (a:Symbol)-[:USES]->(b:Symbol) WHERE a.workspace = ${workspace}
|
|
1262
|
+
RETURN a.id AS s, b.id AS t`;
|
|
1263
|
+
for (const row of rows) {
|
|
1264
|
+
const list = out.get(row.s);
|
|
1265
|
+
if (list)
|
|
1266
|
+
list.push(row.t);
|
|
1267
|
+
else
|
|
1268
|
+
out.set(row.s, [row.t]);
|
|
1269
|
+
}
|
|
1270
|
+
return out;
|
|
1271
|
+
},
|
|
1272
|
+
async componentCandidates(files) {
|
|
1273
|
+
if (files.length === 0)
|
|
1274
|
+
return [];
|
|
1275
|
+
return db.query`
|
|
1276
|
+
MATCH (f:File)-[:DECLARES]->(owner:Symbol)
|
|
1277
|
+
WHERE f.workspace = ${workspace} AND f.path IN ${files}
|
|
1278
|
+
OPTIONAL MATCH (owner)-[:USES]->(used:Symbol)
|
|
1279
|
+
UNWIND [owner, used] AS c
|
|
1280
|
+
WITH f.path AS file, c
|
|
1281
|
+
WHERE c IS NOT NULL
|
|
1282
|
+
RETURN DISTINCT file, c.name AS name, c.id AS symbolId`;
|
|
1283
|
+
},
|
|
1284
|
+
async filePaths() {
|
|
1285
|
+
const rows = await db.query`
|
|
1286
|
+
MATCH (f:File) WHERE f.workspace = ${workspace} RETURN f.path AS path`;
|
|
1287
|
+
return rows.map((r) => r.path);
|
|
1288
|
+
},
|
|
1289
|
+
async declaredSymbols(files) {
|
|
1290
|
+
if (files.length === 0)
|
|
1291
|
+
return [];
|
|
1292
|
+
return db.query`
|
|
1293
|
+
MATCH (f:File)-[:DECLARES]->(s:Symbol)
|
|
1294
|
+
WHERE f.workspace = ${workspace} AND f.path IN ${files}
|
|
1295
|
+
RETURN f.path AS file, s.name AS name, s.id AS symbolId, s.type AS type`;
|
|
1296
|
+
}
|
|
1297
|
+
});
|
|
1298
|
+
|
|
1299
|
+
// src/payload.ts
|
|
1300
|
+
var OPEN = { tags: [], description: null, data: {} };
|
|
1301
|
+
var toPayload = (model) => {
|
|
1302
|
+
const routeOf = new Map(model.routes.map((r) => [r.name, r.id]));
|
|
1303
|
+
const nodes = [
|
|
1304
|
+
...model.routes.map((r) => ({
|
|
1305
|
+
_label: "Route",
|
|
1306
|
+
id: r.id,
|
|
1307
|
+
name: r.name,
|
|
1308
|
+
registrations: r.registrations,
|
|
1309
|
+
...OPEN
|
|
1310
|
+
})),
|
|
1311
|
+
...model.screens.map((s) => ({
|
|
1312
|
+
_label: "Screen",
|
|
1313
|
+
id: s.id,
|
|
1314
|
+
route: s.route,
|
|
1315
|
+
graph: s.graph,
|
|
1316
|
+
file: s.file,
|
|
1317
|
+
line: s.line,
|
|
1318
|
+
shape: s.shape,
|
|
1319
|
+
guard: s.guard,
|
|
1320
|
+
hasOptions: s.hasOptions,
|
|
1321
|
+
component: s.component,
|
|
1322
|
+
...OPEN
|
|
1323
|
+
})),
|
|
1324
|
+
...model.predicates.map((p) => ({
|
|
1325
|
+
_label: "Predicate",
|
|
1326
|
+
id: p.id,
|
|
1327
|
+
name: p.name,
|
|
1328
|
+
kind: p.kind,
|
|
1329
|
+
...OPEN
|
|
1330
|
+
}))
|
|
1331
|
+
];
|
|
1332
|
+
const edges = [
|
|
1333
|
+
...model.screens.map((s) => ({
|
|
1334
|
+
_label: "SERVES",
|
|
1335
|
+
from: "Route",
|
|
1336
|
+
to: "Screen",
|
|
1337
|
+
fromId: routeOf.get(s.route) ?? "",
|
|
1338
|
+
toId: s.id,
|
|
1339
|
+
...OPEN
|
|
1340
|
+
})),
|
|
1341
|
+
...model.renders.map((r) => ({
|
|
1342
|
+
_label: "RENDERS",
|
|
1343
|
+
from: "Screen",
|
|
1344
|
+
to: "Symbol",
|
|
1345
|
+
fromId: r.screen,
|
|
1346
|
+
toId: r.symbol,
|
|
1347
|
+
...OPEN
|
|
1348
|
+
})),
|
|
1349
|
+
...model.gates.map((g) => ({
|
|
1350
|
+
_label: "GATED_BY",
|
|
1351
|
+
from: "Screen",
|
|
1352
|
+
to: "Predicate",
|
|
1353
|
+
fromId: g.screen,
|
|
1354
|
+
toId: g.predicate,
|
|
1355
|
+
...OPEN
|
|
1356
|
+
})),
|
|
1357
|
+
...model.edges.map((e) => ({
|
|
1358
|
+
_label: "NAVIGATES_TO",
|
|
1359
|
+
from: "Screen",
|
|
1360
|
+
to: "Route",
|
|
1361
|
+
fromId: e.screen,
|
|
1362
|
+
toId: e.route,
|
|
1363
|
+
kind: e.kind,
|
|
1364
|
+
via: e.via,
|
|
1365
|
+
file: e.file,
|
|
1366
|
+
line: e.line,
|
|
1367
|
+
trigger: e.trigger,
|
|
1368
|
+
label: e.label,
|
|
1369
|
+
own: e.own,
|
|
1370
|
+
owners: e.owners,
|
|
1371
|
+
...OPEN
|
|
1372
|
+
}))
|
|
1373
|
+
];
|
|
1374
|
+
return { nodes, edges };
|
|
1375
|
+
};
|
|
1376
|
+
|
|
1377
|
+
// src/react-navigation.contributor.ts
|
|
1378
|
+
var AXES = [
|
|
1379
|
+
"core",
|
|
1380
|
+
"helperCalls",
|
|
1381
|
+
"configKeys",
|
|
1382
|
+
"predicates",
|
|
1383
|
+
"types",
|
|
1384
|
+
"tables",
|
|
1385
|
+
"registries",
|
|
1386
|
+
"routeAliasFiles",
|
|
1387
|
+
"aliasUrl",
|
|
1388
|
+
"aliasRef"
|
|
1389
|
+
];
|
|
1390
|
+
var logReport = (report) => {
|
|
1391
|
+
const { counters, census } = report;
|
|
1392
|
+
if (report.typeIndex)
|
|
1393
|
+
console.log(`[react-navigation] index de types : ${report.typeIndex.sourceFiles} fichiers en ${report.typeIndex.buildMs} ms`);
|
|
1394
|
+
console.log(`[react-navigation] ${report.adjacencySize} symboles sources dans l'adjacence`);
|
|
1395
|
+
console.log(`[react-navigation] dialecte : ${AXES.map((a) => `${a} ${counters[a] ?? 0}`).join(" \xB7 ")}`);
|
|
1396
|
+
const pct = (n, total) => total === 0 ? "\u2014" : `${Math.round(n / total * 100)} %`;
|
|
1397
|
+
const missedCalls = census.callsSeen - census.callsResolved;
|
|
1398
|
+
console.log(`[react-navigation] couverture des appels \`navigate\` : ` + `${census.callsResolved}/${census.callsSeen} (${pct(census.callsResolved, census.callsSeen)})`);
|
|
1399
|
+
if (missedCalls > 0) {
|
|
1400
|
+
const opaque = missedCalls - census.patternedCount;
|
|
1401
|
+
console.log(` ${missedCalls} sans ar\xEAte : ${census.unknown.length > 0 ? `${census.unknown.length}+ hors registre \xB7 ` : ""}` + `${census.patternedCount} avec motif d'URL \xB7 ${opaque} opaques`);
|
|
1402
|
+
if (census.unknown.length > 0) {
|
|
1403
|
+
console.log(` cible lisible mais absente du registre \u2014 inscription probablement rat\xE9e :`);
|
|
1404
|
+
for (const line of census.unknown)
|
|
1405
|
+
console.log(` ${line}`);
|
|
1406
|
+
}
|
|
1407
|
+
if (census.patterned.length > 0) {
|
|
1408
|
+
console.log(` sans litt\xE9ral mais avec motif d'URL \u2014 joignable par la config linking :`);
|
|
1409
|
+
for (const line of census.patterned)
|
|
1410
|
+
console.log(` ${line}`);
|
|
1411
|
+
}
|
|
1412
|
+
if (census.dynamic.length > 0) {
|
|
1413
|
+
console.log(` opaque \u2014 irr\xE9ductible :`);
|
|
1414
|
+
for (const line of census.dynamic)
|
|
1415
|
+
console.log(` ${line}`);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1418
|
+
console.log(`[react-navigation] balises <*.Screen> : ${census.jsxTags} vues, ` + `${census.jsxTags - census.jsxMissed.length} exploit\xE9es`);
|
|
1419
|
+
for (const line of census.jsxMissed)
|
|
1420
|
+
console.log(` nom non r\xE9solu, \xE0 v\xE9rifier : ${line}`);
|
|
1421
|
+
};
|
|
1422
|
+
var silentAxes = (report, d) => AXES.filter((axis) => axis !== "core" && d.configured.has(axis) && (report.counters[axis] ?? 0) === 0);
|
|
1423
|
+
var diagnosticsOf = (report, blind, silent) => {
|
|
1424
|
+
const { census } = report;
|
|
1425
|
+
return [
|
|
1426
|
+
...blind ? [
|
|
1427
|
+
{
|
|
1428
|
+
severity: "error",
|
|
1429
|
+
message: `aucun enregistrement trouv\xE9 alors que ${report.factoryFiles} fichier(s) d\xE9clarent un navigateur \u2014 ` + `le dialecte ne correspond pas \xE0 ce projet`
|
|
1430
|
+
}
|
|
1431
|
+
] : [],
|
|
1432
|
+
...census.callsSeen > 0 && census.callsResolved < census.callsSeen ? [
|
|
1433
|
+
{
|
|
1434
|
+
severity: "info",
|
|
1435
|
+
message: `${census.callsSeen - census.callsResolved} appel(s) \`navigate\` sur ${census.callsSeen} sans ar\xEAte ` + `(${census.unknown.length > 0 ? `dont des cibles hors registre, ex. ${census.unknown[0]}` : "cibles calcul\xE9es"})`
|
|
1436
|
+
}
|
|
1437
|
+
] : [],
|
|
1438
|
+
...silent.length > 0 ? [
|
|
1439
|
+
{
|
|
1440
|
+
severity: "warning",
|
|
1441
|
+
message: `axe(s) de dialecte sans aucune correspondance : ${silent.join(", ")} \u2014 graphe probablement incomplet`
|
|
1442
|
+
}
|
|
1443
|
+
] : [],
|
|
1444
|
+
...report.duplicates.length ? [
|
|
1445
|
+
{
|
|
1446
|
+
severity: "info",
|
|
1447
|
+
message: `${report.duplicates.length} enregistrement(s) en double, premier retenu : ${report.duplicates.slice(0, 5).join(", ")}`
|
|
1448
|
+
}
|
|
1449
|
+
] : [],
|
|
1450
|
+
...report.unlinked.length ? [
|
|
1451
|
+
{
|
|
1452
|
+
severity: "warning",
|
|
1453
|
+
message: `${report.unlinked.length} screen(s) with no component: ${report.unlinked.slice(0, 5).join(", ")}`
|
|
1454
|
+
}
|
|
1455
|
+
] : [],
|
|
1456
|
+
...report.multi.length ? [
|
|
1457
|
+
{
|
|
1458
|
+
severity: "info",
|
|
1459
|
+
message: `${report.multi.length} route(s) registered more than once: ${report.multi.join(", ")}`
|
|
1460
|
+
}
|
|
1461
|
+
] : [],
|
|
1462
|
+
...report.ambiguous.length ? [
|
|
1463
|
+
{
|
|
1464
|
+
severity: "warning",
|
|
1465
|
+
message: `${report.ambiguous.length} nom(s) de composant ambigu(s) dans un fichier de navigateur : ${report.ambiguous.slice(0, 3).join(", ")}`
|
|
1466
|
+
}
|
|
1467
|
+
] : [],
|
|
1468
|
+
...report.orphanCalls ? [
|
|
1469
|
+
{
|
|
1470
|
+
severity: "info",
|
|
1471
|
+
message: `${report.orphanCalls} navigation call site(s) reachable from no screen`
|
|
1472
|
+
}
|
|
1473
|
+
] : []
|
|
1474
|
+
];
|
|
1475
|
+
};
|
|
1476
|
+
var createReactNavigationContributor = (d) => ({
|
|
1477
|
+
id: "react-navigation",
|
|
1478
|
+
region: regionNavigation,
|
|
1479
|
+
source: "sourced",
|
|
1480
|
+
async produce(ctx) {
|
|
1481
|
+
const [symbols] = await ctx.db.query`
|
|
1482
|
+
MATCH (s:Symbol) WHERE s.workspace = ${ctx.workspace} RETURN count(*) AS n`;
|
|
1483
|
+
if ((symbols?.n ?? 0) === 0) {
|
|
1484
|
+
return {
|
|
1485
|
+
status: "error",
|
|
1486
|
+
diagnostics: [
|
|
1487
|
+
{
|
|
1488
|
+
severity: "error",
|
|
1489
|
+
message: `no Symbol in workspace "${ctx.workspace}" \u2014 put this contributor after the code indexer`
|
|
1490
|
+
}
|
|
1491
|
+
]
|
|
1492
|
+
};
|
|
1493
|
+
}
|
|
1494
|
+
console.time("[react-navigation] extraction");
|
|
1495
|
+
const model = await collect({
|
|
1496
|
+
source: ctx.source,
|
|
1497
|
+
projectRoot: ctx.root,
|
|
1498
|
+
graph: cypherReader(ctx.db, ctx.workspace),
|
|
1499
|
+
workspace: ctx.workspace
|
|
1500
|
+
}, d);
|
|
1501
|
+
console.timeEnd("[react-navigation] extraction");
|
|
1502
|
+
const { report } = model;
|
|
1503
|
+
logReport(report);
|
|
1504
|
+
const blind = model.screens.length === 0 && report.factoryFiles > 0;
|
|
1505
|
+
if (blind)
|
|
1506
|
+
console.log(`[react-navigation] AUCUN enregistrement, alors que ${report.factoryFiles} fichier(s) ` + `d\xE9clarent un navigateur \u2014 le dialecte ne correspond pas \xE0 ce projet`);
|
|
1507
|
+
console.log(`[react-navigation] ${model.routes.length} routes / ${model.screens.length} screens / ` + `${report.rendered} rendered / ${report.mounts.length} mounts / ${report.gates} gates / ` + `${report.callSites} call sites -> ${model.edges.length} NAVIGATES_TO edges ` + `(${report.borrowed} sites emprunt\xE9s retir\xE9s)`);
|
|
1508
|
+
const silent = silentAxes(report, d);
|
|
1509
|
+
return {
|
|
1510
|
+
status: blind || silent.length > 0 ? "partial" : "ok",
|
|
1511
|
+
diagnostics: diagnosticsOf(report, blind, silent),
|
|
1512
|
+
payload: toPayload(model)
|
|
1513
|
+
};
|
|
1514
|
+
}
|
|
1515
|
+
});
|
|
1516
|
+
var reactNavigationContributor = (dialect = {}) => {
|
|
1517
|
+
const d = resolveDialect(dialect);
|
|
1518
|
+
return createReactNavigationContributor(d);
|
|
1519
|
+
};
|
|
1520
|
+
export {
|
|
1521
|
+
reactNavigationContributor
|
|
1522
|
+
};
|