@ankhorage/contracts 11.1.0 → 12.0.1

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.
@@ -7,13 +7,14 @@ import type {
7
7
  StackScreenOptions,
8
8
  } from '../navigator';
9
9
  import type { CustomNavigatorRegistry } from './extensions';
10
+ import type { NavigatorDependencyRequirement } from './generation';
10
11
 
11
- export interface ResolvedCustomTabsPresentation {
12
+ export interface ResolvedHeadlessTabsPresentation {
12
13
  presentation: ResolvedTabsPresentation;
13
14
  customPresentationId?: string;
14
15
  }
15
16
 
16
- export type ResolvedTabsImplementation = 'custom' | 'javascript' | 'native';
17
+ export type ResolvedTabsImplementation = 'headless' | 'javascript' | 'native';
17
18
 
18
19
  export type ResolvedTabsPresentation = 'bottom' | 'top' | 'rail' | 'sidebar' | 'custom';
19
20
 
@@ -60,7 +61,7 @@ export type NavigatorAdapterId =
60
61
  | 'drawer'
61
62
  | 'tabs.native'
62
63
  | 'tabs.javascript'
63
- | 'tabs.custom'
64
+ | 'tabs.headless'
64
65
  | 'split-view'
65
66
  | 'custom';
66
67
 
@@ -76,6 +77,8 @@ export interface NavigatorAdapterPlan {
76
77
 
77
78
  export type NavigatorApiStability = 'stable' | 'alpha';
78
79
 
80
+ export type NavigatorCapabilityId = string;
81
+
79
82
  export interface NavigatorDiagnostic {
80
83
  code: string;
81
84
  severity: 'error' | 'warning';
@@ -115,11 +118,9 @@ export interface NavigatorPlan {
115
118
  context: NavigatorValidationContext;
116
119
  root: NavigatorNodePlan;
117
120
  diagnostics: readonly NavigatorDiagnostic[];
118
- supported: boolean;
119
- flows: {
120
- onboarding: boolean;
121
- authentication: boolean;
122
- };
121
+ support: NavigatorSupportStatus;
122
+ capabilityIds: readonly NavigatorCapabilityId[];
123
+ dependencies: readonly NavigatorDependencyRequirement[];
123
124
  }
124
125
 
125
126
  export type NavigatorResponsiveSize = 'compact' | 'medium' | 'expanded';
@@ -138,7 +139,7 @@ export interface NavigatorRoutePlan {
138
139
 
139
140
  export type NavigatorRuntimePlatform = 'android' | 'ios' | 'web';
140
141
 
141
- export type NavigatorSupportStatus = 'supported' | 'unavailable';
142
+ export type NavigatorSupportStatus = 'supported' | 'testing-only' | 'unsupported';
142
143
 
143
144
  export interface NavigatorValidationContext {
144
145
  platform: NavigatorRuntimePlatform;
@@ -2,8 +2,10 @@ import { expect, test } from 'bun:test';
2
2
 
3
3
  import type {
4
4
  CustomNavigatorRegistration,
5
+ NavigatorCatalog,
5
6
  NavigatorGeneratedFile,
6
7
  NavigatorGenerationBindings,
8
+ NavigatorGenerationResult,
7
9
  NavigatorPlan,
8
10
  } from './navigator';
9
11
 
@@ -32,10 +34,59 @@ test('keeps generation bindings and plan output portable across package boundari
32
34
  routes: [],
33
35
  },
34
36
  diagnostics: [],
35
- supported: true,
36
- flows: { onboarding: false, authentication: false },
37
+ support: 'supported',
38
+ capabilityIds: ['slot'],
39
+ dependencies: [],
37
40
  };
38
- expect(JSON.parse(JSON.stringify({ bindings, file, plan }))).toEqual({ bindings, file, plan });
41
+ const result: NavigatorGenerationResult = {
42
+ support: plan.support,
43
+ capabilityIds: plan.capabilityIds,
44
+ dependencies: plan.dependencies,
45
+ diagnostics: plan.diagnostics,
46
+ plan,
47
+ files: [file],
48
+ };
49
+ expect(JSON.parse(JSON.stringify({ bindings, result }))).toEqual({ bindings, result });
50
+ });
51
+
52
+ test('separates capability taxonomy, target support, stability, and verification', () => {
53
+ const catalog: NavigatorCatalog = {
54
+ capabilities: [
55
+ {
56
+ id: 'tabs.headless.sidebar',
57
+ topology: 'tabs',
58
+ implementation: 'headless',
59
+ presentation: 'sidebar',
60
+ stability: 'stable',
61
+ targets: [
62
+ {
63
+ platform: 'web',
64
+ support: 'supported',
65
+ verification: [{ kind: 'browser', status: 'verified' }],
66
+ },
67
+ {
68
+ platform: 'ios',
69
+ support: 'testing-only',
70
+ verification: [{ kind: 'device', status: 'unverified' }],
71
+ },
72
+ ],
73
+ dependencies: [],
74
+ requirements: [],
75
+ incompatibilities: [],
76
+ limitations: [],
77
+ },
78
+ ],
79
+ presets: [
80
+ {
81
+ id: 'drawer',
82
+ description: 'Drawer as the root navigator with direct routes.',
83
+ topology: ['drawer'],
84
+ },
85
+ ],
86
+ };
87
+
88
+ expect(catalog.capabilities[0]?.targets[1]?.support).toBe('testing-only');
89
+ expect(catalog.presets[0]?.topology).toEqual(['drawer']);
39
90
  });
40
91
 
41
92
  test('describes a custom extension without importing Navigator or a UI runtime', () => {
@@ -6,7 +6,7 @@ import { describe, expect, it } from 'bun:test';
6
6
  import { isAppNavigatorManifest } from './appManifest/screens';
7
7
  import type {
8
8
  CustomNavigatorNode,
9
- CustomTabsConfig,
9
+ HeadlessTabsConfig,
10
10
  NavigatorNode,
11
11
  StackImplementationConfig,
12
12
  } from './navigator';
@@ -20,9 +20,9 @@ describe('app navigator manifest type discrimination', () => {
20
20
  implementation: 'experimental',
21
21
  options: { presentation: 'modal' },
22
22
  };
23
- // @ts-expect-error Fixed custom tabs do not accept a registered custom presentation id.
24
- const tabs: CustomTabsConfig = {
25
- implementation: 'custom',
23
+ // @ts-expect-error Fixed headless tabs do not accept a registered custom presentation id.
24
+ const tabs: HeadlessTabsConfig = {
25
+ implementation: 'headless',
26
26
  presentation: 'sidebar',
27
27
  customPresentationId: 'workspace-tabs',
28
28
  };
@@ -70,6 +70,33 @@ describe('app navigator manifest stack validation', () => {
70
70
  });
71
71
  });
72
72
 
73
+ describe('app navigator manifest removed fields', () => {
74
+ it('rejects flow metadata and the superseded custom Tabs implementation', () => {
75
+ expect(
76
+ isAppNavigatorManifest({
77
+ type: 'stack',
78
+ flows: { onboarding: true },
79
+ routes: [],
80
+ }),
81
+ ).toBe(false);
82
+ expect(
83
+ isAppNavigatorManifest({
84
+ type: 'tabs',
85
+ implementation: 'custom',
86
+ presentation: 'bottom',
87
+ routes: [],
88
+ }),
89
+ ).toBe(false);
90
+ expect(
91
+ isAppNavigatorManifest({
92
+ type: 'stack',
93
+ preset: 'root-stack-tabs',
94
+ routes: [],
95
+ }),
96
+ ).toBe(false);
97
+ });
98
+ });
99
+
73
100
  describe('app navigator manifest sheet validation', () => {
74
101
  it('validates native form-sheet detents and their presentation discriminant', () => {
75
102
  for (const sheetAllowedDetents of [[], [0.8, 0.4], [0.4, 0.4], [0], [1.1], [Number.NaN]]) {
@@ -1,8 +1,8 @@
1
1
  import { describe, expect, it } from 'bun:test';
2
2
 
3
- import { isAppNavigatorManifest } from './appManifest/screens';
4
3
  import {
5
4
  type AppNavigatorManifest,
5
+ isAppNavigatorManifest,
6
6
  NAVIGATOR_PRESETS,
7
7
  NAVIGATOR_TYPES,
8
8
  type NavigatorNode,
@@ -10,6 +10,11 @@ import {
10
10
  type TabsNavigatorConfig,
11
11
  } from './navigator';
12
12
 
13
+ function parseNavigator(value: unknown): AppNavigatorManifest {
14
+ if (!isAppNavigatorManifest(value)) throw new Error('Expected a navigator manifest.');
15
+ return value;
16
+ }
17
+
13
18
  function createAdaptiveTabs(): AppNavigatorManifest {
14
19
  return {
15
20
  type: 'tabs',
@@ -23,7 +28,6 @@ function createAdaptiveTabs(): AppNavigatorManifest {
23
28
  expanded: 'sidebar',
24
29
  },
25
30
  },
26
- flows: { onboarding: true },
27
31
  routes: [{ name: 'home', path: '/', screenId: 'home' }],
28
32
  };
29
33
  }
@@ -82,14 +86,33 @@ const VALID_NAVIGATOR_NODES = [
82
86
  },
83
87
  ] as const satisfies readonly NavigatorNode[];
84
88
 
89
+ it('narrows unknown input through the public navigator boundary', () => {
90
+ const manifest = parseNavigator({ type: 'drawer', routes: [] });
91
+
92
+ expect(manifest.type).toBe('drawer');
93
+ });
94
+
85
95
  describe('app navigator manifest topology', () => {
86
96
  it('keeps canonical topology presets finite and authorable', () => {
87
97
  expect(NAVIGATOR_TYPES).toEqual(['slot', 'stack', 'tabs', 'drawer', 'split-view', 'custom']);
88
- expect(NAVIGATOR_PRESETS).toContain('slot');
89
- expect(NAVIGATOR_PRESETS).toContain('split-view');
90
- expect(NAVIGATOR_PRESETS).toContain('custom');
91
- expect(NAVIGATOR_PRESETS).toContain('root-stack-tabs-stack');
92
- expect(NAVIGATOR_PRESETS).toContain('root-stack-drawer-tabs-stack');
98
+ expect(NAVIGATOR_PRESETS).toEqual([
99
+ 'slot',
100
+ 'stack',
101
+ 'tabs',
102
+ 'tabs-stack',
103
+ 'stack-tabs',
104
+ 'stack-tabs-stack',
105
+ 'drawer',
106
+ 'drawer-stack',
107
+ 'stack-drawer',
108
+ 'stack-drawer-stack',
109
+ 'drawer-tabs',
110
+ 'drawer-tabs-stack',
111
+ 'stack-drawer-tabs',
112
+ 'stack-drawer-tabs-stack',
113
+ 'split-view',
114
+ 'custom',
115
+ ]);
93
116
  });
94
117
 
95
118
  it('accepts adaptive native/Web tabs with responsive custom presentation', () => {
@@ -152,12 +175,12 @@ describe('app navigator manifest node variants', () => {
152
175
  });
153
176
  });
154
177
 
155
- describe('app navigator manifest custom presentation', () => {
178
+ describe('app navigator manifest headless presentation', () => {
156
179
  it('accepts fixed and registered custom Web presentations', () => {
157
180
  expect(
158
181
  isAppNavigatorManifest({
159
182
  type: 'tabs',
160
- implementation: 'custom',
183
+ implementation: 'headless',
161
184
  presentation: 'sidebar',
162
185
  routes: [],
163
186
  }),
@@ -166,7 +189,7 @@ describe('app navigator manifest custom presentation', () => {
166
189
  expect(
167
190
  isAppNavigatorManifest({
168
191
  type: 'tabs',
169
- implementation: 'custom',
192
+ implementation: 'headless',
170
193
  presentation: 'custom',
171
194
  customPresentationId: 'workspace-tabs',
172
195
  routes: [],
@@ -178,7 +201,7 @@ describe('app navigator manifest custom presentation', () => {
178
201
  expect(
179
202
  isAppNavigatorManifest({
180
203
  type: 'tabs',
181
- implementation: 'custom',
204
+ implementation: 'headless',
182
205
  presentation: 'responsive',
183
206
  routes: [],
184
207
  }),
@@ -187,7 +210,7 @@ describe('app navigator manifest custom presentation', () => {
187
210
  expect(
188
211
  isAppNavigatorManifest({
189
212
  type: 'tabs',
190
- implementation: 'custom',
213
+ implementation: 'headless',
191
214
  presentation: 'custom',
192
215
  routes: [],
193
216
  }),
@@ -219,7 +242,7 @@ describe('app navigator manifest tabs branch validation', () => {
219
242
  expect(
220
243
  isAppNavigatorManifest({
221
244
  type: 'tabs',
222
- implementation: 'custom',
245
+ implementation: 'headless',
223
246
  presentation: 'sidebar',
224
247
  customPresentationId: 'must-not-apply',
225
248
  routes: [],
@@ -229,7 +252,7 @@ describe('app navigator manifest tabs branch validation', () => {
229
252
  expect(
230
253
  isAppNavigatorManifest({
231
254
  type: 'tabs',
232
- implementation: 'custom',
255
+ implementation: 'headless',
233
256
  presentation: 'custom',
234
257
  customPresentationId: 'workspace-tabs',
235
258
  responsive: { compact: 'bottom', expanded: 'sidebar' },
@@ -240,14 +263,21 @@ describe('app navigator manifest tabs branch validation', () => {
240
263
  });
241
264
 
242
265
  describe('app navigator manifest composition', () => {
243
- it('keeps nested topology separate from app-level flow metadata', () => {
266
+ it('expresses app-owned sequences as ordinary guarded navigator branches', () => {
244
267
  expect(
245
268
  isAppNavigatorManifest({
246
269
  type: 'stack',
247
- flows: { authentication: true },
248
270
  routes: [
249
271
  {
250
- name: 'app',
272
+ name: 'entry',
273
+ navigator: {
274
+ type: 'stack',
275
+ routes: [{ name: 'step', screenId: 'step' }],
276
+ },
277
+ },
278
+ {
279
+ name: 'main',
280
+ guards: ['entry-complete'],
251
281
  navigator: {
252
282
  type: 'tabs',
253
283
  routes: [{ name: 'home', screenId: 'home' }],
@@ -257,7 +287,9 @@ describe('app navigator manifest composition', () => {
257
287
  }),
258
288
  ).toBe(true);
259
289
  });
290
+ });
260
291
 
292
+ describe('app navigator manifest route metadata', () => {
261
293
  it('preserves authored route names, labels, paths, guards, and visibility without inference', () => {
262
294
  const navigator = {
263
295
  type: 'stack',
package/src/navigator.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import type { IconSpec, ManifestValue } from './types';
2
2
 
3
+ export { isAppNavigatorManifest } from './appManifest/navigator';
4
+
3
5
  export const NAVIGATOR_TYPES = ['slot', 'stack', 'tabs', 'drawer', 'split-view', 'custom'] as const;
4
6
  export type NavigatorType = (typeof NAVIGATOR_TYPES)[number];
5
7
 
@@ -8,16 +10,16 @@ export const NAVIGATOR_PRESETS = [
8
10
  'stack',
9
11
  'tabs',
10
12
  'tabs-stack',
13
+ 'stack-tabs',
14
+ 'stack-tabs-stack',
11
15
  'drawer',
12
16
  'drawer-stack',
17
+ 'stack-drawer',
18
+ 'stack-drawer-stack',
13
19
  'drawer-tabs',
14
20
  'drawer-tabs-stack',
15
- 'root-stack-tabs',
16
- 'root-stack-tabs-stack',
17
- 'root-stack-drawer',
18
- 'root-stack-drawer-stack',
19
- 'root-stack-drawer-tabs',
20
- 'root-stack-drawer-tabs-stack',
21
+ 'stack-drawer-tabs',
22
+ 'stack-drawer-tabs-stack',
21
23
  'split-view',
22
24
  'custom',
23
25
  ] as const;
@@ -94,15 +96,15 @@ export interface DrawerNavigatorOptions {
94
96
  headerShown?: boolean;
95
97
  }
96
98
 
97
- export const FIXED_CUSTOM_TABS_PRESENTATIONS = ['bottom', 'top', 'rail', 'sidebar'] as const;
98
- export type FixedCustomTabsPresentation = (typeof FIXED_CUSTOM_TABS_PRESENTATIONS)[number];
99
+ export const FIXED_HEADLESS_TABS_PRESENTATIONS = ['bottom', 'top', 'rail', 'sidebar'] as const;
100
+ export type FixedHeadlessTabsPresentation = (typeof FIXED_HEADLESS_TABS_PRESENTATIONS)[number];
99
101
 
100
- export const CUSTOM_TABS_PRESENTATIONS = [
101
- ...FIXED_CUSTOM_TABS_PRESENTATIONS,
102
+ export const HEADLESS_TABS_PRESENTATIONS = [
103
+ ...FIXED_HEADLESS_TABS_PRESENTATIONS,
102
104
  'responsive',
103
105
  'custom',
104
106
  ] as const;
105
- export type CustomTabsPresentation = (typeof CUSTOM_TABS_PRESENTATIONS)[number];
107
+ export type HeadlessTabsPresentation = (typeof HEADLESS_TABS_PRESENTATIONS)[number];
106
108
 
107
109
  export const JAVASCRIPT_TABS_PRESENTATIONS = ['bottom', 'top'] as const;
108
110
  export type JavaScriptTabsPresentation = (typeof JAVASCRIPT_TABS_PRESENTATIONS)[number];
@@ -116,14 +118,14 @@ export const NATIVE_TABS_MINIMIZE_BEHAVIORS = [
116
118
  export type NativeTabsMinimizeBehavior = (typeof NATIVE_TABS_MINIMIZE_BEHAVIORS)[number];
117
119
 
118
120
  export interface ResponsiveTabsPresentation {
119
- compact: FixedCustomTabsPresentation;
120
- medium?: FixedCustomTabsPresentation;
121
- expanded: FixedCustomTabsPresentation;
121
+ compact: FixedHeadlessTabsPresentation;
122
+ medium?: FixedHeadlessTabsPresentation;
123
+ expanded: FixedHeadlessTabsPresentation;
122
124
  }
123
125
 
124
- export type CustomTabsPresentationConfig =
126
+ export type HeadlessTabsPresentationConfig =
125
127
  | {
126
- presentation: FixedCustomTabsPresentation;
128
+ presentation: FixedHeadlessTabsPresentation;
127
129
  responsive?: never;
128
130
  customPresentationId?: never;
129
131
  }
@@ -139,11 +141,11 @@ export type CustomTabsPresentationConfig =
139
141
  customPresentationId: string;
140
142
  };
141
143
 
142
- export type CustomTabsConfig = {
143
- implementation: 'custom';
144
- } & CustomTabsPresentationConfig;
144
+ export type HeadlessTabsConfig = {
145
+ implementation: 'headless';
146
+ } & HeadlessTabsPresentationConfig;
145
147
 
146
- export type CustomTabsWebConfig = CustomTabsPresentationConfig;
148
+ export type HeadlessTabsWebConfig = HeadlessTabsPresentationConfig;
147
149
 
148
150
  export interface NavigatorScreenReference {
149
151
  screenId: string;
@@ -167,12 +169,12 @@ export interface AdaptiveTabsConfig {
167
169
  implementation?: 'adaptive';
168
170
  /** Android/iOS branch. Expo Router may expose this implementation as unstable. */
169
171
  native?: NativeTabsConfig;
170
- /** Web branch rendered through headless custom tabs. */
171
- web?: CustomTabsWebConfig;
172
+ /** Web branch rendered through headless tabs. */
173
+ web?: HeadlessTabsWebConfig;
172
174
  }
173
175
 
174
176
  export type TabsImplementationConfig =
175
- AdaptiveTabsConfig | CustomTabsConfig | JavaScriptTabsConfig | NativeTabsConfig;
177
+ AdaptiveTabsConfig | HeadlessTabsConfig | JavaScriptTabsConfig | NativeTabsConfig;
176
178
 
177
179
  interface NavigatorNodeBase {
178
180
  initialRouteName?: string;
@@ -237,11 +239,6 @@ export interface RouteDefinition {
237
239
  navigator?: NavigatorNode;
238
240
  }
239
241
 
240
- export interface NavigatorFlows {
241
- onboarding?: boolean;
242
- authentication?: boolean;
243
- }
244
-
245
242
  export interface NavigatorDefaults {
246
243
  tabs?: TabsImplementationConfig;
247
244
  stack?: StackImplementationConfig;
@@ -266,20 +263,33 @@ export interface NavigatorPlatforms {
266
263
  */
267
264
  export type AppNavigatorManifest = NavigatorNode & {
268
265
  preset?: NavigatorPreset;
269
- flows?: NavigatorFlows;
270
266
  defaults?: NavigatorDefaults;
271
267
  platforms?: NavigatorPlatforms;
272
268
  };
273
269
 
270
+ export type {
271
+ NavigatorCapabilityDescriptor,
272
+ NavigatorCapabilityRequirement,
273
+ NavigatorCapabilityTarget,
274
+ NavigatorCapabilityVerification,
275
+ NavigatorCatalog,
276
+ NavigatorImplementation,
277
+ NavigatorPresentation,
278
+ NavigatorPresetDescriptor,
279
+ NavigatorVerificationKind,
280
+ NavigatorVerificationStatus,
281
+ } from './navigator/catalog';
274
282
  export type {
275
283
  CustomNavigatorConfigIssue,
276
284
  CustomNavigatorRegistration,
277
285
  CustomNavigatorRegistry,
278
286
  } from './navigator/extensions';
279
287
  export type {
288
+ NavigatorDependencyRequirement,
280
289
  NavigatorGeneratedFile,
281
290
  NavigatorGenerationBindings,
282
291
  NavigatorGenerationOptions,
292
+ NavigatorGenerationResult,
283
293
  NavigatorScreenModule,
284
294
  } from './navigator/generation';
285
295
  export type {
@@ -288,6 +298,7 @@ export type {
288
298
  NavigatorAdapterId,
289
299
  NavigatorAdapterPlan,
290
300
  NavigatorApiStability,
301
+ NavigatorCapabilityId,
291
302
  NavigatorDiagnostic,
292
303
  NavigatorNodePlan,
293
304
  NavigatorPlan,
@@ -296,7 +307,7 @@ export type {
296
307
  NavigatorRuntimePlatform,
297
308
  NavigatorSupportStatus,
298
309
  NavigatorValidationContext,
299
- ResolvedCustomTabsPresentation,
310
+ ResolvedHeadlessTabsPresentation,
300
311
  ResolvedTabsImplementation,
301
312
  ResolvedTabsPresentation,
302
313
  TabsNavigatorPlan,