feat(expo-router): add Expo Router support for Screens and navigations and introduce Steps API
- Introduces trigger metadata for steps and edges to capture what fires a site (JS prop, on* option, or callback) to improve cross-boundary flow analysis. - Extends parsing/analysis to detect triggers in JSX attributes, on* bindings, and late-bound callbacks; adds utilities (calleeText, lastSegment) to extract trigger sources. - Ships new trigger structures (WireStepTrigger, trigger on WireStepSite/WireStep) and propagates trigger through built steps; updates step labeling to reflect trigger information. - Adds triggerWords helper and uses it to render human-readable trigger descriptions in Steps UI, including edge labels and per-site visuals. - Updates UI (ScreensView, StepsView) to display FIRES FROM information, with styling tweaks to highlight triggers and related elements; enhances tooltips and inline text wrapping for readability. - Extends tests to cover trigger detection and rendering across various binding patterns (prop, option, callback) and inline RN listeners. - Updates design/docs and changelog to reflect Expo Router integration, per-site trigger metadata, and the new Steps surface.
This commit is contained in:
@@ -12,7 +12,7 @@
|
||||
* are the links into and out of the selected step.
|
||||
*/
|
||||
|
||||
import type { WireMapLink, WireMapModule, WireStep, WireStepLink, WireStepsPayload } from './wire';
|
||||
import type { WireMapLink, WireMapModule, WireStep, WireStepLink, WireStepTrigger, WireStepsPayload } from './wire';
|
||||
import { buildMapLayout, linkId, PORT_PITCH, type MapLayout } from './map-model';
|
||||
import {
|
||||
edgeLabel,
|
||||
@@ -83,6 +83,21 @@ export function kindWord(kind: WireStep['kind']): string {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* What fires something, in a few characters: `onPress · <Button>`,
|
||||
* `onSubmit · useFormik(…)`, `addListener('onZipComplete')`, `useEffect`.
|
||||
*/
|
||||
export function triggerWords(t: WireStepTrigger): string {
|
||||
switch (t.kind) {
|
||||
case 'prop':
|
||||
return t.of ? `${t.name} · <${t.of}>` : t.name;
|
||||
case 'option':
|
||||
return t.of ? `${t.name} · ${t.of}(…)` : t.name;
|
||||
default:
|
||||
return t.of ? `${t.name}(${t.of})` : t.name;
|
||||
}
|
||||
}
|
||||
|
||||
/** 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) {
|
||||
@@ -105,7 +120,8 @@ export function stepSub(step: WireStep): string {
|
||||
case 'screen':
|
||||
return step.sub;
|
||||
case 'trigger':
|
||||
return `handler · ${file}`;
|
||||
// The event before the file: `onPress · <Button> · index.tsx`.
|
||||
return step.trigger ? `${triggerWords(step.trigger)} · ${file}` : `handler · ${file}`;
|
||||
case 'bridge':
|
||||
return `native · ${file}`;
|
||||
case 'event':
|
||||
@@ -177,12 +193,16 @@ export function buildStepsModel(payload: WireStepsPayload): StepsModel {
|
||||
byKind: [{ kind: 'calls', count: group.length }],
|
||||
topPairs: [],
|
||||
});
|
||||
// A link into a handler says the EVENT — `onPress · <Button>` — not the
|
||||
// conditions; those are one hover away, and the event is what a reader
|
||||
// asking "at what point does this run" came for.
|
||||
const trigger = group.length === 1 && first.kind === 'handler' && first.trigger ? first.trigger : null;
|
||||
edges.set(key, {
|
||||
id: key,
|
||||
from: first.from,
|
||||
to: first.to,
|
||||
links: group,
|
||||
label: edgeLabel(group),
|
||||
label: trigger ? triggerWords(trigger) : edgeLabel(group),
|
||||
synthesized: group.every((l) => l.synthesized),
|
||||
kind: group.every((l) => l.kind === first.kind) ? first.kind : 'calls',
|
||||
});
|
||||
|
||||
@@ -710,6 +710,19 @@ export interface WireStepSite {
|
||||
args?: string;
|
||||
/** The conditions THIS site runs under (the whole chain's); '' when unconditional. */
|
||||
when: string;
|
||||
/** What fires THIS site, when it differs from the link's first. */
|
||||
trigger?: WireStepTrigger;
|
||||
}
|
||||
|
||||
/** What fires a step or a link: the event it is written under, and the function that writes it there. */
|
||||
export interface WireStepTrigger {
|
||||
kind: 'prop' | 'option' | 'callback';
|
||||
/** `onPress`, `onSubmit`, `useEffect`, `addListener`. */
|
||||
name: string;
|
||||
/** `Button` for a prop, `useFormik` for an option, the first string argument for a callback; null when unknown. */
|
||||
of: string | null;
|
||||
/** The function the binding is written in. */
|
||||
in: string;
|
||||
}
|
||||
|
||||
export interface WireStep {
|
||||
@@ -733,6 +746,8 @@ export interface WireStep {
|
||||
event?: string;
|
||||
/** Every event that lands on this step. */
|
||||
events?: string[];
|
||||
/** For a handler: what fires it. */
|
||||
trigger?: WireStepTrigger;
|
||||
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 };
|
||||
@@ -752,6 +767,8 @@ export interface WireStepLink {
|
||||
synthesized: boolean;
|
||||
uncertain: boolean;
|
||||
sites: WireStepSite[];
|
||||
/** What fires the first site, when something binds it to an event. */
|
||||
trigger?: WireStepTrigger;
|
||||
}
|
||||
|
||||
export interface WireStepsPayload {
|
||||
|
||||
Reference in New Issue
Block a user