feat(expo-router): add Expo Router support for Screens and navigations and introduce Steps API
Introduce Expo Router integration with a new Screens view and API to surface screens and transitions, plus a new Steps API and UI to depict typed steps from anchors or symbols. Extend codegraph’s extraction and resolution to handle namespace objects (export default NAME, two-statement forms, and default bindings) and React hook bindings for handlers, improving accuracy of flows across JS ↔ native boundaries. Add Swift/React Native bridge receiver evidence (RCT_EXTERN_MODULE, RCT_EXTERN_METHOD) and related resolution logic, with tests covering namespace-object resolution, useCallback-driven handlers, and inline RN event listeners. Update UI to include a Steps tab and associated components (StepsView, StepNode, ScreenEdge) and wire navigation to expose steps-based exploration via /api/steps and UI routes. Documentation and changelog reflect the new Expo Router integration and steps surface capabilities.
This commit is contained in:
@@ -36,6 +36,7 @@ import type {
|
||||
WireFlowPayload,
|
||||
WireMapPayload,
|
||||
WireScreensPayload,
|
||||
WireStepsPayload,
|
||||
WireNodeRefs,
|
||||
WireRoutes,
|
||||
WireSearch,
|
||||
@@ -178,6 +179,16 @@ export interface LiveHandlers {
|
||||
error(): void;
|
||||
}
|
||||
|
||||
/** What happens from an anchor: by id, or by name (the first screen-like match). */
|
||||
export interface StepsRequest {
|
||||
anchor?: string;
|
||||
symbol?: string;
|
||||
depth?: number;
|
||||
limit?: number;
|
||||
/** Enter the screens the walk reaches, instead of drawing them as boundaries. */
|
||||
through?: boolean;
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------- adapter -- */
|
||||
|
||||
/**
|
||||
@@ -213,6 +224,11 @@ export interface GraphAdapter {
|
||||
map(request?: MapRequest, signal?: AbortSignal): Promise<WireMapPayload>;
|
||||
/** The app's screens and the transitions between them, with their conditions. */
|
||||
screens(signal?: AbortSignal): Promise<WireScreensPayload>;
|
||||
/**
|
||||
* What happens from a screen or a symbol, as typed steps. Optional: a host
|
||||
* that has not wired it renders the Steps view as absent-and-explained.
|
||||
*/
|
||||
steps?(request: StepsRequest, signal?: AbortSignal): Promise<WireStepsPayload>;
|
||||
/** The URL → handler map. */
|
||||
routes(request?: RoutesRequest, signal?: AbortSignal): Promise<WireRoutes>;
|
||||
/** Where a reader starts: routes, files that run something, tests, hubs. */
|
||||
@@ -400,6 +416,16 @@ export function createHttpAdapter(options: HttpAdapterOptions = {}): GraphAdapte
|
||||
return getJson<WireScreensPayload>('api/screens', signal);
|
||||
},
|
||||
|
||||
steps(request = {}, signal) {
|
||||
const params = new URLSearchParams();
|
||||
if (request.anchor) params.set('anchor', request.anchor);
|
||||
else if (request.symbol) params.set('symbol', request.symbol);
|
||||
if (request.depth) params.set('depth', String(request.depth));
|
||||
if (request.limit) params.set('limit', String(request.limit));
|
||||
if (request.through) params.set('through', '1');
|
||||
return getJson<WireStepsPayload>(`api/steps${query(params)}`, signal);
|
||||
},
|
||||
|
||||
entryPoints(request = {}, signal) {
|
||||
const params = new URLSearchParams();
|
||||
if (request.limit) params.set('limit', String(request.limit));
|
||||
|
||||
+21
-1
@@ -20,6 +20,7 @@ import type {
|
||||
WireFlowPayload,
|
||||
WireMapPayload,
|
||||
WireScreensPayload,
|
||||
WireStepsPayload,
|
||||
WireNodeRefs,
|
||||
WireRoutes,
|
||||
WireSearch,
|
||||
@@ -28,7 +29,7 @@ import type {
|
||||
WireSymbolPayload,
|
||||
WireTrails,
|
||||
} from './wire';
|
||||
import type { SaveTrailRequest } from './adapter';
|
||||
import type { SaveTrailRequest, StepsRequest } from './adapter';
|
||||
|
||||
export * from './wire';
|
||||
export { ApiFailure } from './adapter';
|
||||
@@ -44,6 +45,7 @@ export type {
|
||||
SaveTrailRequest,
|
||||
SearchRequest,
|
||||
SourceRequest,
|
||||
StepsRequest,
|
||||
} from './adapter';
|
||||
|
||||
export function fetchStats(signal?: AbortSignal): Promise<WireStats> {
|
||||
@@ -146,6 +148,24 @@ export function fetchScreens(signal?: AbortSignal): Promise<WireScreensPayload>
|
||||
return getGraphAdapter().screens(signal);
|
||||
}
|
||||
|
||||
/**
|
||||
* What happens from an anchor — a screen, a handler, any symbol — as typed
|
||||
* steps with the conditions between them. Refused, not thrown at random, by
|
||||
* an adapter that never offered it (see {@link canDrawSteps}).
|
||||
*/
|
||||
export function fetchSteps(request: StepsRequest, signal?: AbortSignal): Promise<WireStepsPayload> {
|
||||
const adapter = getGraphAdapter();
|
||||
if (typeof adapter.steps !== 'function') {
|
||||
return Promise.reject(new ApiFailure(0, 'refused', 'This viewer cannot draw steps.', null));
|
||||
}
|
||||
return adapter.steps(request, signal);
|
||||
}
|
||||
|
||||
/** Whether the installed adapter can answer {@link fetchSteps} at all. */
|
||||
export function canDrawSteps(): boolean {
|
||||
return typeof getGraphAdapter().steps === 'function';
|
||||
}
|
||||
|
||||
export function fetchMap(
|
||||
opts: { root?: string | null; depth?: number } = {},
|
||||
signal?: AbortSignal
|
||||
|
||||
@@ -55,6 +55,16 @@ export interface FlowHrefOptions {
|
||||
trail?: string;
|
||||
}
|
||||
|
||||
export interface StepsHrefOptions {
|
||||
/** A node id — a screen's route, a handler, any symbol. */
|
||||
anchor?: string;
|
||||
/** A name, when no id is at hand; the answering side picks the most screen-like match. */
|
||||
symbol?: string;
|
||||
depth?: number;
|
||||
/** Enter the screens the walk reaches, instead of drawing them as boundaries. */
|
||||
through?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Where the components send the reader.
|
||||
*
|
||||
@@ -69,6 +79,7 @@ export interface NavigationDriver {
|
||||
flowHref(opts?: FlowHrefOptions): string;
|
||||
entryHref(): string;
|
||||
screensHref(): string;
|
||||
stepsHref(opts?: StepsHrefOptions): string;
|
||||
deadHref(opts?: DeadCodeHrefOptions): string;
|
||||
/** Go to an href this driver built. */
|
||||
navigate(href: string, opts?: { replace?: boolean }): void;
|
||||
@@ -138,6 +149,15 @@ export const hashNavigation: NavigationDriver = {
|
||||
return '#/screens';
|
||||
},
|
||||
|
||||
stepsHref(opts = {}) {
|
||||
const params = new URLSearchParams();
|
||||
if (opts.anchor) params.set('anchor', opts.anchor);
|
||||
else if (opts.symbol) params.set('symbol', opts.symbol);
|
||||
if (opts.depth) params.set('depth', String(opts.depth));
|
||||
if (opts.through) params.set('through', '1');
|
||||
return `#/steps${query(params)}`;
|
||||
},
|
||||
|
||||
deadHref(opts = {}) {
|
||||
const params = new URLSearchParams();
|
||||
if (opts.exported) params.set('exported', '1');
|
||||
@@ -221,6 +241,10 @@ export function screensHref(): string {
|
||||
return driver.screensHref();
|
||||
}
|
||||
|
||||
export function stepsHref(opts: StepsHrefOptions = {}): string {
|
||||
return driver.stepsHref(opts);
|
||||
}
|
||||
|
||||
export function deadHref(opts: DeadCodeHrefOptions = {}): string {
|
||||
return driver.deadHref(opts);
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
* #/flow flow strip (?from=&to= | ?symbols= | ?t=<trail>)
|
||||
* #/entry entry points (where a flow starts)
|
||||
* #/screens screens (the app's screens and transitions)
|
||||
* #/steps steps (?anchor=<id> | ?symbol=<name>: what happens from there)
|
||||
* #/dead dead code (?exported=1 widens the claim)
|
||||
*
|
||||
* Node ids are opaque engine strings shaped `<kind>:<hash>` or
|
||||
@@ -43,6 +44,7 @@ export {
|
||||
navigate,
|
||||
screensHref,
|
||||
setNavigationDriver,
|
||||
stepsHref,
|
||||
symbolHref,
|
||||
} from './navigation';
|
||||
export type {
|
||||
@@ -51,6 +53,7 @@ export type {
|
||||
FlowHrefOptions,
|
||||
MapHrefOptions,
|
||||
NavigationDriver,
|
||||
StepsHrefOptions,
|
||||
SymbolHrefOptions,
|
||||
} from './navigation';
|
||||
|
||||
@@ -77,6 +80,14 @@ export type Route =
|
||||
}
|
||||
| { view: 'entry' }
|
||||
| { view: 'screens' }
|
||||
| {
|
||||
view: 'steps';
|
||||
/** The anchor by id; null with `symbol` set, or on the bare tab. */
|
||||
anchor: string | null;
|
||||
symbol: string | null;
|
||||
depth: number | null;
|
||||
through: boolean;
|
||||
}
|
||||
| {
|
||||
view: 'dead';
|
||||
/** Symbols reachable from outside the index are on the list. */
|
||||
@@ -141,6 +152,17 @@ export function parseHash(hash: string): RouterLocation {
|
||||
route = { view: 'entry' };
|
||||
} else if (head === 'screens' && rest.length === 0) {
|
||||
route = { view: 'screens' };
|
||||
} else if (head === 'steps' && rest.length === 0) {
|
||||
// The anchor travels in the URL, so "what happens on the review screen"
|
||||
// is a link that reopens as the same picture.
|
||||
const depth = Number.parseInt(params.get('depth') ?? '', 10);
|
||||
route = {
|
||||
view: 'steps',
|
||||
anchor: params.get('anchor'),
|
||||
symbol: params.get('symbol'),
|
||||
depth: Number.isFinite(depth) && depth >= 1 && depth <= 14 ? depth : null,
|
||||
through: params.get('through') === '1',
|
||||
};
|
||||
} else if (head === 'dead' && rest.length === 0) {
|
||||
// The widening travels in the URL like the map's shape does: a link to
|
||||
// "including exported symbols" has to reopen the same list.
|
||||
|
||||
@@ -123,6 +123,19 @@ export interface Point {
|
||||
y: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* What the label placement and the pointer need from a picture: the Screens
|
||||
* view's model, or any other drawn with its machinery (the Steps view draws
|
||||
* typed steps with the same layout, curves, pills and hit-testing).
|
||||
*/
|
||||
export interface Picture {
|
||||
layout: MapLayout;
|
||||
layerGap: number;
|
||||
edges: Map<string, { label: string }>;
|
||||
curves: Map<string, Curve>;
|
||||
polylines: Map<string, Point[]>;
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------- layering -- */
|
||||
|
||||
/**
|
||||
@@ -286,7 +299,7 @@ export function clauses(when: string): string[] {
|
||||
* …` on both arms of a fork); the last clause is the one that tells the two
|
||||
* apart, and the full text is a hover away.
|
||||
*/
|
||||
export function edgeLabel(links: readonly WireScreenLink[]): string {
|
||||
export function edgeLabel(links: ReadonlyArray<{ when: string }>): string {
|
||||
if (links.length === 1) {
|
||||
const when = links[0]!.when;
|
||||
if (!when) return '';
|
||||
@@ -630,7 +643,7 @@ export interface EdgeHit {
|
||||
* the smaller id, so two visits agree.
|
||||
*/
|
||||
export function nearestEdge(
|
||||
model: ScreensModel,
|
||||
model: Picture,
|
||||
point: Point,
|
||||
among: ReadonlySet<string> | null,
|
||||
reach: number
|
||||
@@ -713,7 +726,7 @@ export function laneCount(layerGap: number): number {
|
||||
* the selected screen — `→` leaving it, `←` arriving — and the edge's label.
|
||||
* Empty when the edge has nothing to say (a single, unconditional transition).
|
||||
*/
|
||||
export function pillText(info: ScreenEdgeInfo, edge: MapEdgeLayout, selected: string | null): string {
|
||||
export function pillText(info: { label: string }, edge: MapEdgeLayout, selected: string | null): string {
|
||||
if (!info.label) return '';
|
||||
const arriving = selected !== null && edge.target === selected && edge.source !== selected;
|
||||
return `${arriving ? '←' : '→'} ${info.label}`;
|
||||
@@ -737,7 +750,7 @@ function intersects(a: Rect, b: Rect, gapX: number): boolean {
|
||||
* lane is free — or when `lanes` is 1 and that lane is taken.
|
||||
*/
|
||||
function layPill(
|
||||
model: ScreensModel,
|
||||
model: Picture,
|
||||
edge: MapEdgeLayout,
|
||||
end: 'source' | 'target',
|
||||
text: string,
|
||||
@@ -781,7 +794,7 @@ function layPill(
|
||||
* pill: the pill for a hovered edge that is not the selected screen's is
|
||||
* placed separately by {@link hoverPill}.
|
||||
*/
|
||||
export function placeLabels(model: ScreensModel, selected: string | null): PillLayout {
|
||||
export function placeLabels(model: Picture, selected: string | null): PillLayout {
|
||||
const pills = new Map<string, PillPlacement>();
|
||||
if (selected === null) return { pills, hidden: 0 };
|
||||
const nodes = new Map(model.layout.nodes.map((n) => [n.id, n]));
|
||||
@@ -827,7 +840,7 @@ export function placeLabels(model: ScreensModel, selected: string | null): PillL
|
||||
* with the whole condition, not the connector's short label.
|
||||
*/
|
||||
export function hoverPill(
|
||||
model: ScreensModel,
|
||||
model: Picture,
|
||||
edgeId: string,
|
||||
selected: string | null,
|
||||
text?: string,
|
||||
|
||||
@@ -0,0 +1,238 @@
|
||||
/**
|
||||
* The Steps view's model — what happens from an anchor, as typed steps laid
|
||||
* out so that a step sits above the steps it sets in motion.
|
||||
*
|
||||
* Everything geometric is the Screens view's (`screens-model.ts`): the Map's
|
||||
* layout with directional ports, a curve per edge on a track of its own, the
|
||||
* pills that label a selected step's links at the far end of each line, and
|
||||
* the nearest-line pointer. What is this file's own is small: the row a step
|
||||
* sits on is its distance from the anchor, which the server already counted
|
||||
* (`WireStep.depth`), so the layering is a lookup rather than a search; the
|
||||
* words in a box come from the step's kind; and the side panel's two lists
|
||||
* are the links into and out of the selected step.
|
||||
*/
|
||||
|
||||
import type { WireMapLink, WireMapModule, WireStep, WireStepLink, WireStepsPayload } from './wire';
|
||||
import { buildMapLayout, linkId, PORT_PITCH, type MapLayout } from './map-model';
|
||||
import {
|
||||
edgeLabel,
|
||||
samplePolyline,
|
||||
trackedCurves,
|
||||
SCREEN_LAYER_GAP,
|
||||
type Curve,
|
||||
type Picture,
|
||||
type Point,
|
||||
} from './screens-model';
|
||||
|
||||
export interface StepNodeInfo {
|
||||
id: string;
|
||||
step: WireStep;
|
||||
/** What the box prints on its first line. */
|
||||
label: string;
|
||||
/** …and on its second. */
|
||||
sub: string;
|
||||
}
|
||||
|
||||
export interface StepEdgeInfo {
|
||||
id: string;
|
||||
from: string;
|
||||
to: string;
|
||||
/** Every link between the pair — one connector, several stories. */
|
||||
links: WireStepLink[];
|
||||
/** The connector's short label: the innermost condition, or how many links. */
|
||||
label: string;
|
||||
/** Every link behind it was synthesized (a dynamic-dispatch bridge). */
|
||||
synthesized: boolean;
|
||||
/** The kind the links agree on, or `calls` when they differ. */
|
||||
kind: WireStepLink['kind'];
|
||||
}
|
||||
|
||||
export interface StepsModel extends Picture {
|
||||
layout: MapLayout;
|
||||
nodes: Map<string, StepNodeInfo>;
|
||||
edges: Map<string, StepEdgeInfo>;
|
||||
layerGap: number;
|
||||
curves: Map<string, Curve>;
|
||||
polylines: Map<string, Point[]>;
|
||||
/** Steps per kind, for the panel's summary. */
|
||||
counts: Record<WireStep['kind'], number>;
|
||||
}
|
||||
|
||||
/** Points a curve is sampled at for hit-testing (as the Screens view's). */
|
||||
const HIT_SAMPLES = 24;
|
||||
|
||||
/* ---------------------------------------------------------------- words -- */
|
||||
|
||||
/** A short word for a step's kind, as the panel and the legend say it. */
|
||||
export function kindWord(kind: WireStep['kind']): string {
|
||||
switch (kind) {
|
||||
case 'screen':
|
||||
return 'screen';
|
||||
case 'trigger':
|
||||
return 'handler';
|
||||
case 'bridge':
|
||||
return 'native call';
|
||||
case 'event':
|
||||
return 'native event';
|
||||
case 'store':
|
||||
return 'store action';
|
||||
case 'effect':
|
||||
return 'outside the index';
|
||||
default:
|
||||
return 'start';
|
||||
}
|
||||
}
|
||||
|
||||
/** The first line of a step's box. Boundary crossings carry an arrow for which way the code goes. */
|
||||
export function stepLabel(step: WireStep): string {
|
||||
switch (step.kind) {
|
||||
case 'bridge':
|
||||
return `⇢ ${step.label}`;
|
||||
case 'event': {
|
||||
const events = step.events ?? (step.event ? [step.event] : []);
|
||||
if (events.length === 0) return `⇠ ${step.label}`;
|
||||
return events.length === 1 ? `⇠ ${events[0]}` : `⇠ ${events[0]} +${events.length - 1}`;
|
||||
}
|
||||
default:
|
||||
return step.label;
|
||||
}
|
||||
}
|
||||
|
||||
/** The second line: what the step is, then where it is. */
|
||||
export function stepSub(step: WireStep): string {
|
||||
const file = step.node ? step.node.file.slice(step.node.file.lastIndexOf('/') + 1) : '';
|
||||
switch (step.kind) {
|
||||
case 'screen':
|
||||
return step.sub;
|
||||
case 'trigger':
|
||||
return `handler · ${file}`;
|
||||
case 'bridge':
|
||||
return `native · ${file}`;
|
||||
case 'event':
|
||||
return `${step.label} · ${file}`;
|
||||
case 'store':
|
||||
return `store · ${file}`;
|
||||
case 'effect':
|
||||
return step.sub;
|
||||
default:
|
||||
return step.sub;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------------------------------------------------------------- build -- */
|
||||
|
||||
export function buildStepsModel(payload: WireStepsPayload): StepsModel {
|
||||
const nodes = new Map<string, StepNodeInfo>();
|
||||
const modules: WireMapModule[] = [];
|
||||
const counts: Record<WireStep['kind'], number> = {
|
||||
anchor: 0,
|
||||
screen: 0,
|
||||
trigger: 0,
|
||||
bridge: 0,
|
||||
event: 0,
|
||||
store: 0,
|
||||
effect: 0,
|
||||
};
|
||||
const degree = new Map<string, number>();
|
||||
for (const link of payload.links) {
|
||||
degree.set(link.from, (degree.get(link.from) ?? 0) + 1);
|
||||
degree.set(link.to, (degree.get(link.to) ?? 0) + 1);
|
||||
}
|
||||
for (const step of payload.steps) {
|
||||
counts[step.kind]++;
|
||||
const info: StepNodeInfo = { id: step.id, step, label: stepLabel(step), sub: stepSub(step) };
|
||||
nodes.set(step.id, info);
|
||||
modules.push({
|
||||
id: step.id,
|
||||
label: info.label,
|
||||
files: 1,
|
||||
symbols: degree.get(step.id) ?? 0,
|
||||
languages: [],
|
||||
test: false,
|
||||
generated: 0,
|
||||
generatedFiles: [],
|
||||
facade: false,
|
||||
fileList: { total: 1, shown: 1, truncated: false, items: [step.node?.file ?? step.sub] },
|
||||
});
|
||||
}
|
||||
|
||||
// One layout link per (from, to); the links behind it stay listed.
|
||||
const byPair = new Map<string, WireStepLink[]>();
|
||||
for (const link of payload.links) {
|
||||
if (!nodes.has(link.from) || !nodes.has(link.to) || link.from === link.to) continue;
|
||||
const key = linkId({ source: link.from, target: link.to });
|
||||
const list = byPair.get(key) ?? [];
|
||||
list.push(link);
|
||||
byPair.set(key, list);
|
||||
}
|
||||
const links: WireMapLink[] = [];
|
||||
const edges = new Map<string, StepEdgeInfo>();
|
||||
for (const [key, group] of byPair) {
|
||||
const first = group[0]!;
|
||||
links.push({
|
||||
source: first.from,
|
||||
target: first.to,
|
||||
count: group.length,
|
||||
declared: group.length,
|
||||
byKind: [{ kind: 'calls', count: group.length }],
|
||||
topPairs: [],
|
||||
});
|
||||
edges.set(key, {
|
||||
id: key,
|
||||
from: first.from,
|
||||
to: first.to,
|
||||
links: group,
|
||||
label: edgeLabel(group),
|
||||
synthesized: group.every((l) => l.synthesized),
|
||||
kind: group.every((l) => l.kind === first.kind) ? first.kind : 'calls',
|
||||
});
|
||||
}
|
||||
|
||||
// Layer = distance from the anchor, counted by the server. Layer 0 is the
|
||||
// bottom, so the deepest row is 0 and the anchor is on top.
|
||||
const depthOf = new Map(payload.steps.map((s) => [s.id, s.depth]));
|
||||
const deepest = Math.max(0, ...payload.steps.map((s) => s.depth));
|
||||
const layering = (ids: string[]): Map<string, number> =>
|
||||
new Map(ids.map((id) => [id, deepest - (depthOf.get(id) ?? deepest)]));
|
||||
|
||||
const layout = buildMapLayout(
|
||||
{ modules, links },
|
||||
{
|
||||
includeTests: true,
|
||||
minWeight: 0,
|
||||
sizing: (m) => {
|
||||
const info = nodes.get(m.id);
|
||||
return { label: info?.label ?? m.id, meta: info?.sub ?? '' };
|
||||
},
|
||||
layering,
|
||||
layerGap: SCREEN_LAYER_GAP,
|
||||
portPitch: PORT_PITCH,
|
||||
ports: 'directional',
|
||||
}
|
||||
);
|
||||
const curves = trackedCurves(layout, SCREEN_LAYER_GAP);
|
||||
const polylines = new Map<string, Point[]>();
|
||||
for (const [id, curve] of curves) polylines.set(id, samplePolyline(curve, HIT_SAMPLES));
|
||||
return { layout, nodes, edges, layerGap: SCREEN_LAYER_GAP, curves, polylines, counts };
|
||||
}
|
||||
|
||||
/** The side panel's two lists for a selected step. */
|
||||
export function stepNeighbourhood(
|
||||
payload: WireStepsPayload,
|
||||
id: string
|
||||
): { arrivesFrom: WireStepLink[]; leadsTo: WireStepLink[] } {
|
||||
return {
|
||||
arrivesFrom: payload.links.filter((l) => l.to === id),
|
||||
leadsTo: payload.links.filter((l) => l.from === id),
|
||||
};
|
||||
}
|
||||
|
||||
/** `useReviewHandlers → handleApproveAllImages`, or '' when nothing was folded. */
|
||||
export function stepViaText(link: WireStepLink): string {
|
||||
return link.via.map((v) => v.name).join(' → ');
|
||||
}
|
||||
|
||||
/** The layout edge a link draws as, or null when it is a self-loop. */
|
||||
export function stepPairId(link: WireStepLink): string | null {
|
||||
return link.from === link.to ? null : linkId({ source: link.from, target: link.to });
|
||||
}
|
||||
@@ -694,6 +694,76 @@ export interface WireScreensPayload {
|
||||
timing: { elapsedMs: number };
|
||||
}
|
||||
|
||||
/* ------------------------------------------------------------------ steps -- */
|
||||
|
||||
export type WireStepKind = 'anchor' | 'screen' | 'trigger' | 'bridge' | 'event' | 'store' | 'effect';
|
||||
|
||||
export type WireStepLinkKind = 'calls' | 'navigates' | 'handler' | 'bridge' | 'event' | 'store' | 'effect';
|
||||
|
||||
export interface WireStepSite {
|
||||
file: string;
|
||||
line: number;
|
||||
/** `push /capture`, `calls`, `client.post` — what the site does, in a word or two. */
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface WireStep {
|
||||
/** The node's id, or `effect:<function id>:<api>` for a call leaving the index. */
|
||||
id: string;
|
||||
kind: WireStepKind;
|
||||
/** The step the picture starts from. A screen anchor keeps `kind: 'screen'`. */
|
||||
anchor: boolean;
|
||||
/** Null only for an effect, which is a call site rather than a symbol. */
|
||||
node: WireNodeRef | null;
|
||||
label: string;
|
||||
sub: string;
|
||||
/** Steps from the anchor: the row. */
|
||||
depth: number;
|
||||
/**
|
||||
* Why the walk did not go on from this step: a cap (`depth`, `fan-out`,
|
||||
* `folded`, `steps`), or `screen` — another screen, drawn as a boundary.
|
||||
*/
|
||||
cut: 'depth' | 'fan-out' | 'folded' | 'steps' | 'screen' | 'component' | null;
|
||||
/** The event name a native event step arrived on — the first, when several land here. */
|
||||
event?: string;
|
||||
/** Every event that lands on this step. */
|
||||
events?: string[];
|
||||
screen?: { path: string; component: WireNodeRef | null };
|
||||
/** The calls one function makes into one category, and the function. */
|
||||
effect?: { api: string; apis: string[]; category: string; by: WireNodeRef; line: number };
|
||||
}
|
||||
|
||||
export interface WireStepLink {
|
||||
id: string;
|
||||
from: string;
|
||||
to: string;
|
||||
kind: WireStepLinkKind;
|
||||
/** The symbols folded between the two steps, in order. */
|
||||
via: WireNodeRef[];
|
||||
/** Conditions along the whole chain, joined; '' when unconditional. */
|
||||
when: string;
|
||||
/** How the last hop was established when it was not a plain call. */
|
||||
label: string;
|
||||
synthesized: boolean;
|
||||
uncertain: boolean;
|
||||
sites: WireStepSite[];
|
||||
}
|
||||
|
||||
export interface WireStepsPayload {
|
||||
anchor: WireNodeRef;
|
||||
/** Other symbols that share the anchor's name, when it was given by name. */
|
||||
ambiguous: WireNodeRef[];
|
||||
steps: WireStep[];
|
||||
links: WireStepLink[];
|
||||
depth: number;
|
||||
limit: number;
|
||||
/** Screens reached from the anchor were entered rather than drawn as boundaries. */
|
||||
through: boolean;
|
||||
truncated: { steps: number; hubs: number; chrome: number };
|
||||
index: { lastIndexedAt: number | null; edges: number; files: number };
|
||||
timing: { elapsedMs: number };
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------- dead code -- */
|
||||
|
||||
/** One symbol nothing in the index reaches. */
|
||||
|
||||
Reference in New Issue
Block a user