@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.
@@ -0,0 +1,42 @@
1
+ /**
2
+ * What a project invented and React Navigation does not name.
3
+ *
4
+ * Every axis left unset falls back to a default. The axes with no default —
5
+ * `helperCalls`, `predicates` — stay empty rather than being guessed: a wrong
6
+ * default manufactures a plausible graph, whereas an empty axis shows up in the
7
+ * match counts.
8
+ */
9
+ export interface ReactNavigationDialect {
10
+ /** Project-specific helpers that build a route. **Group 1 = route name.** */
11
+ helperCalls?: RegExp[];
12
+ /** Object keys carrying a literal route name (`screen: "Offer"`). */
13
+ configKeys?: string[];
14
+ /** JSX props and object keys carrying the user-visible label. */
15
+ labelProps?: string[];
16
+ /** Keys of registry entries in array form. */
17
+ registryEntry?: {
18
+ name: string;
19
+ component: string;
20
+ };
21
+ /** Mount-guard key in the static API (`if: useIsSignedIn`). */
22
+ guardKey?: string;
23
+ /** Predicates read from the component. **Group 1 = predicate name.** */
24
+ predicates?: Array<{
25
+ kind: string;
26
+ pattern: RegExp;
27
+ }>;
28
+ /** Path fragments to skip. */
29
+ exclude?: string[];
30
+ /** Depth of the closure used to attribute a call site to a screen. */
31
+ closureDepth?: number;
32
+ /** Graph name derived from the navigator's path. */
33
+ graphLabel?: (path: string) => string;
34
+ /**
35
+ * `linking` configuration files mapping screen -> URL path.
36
+ *
37
+ * A project that navigates by URL (`navigate(ROUTES.SETTINGS_PROFILE)`)
38
+ * never names the screen at the call site: the link between the two lives
39
+ * only in that configuration. Without it, zero edges.
40
+ */
41
+ routeAliasFiles?: string[];
42
+ }
@@ -0,0 +1,95 @@
1
+ import type { Domain } from "@cleya/domain";
2
+ import { type Census } from "./census";
3
+ import type { Dialect } from "./dialect";
4
+ import { type Resolve } from "./route-names";
5
+ import type { RegistryLiteral } from "./type-index";
6
+ /**
7
+ * `withAuthProtection(BookingDetails)` -> `BookingDetails`.
8
+ *
9
+ * Registries wrap screens in HOCs; the first identifier is the wrapper, which
10
+ * would make every guarded route point at the same component.
11
+ *
12
+ * A namespace import is not a component: the code graph does not declare its
13
+ * members, so keeping the first segment would point forty-seven mounts at the
14
+ * same non-existent symbol. The last segment is kept instead.
15
+ *
16
+ * @example
17
+ * unwrapComponent("withOnyx(HomeScreen)"); // "HomeScreen"
18
+ * unwrapComponent("React.memo(withOnyx(HomeScreen))"); // "HomeScreen"
19
+ * unwrapComponent("ModalStackNavigators.SettingsModalStackNavigator");// "SettingsModalStackNavigator"
20
+ * unwrapComponent(`lazy(() => require("./X").default)`); // "lazy" — nothing to descend into
21
+ * unwrapComponent("123"); // null
22
+ */
23
+ export declare const unwrapComponent: (expr: string) => string | null;
24
+ /** One screen registration, as read from a registry or a JSX tag. */
25
+ export interface Registration {
26
+ route: string;
27
+ line: number;
28
+ shape: Domain.Region.Navigation.ScreenShape;
29
+ /**
30
+ * The component rendered: an identifier from the file, or
31
+ * `module:<specifier>` when the registry loads it lazily
32
+ * (`() => require("…").default`). The two resolve differently — through the
33
+ * graph for the first, through the module path for the second — hence the
34
+ * distinction carried here.
35
+ */
36
+ component: string | null;
37
+ guard: string | null;
38
+ hasOptions: boolean;
39
+ /** Navigator named by the enclosing declaration, when a file holds several. */
40
+ graph?: string;
41
+ }
42
+ /**
43
+ * Lazy loaders declared in the file:
44
+ * `const loadSettings = () => require("…/InitialSettingsPage").default`.
45
+ *
46
+ * `getComponent={loadSettings}` does not name a component, it names the loader.
47
+ * Without this table the screen is registered with no component — hence no
48
+ * closure, hence no outgoing edge at all. Eighteen `AuthScreens` screens depend
49
+ * on it, the home screen among them.
50
+ *
51
+ * @example
52
+ * const src = `const loadSettings = () => require("./Settings").default;`;
53
+ * lazyLoaders(src, stripNoise(src)).get("loadSettings"); // "./Settings"
54
+ */
55
+ export declare const lazyLoaders: (src: string, blanked: string) => Map<string, string>;
56
+ /**
57
+ * Registrations read from a `screens: {…}` block or an array-form registry.
58
+ *
59
+ * @example
60
+ * fromRegistries(src, stripNoise(src), dialect, "Nav.tsx", resolve);
61
+ * // [{ route: "Home", shape: "static-config", component: "HomePage", … }]
62
+ */
63
+ export declare const fromRegistries: (src: string, blanked: string, d: Dialect, file: string, resolve: Resolve) => Registration[];
64
+ /**
65
+ * The registries typing designated, whatever their spelling.
66
+ *
67
+ * `createXNavigator({ [SCREENS.A]: () => require("…") })` and
68
+ * `const CENTRAL_PANE_SCREENS = {…} satisfies Screens` are the same thing: an
69
+ * object whose keys name routes and whose values render components.
70
+ * Distinguishing them used to demand two dialect axes and two regular
71
+ * expressions written per project; the type says it without being asked.
72
+ *
73
+ * One difference remains, but it is structural and the AST carries it: an
74
+ * object passed as a call argument produces a navigator, which the receiving
75
+ * declaration names — a project may place forty of them in one file, which
76
+ * rules out naming the graph after the path.
77
+ *
78
+ * @example
79
+ * fromRegistryLiterals(src, blanked, dialect, "Nav.tsx", resolve, types.registryLiterals("Nav.tsx"));
80
+ * // [{ route: "Travel_MyTrips", shape: "factory-arg", graph: "TravelNavigator", … }]
81
+ */
82
+ export declare const fromRegistryLiterals: (src: string, blanked: string, d: Dialect, file: string, resolve: Resolve, registries: RegistryLiteral[]) => Registration[];
83
+ /**
84
+ * `<Stack.Screen name="Route" component={Comp} />`, brace-balanced so an
85
+ * `options={() => …}` arrow cannot end the tag early.
86
+ *
87
+ * Records into `census` every `<Screen>` tag whose name does not resolve: such
88
+ * a tag is a lost screen, not an uninteresting one.
89
+ *
90
+ * @example
91
+ * fromJsx(src, stripNoise(src), dialect, "Nav.tsx", resolve, census);
92
+ * // [{ route: "Home", shape: "inline-jsx", component: "HomePage", … }]
93
+ * census.jsxMissed; // ["Nav.tsx:12 name={SCREENS.UNKNOWN}"]
94
+ */
95
+ export declare const fromJsx: (src: string, blanked: string, d: Dialect, file: string, resolve: Resolve, census: Census) => Registration[];
@@ -0,0 +1,135 @@
1
+ import { type Attr } from "./blank";
2
+ /**
3
+ * Collect string enum members into the name table.
4
+ *
5
+ * Numeric members are skipped: only a string member designates a route name.
6
+ *
7
+ * @example
8
+ * const names = new Map<string, string>();
9
+ * const src = `enum Screens { HOME = "home", PROFILE = "profile" }`;
10
+ * collectEnums(src, stripNoise(src), names);
11
+ * names.get("Screens.HOME"); // "home"
12
+ * // `enum Mixed { A = 1, B = "b" }` contributes only "Mixed.B"
13
+ */
14
+ export declare const collectEnums: (src: string, blanked: string, into: Map<string, string>) => void;
15
+ /**
16
+ * Name table: enum members and constant paths, mixed together.
17
+ *
18
+ * Both answer the same question — which route does this symbol designate — and
19
+ * a project may use both. One table, therefore one resolution downstream.
20
+ */
21
+ export type Names = Map<string, string>;
22
+ /**
23
+ * A leading `IDENT.PATH` reference — at least one dot, so a bare identifier
24
+ * does not match. Anchored: it reads what an expression *starts* with, which is
25
+ * the constant, the rest being accessors.
26
+ */
27
+ export declare const REF_PATH: RegExp;
28
+ /**
29
+ * Resolve a reference through the table, tail segment by tail segment.
30
+ *
31
+ * **Fallback.** The nominal resolver is the TypeChecker (`./type-index`); this
32
+ * one only serves projects offering no usable TypeScript program, and enums,
33
+ * which `collectEnums` picks up without configuration.
34
+ *
35
+ * The site almost never writes the bare path: `ROUTES.SETTINGS.route`,
36
+ * `ROUTES.SEARCH_ROOT.getRoute(…)` — the constant is the prefix, the rest is
37
+ * an accessor.
38
+ *
39
+ * @example
40
+ * const names = new Map([["ROUTES.SETTINGS", "Settings"]]);
41
+ * resolveRef(names, "ROUTES.SETTINGS.getRoute"); // "Settings" — trailing accessor dropped
42
+ * resolveRef(names, "ROUTES.SETTINGS"); // "Settings"
43
+ * resolveRef(names, "OTHER"); // null
44
+ */
45
+ export declare const resolveRef: (names: Names, ref: string) => string | null;
46
+ /**
47
+ * "Which route does this piece of code name?", asked once.
48
+ *
49
+ * All three families of site — registry key, JSX attribute, call argument — go
50
+ * through here. The range designates the expression in the file; the text only
51
+ * serves the table fallback.
52
+ *
53
+ * @example
54
+ * // The checker first, the table as a fallback — the order is a hierarchy of
55
+ * // evidence, not a preference.
56
+ * const resolve: Resolve = (file, start, end, text) =>
57
+ * types?.literalIn(file, start, end) ?? resolveRef(names, text);
58
+ */
59
+ export type Resolve = (file: string, start: number, end: number, text: string) => string | null;
60
+ /**
61
+ * The fate of a censused site.
62
+ *
63
+ * The three failures do not call for the same work, and conflating them under
64
+ * "unresolved" would send you looking in the wrong place:
65
+ * `unknown` — readable target the registry ignores: a missed registration
66
+ * `pattern` — no literal, but a known URL pattern: joinable, later
67
+ * `dynamic` — opaque: irreducible, to be documented as such
68
+ */
69
+ export type Verdict = "resolved" | "unknown" | "pattern" | "dynamic";
70
+ /**
71
+ * Classifies a censused site into a {@link Verdict}.
72
+ *
73
+ * Separate from {@link Resolve} on purpose: resolving answers "which route",
74
+ * classifying answers "why did this one produce nothing", and the census must
75
+ * not feed its own counters back into the dialect report.
76
+ *
77
+ * @example
78
+ * classify("a.ts", start, end, "ROUTES.X"); // "unknown" — readable, not registered
79
+ * classify("a.ts", start, end, "url(id)"); // "pattern" — a URL shape, joinable later
80
+ */
81
+ export type Classify = (file: string, start: number, end: number, text: string) => Verdict;
82
+ /**
83
+ * The route named by a registry key.
84
+ *
85
+ * Two cases in one field: the literal key (`Home:`), which *is* the route name,
86
+ * and the expression (`[SCREENS.TRAVEL.MY_TRIPS]:`), which designates it. The
87
+ * fallback to `key` covers the first: a key that does not resolve is still a
88
+ * valid route name, and that is the majority convention.
89
+ *
90
+ * A dotted key that does not resolve returns `null` rather than the raw text —
91
+ * `SCREENS.TRAVEL.MY_TRIPS` is a reference, and taking it for a route name
92
+ * would register a route nobody can navigate to.
93
+ *
94
+ * @example
95
+ * const resolve = () => null;
96
+ * keyRoute("Home", 0, "a.ts", resolve); // "Home" — a plain key is its own name
97
+ * keyRoute("SCREENS.HOME", 0, "a.ts", resolve); // null — unresolved reference
98
+ */
99
+ export declare const keyRoute: (key: string, keyAt: number, file: string, resolve: Resolve) => string | null;
100
+ /**
101
+ * The route name carried by an attribute: literal, or expression to resolve.
102
+ *
103
+ * @example
104
+ * routeNameOf({ literal: "Home", expr: null, at: null }, "a.tsx", 0, resolve); // "Home"
105
+ * routeNameOf({ literal: null, expr: null, at: null }, "a.tsx", 0, resolve); // null
106
+ */
107
+ export declare const routeNameOf: (value: Attr, file: string, base: number, resolve: Resolve) => string | null;
108
+ /**
109
+ * Recursive walk of the `linking` configuration.
110
+ *
111
+ * An entry whose value contains `screens:` is a nested navigator: its value
112
+ * carries its children's entries, and harvesting URL constants there would
113
+ * attach them all to the navigator instead of the screen. So we descend rather
114
+ * than read.
115
+ *
116
+ * @example
117
+ * const aliases: Names = new Map();
118
+ * collectAliases(src, stripNoise(src), objStart, "config.ts", resolve, aliases, 0);
119
+ * aliases.get("settings/profile"); // "Settings_Profile" — URL keyed to its screen
120
+ */
121
+ export declare const collectAliases: (src: string, blanked: string, objStart: number, file: string, resolve: Resolve, into: Names, depth: number) => void;
122
+ /**
123
+ * Nested symbol: more than one segment after the file path.
124
+ *
125
+ * `component={Comp}` names a top-level binding — an import or a declaration of
126
+ * the file. An enum member, on the other hand, carries a five-segment id
127
+ * (`…|AppNavigationNames|HighlightScreen`) and happens to be the homonym of the
128
+ * component it names. Keeping it as a candidate points `RENDERS` at the member
129
+ * instead of the screen.
130
+ *
131
+ * @example
132
+ * isNested("ws|internal|a.ts|Comp"); // false — a top-level binding
133
+ * isNested("ws|internal|a.ts|Names|HomeScreen"); // true — an enum member
134
+ */
135
+ export declare const isNested: (id: string) => boolean;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * The compiler's answers about one project, queried by source range.
3
+ *
4
+ * Ranges are byte offsets into the file as it sits on disk at build time. The
5
+ * index is a snapshot: a file edited after `createTypeIndex` returns will be
6
+ * answered against its old contents, so an extraction pass must not outlive the
7
+ * read that fed it.
8
+ *
9
+ * A range naming no file the program knows — outside the `tsconfig` scope, or
10
+ * spelled differently from the compiler's own path — yields `null` rather than
11
+ * throwing. Absence therefore means two things at once, "not a literal" and "not
12
+ * in the program", and callers that need to tell them apart must check
13
+ * `sourceFiles` first.
14
+ *
15
+ * Holding this index holds the whole TypeScript program — about 2 GB on a large
16
+ * repository. Release it as soon as extraction ends.
17
+ */
18
+ export interface TypeIndex {
19
+ /**
20
+ * The string literal carried by the expression spanning `[start, end)`.
21
+ *
22
+ * A range is given rather than a position: the site is found by a pattern,
23
+ * which knows the bounds of the captured text but not the shape of the tree
24
+ * beneath it. The range designates the node without having to name it.
25
+ */
26
+ literalIn(file: string, start: number, end: number): string | null;
27
+ /**
28
+ * The URL pattern, when the expression has no literal but a *template* type:
29
+ * `ROUTES.SEARCH_REPORT.getRoute(id)` has type
30
+ * `` `search/view/${string}` ``.
31
+ *
32
+ * Not yet used to build edges — it is measured, so that "unresolved" splits
33
+ * into "computed, but I know the URL shape" and "opaque". The two do not call
34
+ * for the same work, and conflating them would make something reducible look
35
+ * irreducible.
36
+ */
37
+ patternIn(file: string, start: number, end: number): string[] | null;
38
+ /**
39
+ * The object literals in this file that *are* a screen registry.
40
+ *
41
+ * Recognised by their typed shape, not by their name nor by what surrounds
42
+ * them: every key resolves to a string literal, and the values are callable.
43
+ * That holds for a factory's argument as much as for a `const` walked thirty
44
+ * lines further down — two spellings that used to demand two dialect axes,
45
+ * and that are only one thing.
46
+ *
47
+ * The neighbouring `SIDEBAR_TO_SPLIT` has exactly the same *syntactic shape*
48
+ * and excludes itself: its values are navigator names, hence strings, hence
49
+ * nothing to mount.
50
+ */
51
+ registryLiterals(file: string): RegistryLiteral[];
52
+ readonly sourceFiles: number;
53
+ readonly buildMs: number;
54
+ }
55
+ /**
56
+ * An object literal the checker recognises as a screen registry.
57
+ *
58
+ * `start` is the offset of the opening brace and `end` the offset just past the
59
+ * closing one, so `src.slice(start, end)` is the literal itself — the form a key
60
+ * walk expects.
61
+ *
62
+ * Recognition is by typed shape, never by name: an object whose keys all resolve
63
+ * to string literals and whose values are callable. A lookup table of navigator
64
+ * *names* has the same syntax and is excluded, because its values are strings
65
+ * and a string mounts nothing.
66
+ */
67
+ export interface RegistryLiteral {
68
+ /** Position of the opening brace, ready for a key walk. */
69
+ start: number;
70
+ end: number;
71
+ /**
72
+ * The object is a call's argument.
73
+ *
74
+ * Not a detail of shape: the call *produces* the navigator, so the
75
+ * declaration receiving it names it. An object declared on its own is only a
76
+ * list of screens mounted elsewhere — its name names no navigator.
77
+ */
78
+ viaCall: boolean;
79
+ }
80
+ /**
81
+ * Build the index, or return `null` when the project offers no way to.
82
+ *
83
+ * Returning `null` rather than throwing: a project without a `tsconfig.json`,
84
+ * or whose dependencies are not installed, must keep indexing through the
85
+ * tables — degraded, not broken. The caller reports the fallback.
86
+ *
87
+ * @example
88
+ * const index = createTypeIndex(root, ["navigator.ts", "screens.ts"]);
89
+ * index?.literalIn("navigator.ts", start, end); // "Travel_MyTrips"
90
+ * createTypeIndex("/no-tsconfig-here", []); // null — degrade, do not throw
91
+ */
92
+ export declare const createTypeIndex: (projectRoot: string, files: string[]) => TypeIndex | null;
package/package.json CHANGED
@@ -1,6 +1,38 @@
1
1
  {
2
- "name": "@cleya/contributor-react-navigation",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
2
+ "name": "@cleya/contributor-react-navigation",
3
+ "description": "Indexes React Navigation screens, routes and navigation edges",
4
+ "cleya": {
5
+ "category": "contributor"
6
+ },
7
+ "version": "1.0.0-next.0",
8
+ "type": "module",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/react-navigation.index.d.ts",
12
+ "default": "./dist/react-navigation.index.js"
13
+ }
14
+ },
15
+ "types": "./dist/react-navigation.index.d.ts",
16
+ "files": [
17
+ "dist"
18
+ ],
19
+ "scripts": {
20
+ "build": "bun build ./src/react-navigation.index.ts --outdir dist --target bun --external @cleya/config --external @cleya/domain --external @cleya/region-navigation --external typescript && bunx tsc -p tsconfig.build.json"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
25
+ "peerDependencies": {
26
+ "@cleya/config": "1.0.0-next.0",
27
+ "@cleya/domain": "1.0.0-next.0",
28
+ "@cleya/region-navigation": "1.0.0-next.0",
29
+ "typescript": "^5"
30
+ },
31
+ "devDependencies": {
32
+ "@cleya/config": "1.0.0-next.0",
33
+ "@cleya/core": "0.1.2-next.0",
34
+ "@cleya/domain": "1.0.0-next.0",
35
+ "@cleya/region-navigation": "1.0.0-next.0",
36
+ "@cleya/region-trace": "1.0.0-next.0"
37
+ }
38
+ }