@ankhorage/contracts 10.1.0 → 11.1.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/README.md +1 -1
  3. package/dist/appManifest/navigator.d.ts +3 -0
  4. package/dist/appManifest/navigator.d.ts.map +1 -0
  5. package/dist/appManifest/navigator.js +142 -0
  6. package/dist/appManifest/navigator.js.map +1 -0
  7. package/dist/appManifest/navigatorOptions.d.ts +13 -0
  8. package/dist/appManifest/navigatorOptions.d.ts.map +1 -0
  9. package/dist/appManifest/navigatorOptions.js +226 -0
  10. package/dist/appManifest/navigatorOptions.js.map +1 -0
  11. package/dist/appManifest/screens.d.ts +1 -2
  12. package/dist/appManifest/screens.d.ts.map +1 -1
  13. package/dist/appManifest/screens.js +2 -116
  14. package/dist/appManifest/screens.js.map +1 -1
  15. package/dist/navigator/extensions.d.ts +23 -0
  16. package/dist/navigator/extensions.d.ts.map +1 -0
  17. package/dist/navigator/extensions.js +2 -0
  18. package/dist/navigator/extensions.js.map +1 -0
  19. package/dist/navigator/generation.d.ts +26 -0
  20. package/dist/navigator/generation.d.ts.map +1 -0
  21. package/dist/navigator/generation.js +2 -0
  22. package/dist/navigator/generation.js.map +1 -0
  23. package/dist/navigator/planning.d.ts +105 -0
  24. package/dist/navigator/planning.d.ts.map +1 -0
  25. package/dist/navigator/planning.js +2 -0
  26. package/dist/navigator/planning.js.map +1 -0
  27. package/dist/navigator.d.ts +104 -15
  28. package/dist/navigator.d.ts.map +1 -1
  29. package/dist/navigator.js +23 -1
  30. package/dist/navigator.js.map +1 -1
  31. package/package.json +4 -4
  32. package/src/appManifest/navigator.ts +179 -0
  33. package/src/appManifest/navigatorOptions.ts +292 -0
  34. package/src/appManifest/screens.ts +3 -170
  35. package/src/contracts.test.ts +1 -1
  36. package/src/navigator/extensions.ts +26 -0
  37. package/src/navigator/generation.ts +28 -0
  38. package/src/navigator/planning.ts +146 -0
  39. package/src/navigator-runtime-contracts.test.ts +53 -0
  40. package/src/navigator-validation.test.ts +153 -0
  41. package/src/navigator.test.ts +153 -10
  42. package/src/navigator.ts +180 -13
package/dist/navigator.js CHANGED
@@ -1,5 +1,6 @@
1
- export const NAVIGATOR_TYPES = ['stack', 'tabs', 'drawer'];
1
+ export const NAVIGATOR_TYPES = ['slot', 'stack', 'tabs', 'drawer', 'split-view', 'custom'];
2
2
  export const NAVIGATOR_PRESETS = [
3
+ 'slot',
3
4
  'stack',
4
5
  'tabs',
5
6
  'tabs-stack',
@@ -13,7 +14,22 @@ export const NAVIGATOR_PRESETS = [
13
14
  'root-stack-drawer-stack',
14
15
  'root-stack-drawer-tabs',
15
16
  'root-stack-drawer-tabs-stack',
17
+ 'split-view',
18
+ 'custom',
19
+ ];
20
+ export const STACK_IMPLEMENTATIONS = ['native', 'javascript', 'experimental'];
21
+ export const STACK_PRESENTATIONS = [
22
+ 'card',
23
+ 'modal',
24
+ 'transparentModal',
25
+ 'containedModal',
26
+ 'containedTransparentModal',
27
+ 'fullScreenModal',
28
+ 'formSheet',
16
29
  ];
30
+ export const JAVASCRIPT_STACK_PRESENTATIONS = ['card', 'modal', 'transparentModal'];
31
+ export const DRAWER_POSITIONS = ['left', 'right'];
32
+ export const DRAWER_TYPES = ['front', 'back', 'slide', 'permanent'];
17
33
  export const FIXED_CUSTOM_TABS_PRESENTATIONS = ['bottom', 'top', 'rail', 'sidebar'];
18
34
  export const CUSTOM_TABS_PRESENTATIONS = [
19
35
  ...FIXED_CUSTOM_TABS_PRESENTATIONS,
@@ -21,4 +37,10 @@ export const CUSTOM_TABS_PRESENTATIONS = [
21
37
  'custom',
22
38
  ];
23
39
  export const JAVASCRIPT_TABS_PRESENTATIONS = ['bottom', 'top'];
40
+ export const NATIVE_TABS_MINIMIZE_BEHAVIORS = [
41
+ 'automatic',
42
+ 'never',
43
+ 'onScrollDown',
44
+ 'onScrollUp',
45
+ ];
24
46
  //# sourceMappingURL=navigator.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"navigator.js","sourceRoot":"","sources":["../src/navigator.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,CAAU,CAAC;AAGpE,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,OAAO;IACP,MAAM;IACN,YAAY;IACZ,QAAQ;IACR,cAAc;IACd,aAAa;IACb,mBAAmB;IACnB,iBAAiB;IACjB,uBAAuB;IACvB,mBAAmB;IACnB,yBAAyB;IACzB,wBAAwB;IACxB,8BAA8B;CACtB,CAAC;AAGX,MAAM,CAAC,MAAM,+BAA+B,GAAG,CAAC,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,CAAU,CAAC;AAG7F,MAAM,CAAC,MAAM,yBAAyB,GAAG;IACvC,GAAG,+BAA+B;IAClC,YAAY;IACZ,QAAQ;CACA,CAAC;AAGX,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAU,CAAC","sourcesContent":["import type { IconSpec } from './types';\n\nexport const NAVIGATOR_TYPES = ['stack', 'tabs', 'drawer'] as const;\nexport type NavigatorType = (typeof NAVIGATOR_TYPES)[number];\n\nexport const NAVIGATOR_PRESETS = [\n 'stack',\n 'tabs',\n 'tabs-stack',\n 'drawer',\n 'drawer-stack',\n 'drawer-tabs',\n 'drawer-tabs-stack',\n 'root-stack-tabs',\n 'root-stack-tabs-stack',\n 'root-stack-drawer',\n 'root-stack-drawer-stack',\n 'root-stack-drawer-tabs',\n 'root-stack-drawer-tabs-stack',\n] as const;\nexport type NavigatorPreset = (typeof NAVIGATOR_PRESETS)[number];\n\nexport const FIXED_CUSTOM_TABS_PRESENTATIONS = ['bottom', 'top', 'rail', 'sidebar'] as const;\nexport type FixedCustomTabsPresentation = (typeof FIXED_CUSTOM_TABS_PRESENTATIONS)[number];\n\nexport const CUSTOM_TABS_PRESENTATIONS = [\n ...FIXED_CUSTOM_TABS_PRESENTATIONS,\n 'responsive',\n 'custom',\n] as const;\nexport type CustomTabsPresentation = (typeof CUSTOM_TABS_PRESENTATIONS)[number];\n\nexport const JAVASCRIPT_TABS_PRESENTATIONS = ['bottom', 'top'] as const;\nexport type JavaScriptTabsPresentation = (typeof JAVASCRIPT_TABS_PRESENTATIONS)[number];\n\nexport interface ResponsiveTabsPresentation {\n compact: FixedCustomTabsPresentation;\n medium?: FixedCustomTabsPresentation;\n expanded: FixedCustomTabsPresentation;\n}\n\nexport interface CustomTabsConfig {\n implementation: 'custom';\n presentation: CustomTabsPresentation;\n responsive?: ResponsiveTabsPresentation;\n /** Serializable registered presentation id used when `presentation` is `custom`. */\n customPresentationId?: string;\n}\n\nexport interface NativeTabsConfig {\n implementation: 'native';\n}\n\nexport interface JavaScriptTabsConfig {\n implementation: 'javascript';\n presentation?: JavaScriptTabsPresentation;\n}\n\nexport interface AdaptiveTabsConfig {\n /** Omission selects the canonical adaptive default. */\n implementation?: 'adaptive';\n /** Android/iOS branch. Expo Router may expose this implementation as unstable. */\n native?: NativeTabsConfig;\n /** Web branch rendered through headless custom tabs. */\n web?: Omit<CustomTabsConfig, 'implementation'>;\n}\n\nexport type TabsImplementationConfig =\n AdaptiveTabsConfig | CustomTabsConfig | JavaScriptTabsConfig | NativeTabsConfig;\n\ninterface NavigatorNodeBase {\n initialRouteName?: string;\n routes: RouteDefinition[];\n /** Typed upstream options can be layered by the owning Navigator package. */\n options?: Record<string, unknown>;\n}\n\nexport interface StackNavigatorNode extends NavigatorNodeBase {\n type: 'stack';\n}\n\nexport interface DrawerNavigatorNode extends NavigatorNodeBase {\n type: 'drawer';\n}\n\nexport type TabsNavigatorConfig = {\n type: 'tabs';\n} & TabsImplementationConfig;\n\nexport type TabsNavigatorNode = NavigatorNodeBase & TabsNavigatorConfig;\n\nexport type NavigatorNode = DrawerNavigatorNode | StackNavigatorNode | TabsNavigatorNode;\n\nexport interface RouteDefinition {\n name: string;\n path?: string;\n label?: string;\n icon?: IconSpec;\n /** Hide this route from primary Tabs/Drawer presentation without making it unnavigable. */\n showInPrimaryNavigation?: boolean;\n guards?: string[];\n screenId?: string;\n navigator?: NavigatorNode;\n}\n\nexport interface NavigatorFlows {\n onboarding?: boolean;\n authentication?: boolean;\n}\n\nexport interface NavigatorDefaults {\n tabs?: TabsImplementationConfig;\n}\n\nexport interface NavigatorPlatformConfig {\n tabs?: TabsImplementationConfig;\n}\n\nexport interface NavigatorPlatforms {\n android?: NavigatorPlatformConfig;\n ios?: NavigatorPlatformConfig;\n web?: NavigatorPlatformConfig;\n}\n\n/**\n * Serializable desired state for `AppManifest.navigator`.\n *\n * The manifest slice is the root navigator tree itself; optional authoring metadata does not add\n * a redundant `root` wrapper. The standalone Navigator capability consumes this slice directly.\n */\nexport type AppNavigatorManifest = NavigatorNode & {\n preset?: NavigatorPreset;\n flows?: NavigatorFlows;\n defaults?: NavigatorDefaults;\n platforms?: NavigatorPlatforms;\n};\n"]}
1
+ {"version":3,"file":"navigator.js","sourceRoot":"","sources":["../src/navigator.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,QAAQ,CAAU,CAAC;AAGpG,MAAM,CAAC,MAAM,iBAAiB,GAAG;IAC/B,MAAM;IACN,OAAO;IACP,MAAM;IACN,YAAY;IACZ,QAAQ;IACR,cAAc;IACd,aAAa;IACb,mBAAmB;IACnB,iBAAiB;IACjB,uBAAuB;IACvB,mBAAmB;IACnB,yBAAyB;IACzB,wBAAwB;IACxB,8BAA8B;IAC9B,YAAY;IACZ,QAAQ;CACA,CAAC;AAGX,MAAM,CAAC,MAAM,qBAAqB,GAAG,CAAC,QAAQ,EAAE,YAAY,EAAE,cAAc,CAAU,CAAC;AAGvF,MAAM,CAAC,MAAM,mBAAmB,GAAG;IACjC,MAAM;IACN,OAAO;IACP,kBAAkB;IAClB,gBAAgB;IAChB,2BAA2B;IAC3B,iBAAiB;IACjB,WAAW;CACH,CAAC;AAGX,MAAM,CAAC,MAAM,8BAA8B,GAAG,CAAC,MAAM,EAAE,OAAO,EAAE,kBAAkB,CAAU,CAAC;AA4C7F,MAAM,CAAC,MAAM,gBAAgB,GAAG,CAAC,MAAM,EAAE,OAAO,CAAU,CAAC;AAG3D,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,CAAU,CAAC;AAU7E,MAAM,CAAC,MAAM,+BAA+B,GAAG,CAAC,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,CAAU,CAAC;AAG7F,MAAM,CAAC,MAAM,yBAAyB,GAAG;IACvC,GAAG,+BAA+B;IAClC,YAAY;IACZ,QAAQ;CACA,CAAC;AAGX,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAAC,QAAQ,EAAE,KAAK,CAAU,CAAC;AAGxE,MAAM,CAAC,MAAM,8BAA8B,GAAG;IAC5C,WAAW;IACX,OAAO;IACP,cAAc;IACd,YAAY;CACJ,CAAC","sourcesContent":["import type { IconSpec, ManifestValue } from './types';\n\nexport const NAVIGATOR_TYPES = ['slot', 'stack', 'tabs', 'drawer', 'split-view', 'custom'] as const;\nexport type NavigatorType = (typeof NAVIGATOR_TYPES)[number];\n\nexport const NAVIGATOR_PRESETS = [\n 'slot',\n 'stack',\n 'tabs',\n 'tabs-stack',\n 'drawer',\n 'drawer-stack',\n 'drawer-tabs',\n 'drawer-tabs-stack',\n 'root-stack-tabs',\n 'root-stack-tabs-stack',\n 'root-stack-drawer',\n 'root-stack-drawer-stack',\n 'root-stack-drawer-tabs',\n 'root-stack-drawer-tabs-stack',\n 'split-view',\n 'custom',\n] as const;\nexport type NavigatorPreset = (typeof NAVIGATOR_PRESETS)[number];\n\nexport const STACK_IMPLEMENTATIONS = ['native', 'javascript', 'experimental'] as const;\nexport type StackImplementation = (typeof STACK_IMPLEMENTATIONS)[number];\n\nexport const STACK_PRESENTATIONS = [\n 'card',\n 'modal',\n 'transparentModal',\n 'containedModal',\n 'containedTransparentModal',\n 'fullScreenModal',\n 'formSheet',\n] as const;\nexport type StackPresentation = (typeof STACK_PRESENTATIONS)[number];\n\nexport const JAVASCRIPT_STACK_PRESENTATIONS = ['card', 'modal', 'transparentModal'] as const;\nexport type JavaScriptStackPresentation = (typeof JAVASCRIPT_STACK_PRESENTATIONS)[number];\n\nexport interface StackHeaderOptions {\n title?: string;\n headerShown?: boolean;\n headerTransparent?: boolean;\n headerBackVisible?: boolean;\n}\n\nexport type StackScreenOptions = StackHeaderOptions &\n (\n | {\n presentation?: Exclude<StackPresentation, 'formSheet'>;\n sheetAllowedDetents?: never;\n sheetGrabberVisible?: never;\n }\n | {\n presentation: 'formSheet';\n sheetAllowedDetents?: 'fitToContents' | number[];\n sheetGrabberVisible?: boolean;\n }\n );\n\nexport type JavaScriptStackScreenOptions = StackHeaderOptions & {\n presentation?: JavaScriptStackPresentation;\n};\n\nexport type StackImplementationConfig =\n | {\n /** Omission selects the stable native stack implementation. */\n implementation?: 'native';\n options?: StackScreenOptions;\n }\n | {\n implementation: 'javascript';\n options?: JavaScriptStackScreenOptions;\n }\n | {\n /** Expo Router Experimental Stack is alpha, native-only, and available from SDK 56. */\n implementation: 'experimental';\n options?: StackHeaderOptions;\n };\n\nexport const DRAWER_POSITIONS = ['left', 'right'] as const;\nexport type DrawerPosition = (typeof DRAWER_POSITIONS)[number];\n\nexport const DRAWER_TYPES = ['front', 'back', 'slide', 'permanent'] as const;\nexport type DrawerType = (typeof DRAWER_TYPES)[number];\n\nexport interface DrawerNavigatorOptions {\n drawerPosition?: DrawerPosition;\n drawerType?: DrawerType;\n swipeEnabled?: boolean;\n headerShown?: boolean;\n}\n\nexport const FIXED_CUSTOM_TABS_PRESENTATIONS = ['bottom', 'top', 'rail', 'sidebar'] as const;\nexport type FixedCustomTabsPresentation = (typeof FIXED_CUSTOM_TABS_PRESENTATIONS)[number];\n\nexport const CUSTOM_TABS_PRESENTATIONS = [\n ...FIXED_CUSTOM_TABS_PRESENTATIONS,\n 'responsive',\n 'custom',\n] as const;\nexport type CustomTabsPresentation = (typeof CUSTOM_TABS_PRESENTATIONS)[number];\n\nexport const JAVASCRIPT_TABS_PRESENTATIONS = ['bottom', 'top'] as const;\nexport type JavaScriptTabsPresentation = (typeof JAVASCRIPT_TABS_PRESENTATIONS)[number];\n\nexport const NATIVE_TABS_MINIMIZE_BEHAVIORS = [\n 'automatic',\n 'never',\n 'onScrollDown',\n 'onScrollUp',\n] as const;\nexport type NativeTabsMinimizeBehavior = (typeof NATIVE_TABS_MINIMIZE_BEHAVIORS)[number];\n\nexport interface ResponsiveTabsPresentation {\n compact: FixedCustomTabsPresentation;\n medium?: FixedCustomTabsPresentation;\n expanded: FixedCustomTabsPresentation;\n}\n\nexport type CustomTabsPresentationConfig =\n | {\n presentation: FixedCustomTabsPresentation;\n responsive?: never;\n customPresentationId?: never;\n }\n | {\n presentation: 'responsive';\n responsive: ResponsiveTabsPresentation;\n customPresentationId?: never;\n }\n | {\n presentation: 'custom';\n responsive?: never;\n /** Serializable registered presentation id. */\n customPresentationId: string;\n };\n\nexport type CustomTabsConfig = {\n implementation: 'custom';\n} & CustomTabsPresentationConfig;\n\nexport type CustomTabsWebConfig = CustomTabsPresentationConfig;\n\nexport interface NavigatorScreenReference {\n screenId: string;\n}\n\nexport interface NativeTabsConfig {\n implementation: 'native';\n /** Expo Router Native Tabs is alpha; minimizing is available on iOS 26+ from SDK 55. */\n minimizeBehavior?: NativeTabsMinimizeBehavior;\n /** Registered screen rendered through NativeTabs.BottomAccessory, available from SDK 55. */\n bottomAccessory?: NavigatorScreenReference;\n}\n\nexport interface JavaScriptTabsConfig {\n implementation: 'javascript';\n presentation?: JavaScriptTabsPresentation;\n}\n\nexport interface AdaptiveTabsConfig {\n /** Omission selects the canonical adaptive default. */\n implementation?: 'adaptive';\n /** Android/iOS branch. Expo Router may expose this implementation as unstable. */\n native?: NativeTabsConfig;\n /** Web branch rendered through headless custom tabs. */\n web?: CustomTabsWebConfig;\n}\n\nexport type TabsImplementationConfig =\n AdaptiveTabsConfig | CustomTabsConfig | JavaScriptTabsConfig | NativeTabsConfig;\n\ninterface NavigatorNodeBase {\n initialRouteName?: string;\n routes: RouteDefinition[];\n}\n\nexport interface SlotNavigatorNode extends NavigatorNodeBase {\n type: 'slot';\n}\n\nexport type StackNavigatorNode = NavigatorNodeBase & {\n type: 'stack';\n} & StackImplementationConfig;\n\nexport interface DrawerNavigatorNode extends NavigatorNodeBase {\n type: 'drawer';\n options?: DrawerNavigatorOptions;\n}\n\nexport type TabsNavigatorConfig = {\n type: 'tabs';\n} & TabsImplementationConfig;\n\nexport type TabsNavigatorNode = NavigatorNodeBase & TabsNavigatorConfig;\n\nexport interface SplitViewNavigatorNode extends NavigatorNodeBase {\n type: 'split-view';\n columns: {\n primary: NavigatorScreenReference;\n supplementary?: NavigatorScreenReference;\n };\n inspector?: NavigatorScreenReference;\n /** Split View is alpha and iOS-only; other platforms fall back to Slot. */\n topColumnForCollapsing?: 'primary' | 'supplementary' | 'secondary';\n}\n\nexport interface CustomNavigatorNode extends NavigatorNodeBase {\n type: 'custom';\n navigatorId: string;\n config?: Readonly<Record<string, ManifestValue>>;\n}\n\nexport type NavigatorNode =\n | CustomNavigatorNode\n | DrawerNavigatorNode\n | SlotNavigatorNode\n | SplitViewNavigatorNode\n | StackNavigatorNode\n | TabsNavigatorNode;\n\nexport interface RouteDefinition {\n name: string;\n path?: string;\n label?: string;\n icon?: IconSpec;\n /** Hide this route from primary Tabs/Drawer presentation without making it unnavigable. */\n showInPrimaryNavigation?: boolean;\n guards?: string[];\n screenId?: string;\n /** Presentation applied by the resolved parent stack implementation. */\n stackOptions?: StackScreenOptions;\n navigator?: NavigatorNode;\n}\n\nexport interface NavigatorFlows {\n onboarding?: boolean;\n authentication?: boolean;\n}\n\nexport interface NavigatorDefaults {\n tabs?: TabsImplementationConfig;\n stack?: StackImplementationConfig;\n}\n\nexport interface NavigatorPlatformConfig {\n tabs?: TabsImplementationConfig;\n stack?: StackImplementationConfig;\n}\n\nexport interface NavigatorPlatforms {\n android?: NavigatorPlatformConfig;\n ios?: NavigatorPlatformConfig;\n web?: NavigatorPlatformConfig;\n}\n\n/**\n * Serializable desired state for `AppManifest.navigator`.\n *\n * The manifest slice is the root navigator tree itself; optional authoring metadata does not add\n * a redundant `root` wrapper. The standalone Navigator capability consumes this slice directly.\n */\nexport type AppNavigatorManifest = NavigatorNode & {\n preset?: NavigatorPreset;\n flows?: NavigatorFlows;\n defaults?: NavigatorDefaults;\n platforms?: NavigatorPlatforms;\n};\n\nexport type {\n CustomNavigatorConfigIssue,\n CustomNavigatorRegistration,\n CustomNavigatorRegistry,\n} from './navigator/extensions';\nexport type {\n NavigatorGeneratedFile,\n NavigatorGenerationBindings,\n NavigatorGenerationOptions,\n NavigatorScreenModule,\n} from './navigator/generation';\nexport type {\n CreateNavigatorPlanOptions,\n ExpoRouterNavigatorModule,\n NavigatorAdapterId,\n NavigatorAdapterPlan,\n NavigatorApiStability,\n NavigatorDiagnostic,\n NavigatorNodePlan,\n NavigatorPlan,\n NavigatorResponsiveSize,\n NavigatorRoutePlan,\n NavigatorRuntimePlatform,\n NavigatorSupportStatus,\n NavigatorValidationContext,\n ResolvedCustomTabsPresentation,\n ResolvedTabsImplementation,\n ResolvedTabsPresentation,\n TabsNavigatorPlan,\n} from './navigator/planning';\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ankhorage/contracts",
3
- "version": "10.1.0",
3
+ "version": "11.1.0",
4
4
  "main": "./dist/index.js",
5
5
  "ankh": {
6
6
  "category": "contracts",
@@ -13,9 +13,9 @@
13
13
  "@ankhorage/color-theory": "^0.3.0"
14
14
  },
15
15
  "devDependencies": {
16
- "@ankhorage/devtools": "^1.10.9",
16
+ "@ankhorage/devtools": "^1.11.11",
17
17
  "@ankhorage/paradox": "^0.1.24",
18
- "@types/bun": "^1.4.0",
18
+ "@types/bun": "^1.4.1",
19
19
  "@types/node": "^25.2.3",
20
20
  "typescript": "^5.9.3"
21
21
  },
@@ -104,7 +104,7 @@
104
104
  "CHANGELOG.md",
105
105
  "LICENSE"
106
106
  ],
107
- "packageManager": "bun@1.4.0",
107
+ "packageManager": "bun@1.4.2",
108
108
  "repository": {
109
109
  "type": "git",
110
110
  "url": "git+https://github.com/ankhorage/contracts.git"
@@ -0,0 +1,179 @@
1
+ import { NAVIGATOR_PRESETS, NAVIGATOR_TYPES } from '../navigator';
2
+ import { isIconSpec } from './icon';
3
+ import {
4
+ hasOnlyKeys,
5
+ isDrawerNavigatorOptions,
6
+ isNavigatorScreenReference,
7
+ isStackImplementationConfig,
8
+ isStackScreenOptions,
9
+ isTabsImplementationConfig,
10
+ } from './navigatorOptions';
11
+ import { isOptionalBoolean, isOptionalString, isRecord, isStringArray } from './shared';
12
+
13
+ const NAVIGATOR_PRESET_SET = new Set<string>(NAVIGATOR_PRESETS);
14
+ const NAVIGATOR_TYPE_SET = new Set<string>(NAVIGATOR_TYPES);
15
+
16
+ /*** Validate the complete serialized `AppManifest.navigator` slice. */
17
+ export function isAppNavigatorManifest(value: unknown): boolean {
18
+ return (
19
+ isRecord(value) &&
20
+ isNavigatorNode(value) &&
21
+ (value.preset === undefined ||
22
+ (typeof value.preset === 'string' && NAVIGATOR_PRESET_SET.has(value.preset))) &&
23
+ (value.flows === undefined || isNavigatorFlows(value.flows)) &&
24
+ (value.defaults === undefined || isNavigatorDefaults(value.defaults)) &&
25
+ (value.platforms === undefined || isNavigatorPlatforms(value.platforms))
26
+ );
27
+ }
28
+
29
+ /*** Validate one nested navigator node independently from app-level authoring metadata. */
30
+ function isNavigatorNode(value: unknown): boolean {
31
+ if (
32
+ !isRecord(value) ||
33
+ typeof value.type !== 'string' ||
34
+ !NAVIGATOR_TYPE_SET.has(value.type) ||
35
+ !isOptionalString(value.initialRouteName) ||
36
+ !Array.isArray(value.routes) ||
37
+ !value.routes.every(isRouteDefinition)
38
+ ) {
39
+ return false;
40
+ }
41
+
42
+ switch (value.type) {
43
+ case 'slot':
44
+ return value.options === undefined && value.implementation === undefined;
45
+ case 'stack':
46
+ return isStackImplementationConfig(value);
47
+ case 'tabs':
48
+ return isTabsImplementationConfig(value);
49
+ case 'drawer':
50
+ return (
51
+ value.implementation === undefined &&
52
+ (value.options === undefined || isDrawerNavigatorOptions(value.options))
53
+ );
54
+ case 'split-view':
55
+ return isSplitViewNavigatorNode(value);
56
+ case 'custom':
57
+ return isCustomNavigatorNode(value);
58
+ default:
59
+ return false;
60
+ }
61
+ }
62
+
63
+ /*** Validate one route and preserve its portable metadata and nested navigator. */
64
+ function isRouteDefinition(value: unknown): boolean {
65
+ return (
66
+ isRecord(value) &&
67
+ typeof value.name === 'string' &&
68
+ isOptionalString(value.path) &&
69
+ isOptionalString(value.label) &&
70
+ (value.icon === undefined || isIconSpec(value.icon)) &&
71
+ isOptionalBoolean(value.showInPrimaryNavigation) &&
72
+ (value.guards === undefined || isStringArray(value.guards)) &&
73
+ isOptionalString(value.screenId) &&
74
+ (value.stackOptions === undefined || isStackScreenOptions(value.stackOptions)) &&
75
+ (value.navigator === undefined || isNavigatorNode(value.navigator))
76
+ );
77
+ }
78
+
79
+ /*** Validate Split View screen references without duplicating the routed secondary tree. */
80
+ function isSplitViewNavigatorNode(value: Record<string, unknown>): boolean {
81
+ return (
82
+ value.implementation === undefined &&
83
+ value.options === undefined &&
84
+ isRecord(value.columns) &&
85
+ hasOnlyKeys(value.columns, ['primary', 'supplementary']) &&
86
+ isNavigatorScreenReference(value.columns.primary) &&
87
+ (value.columns.supplementary === undefined ||
88
+ isNavigatorScreenReference(value.columns.supplementary)) &&
89
+ (value.inspector === undefined || isNavigatorScreenReference(value.inspector)) &&
90
+ (value.topColumnForCollapsing === undefined ||
91
+ value.topColumnForCollapsing === 'primary' ||
92
+ value.topColumnForCollapsing === 'supplementary' ||
93
+ value.topColumnForCollapsing === 'secondary')
94
+ );
95
+ }
96
+
97
+ /*** Validate a registered custom navigator with JSON-safe configuration only. */
98
+ function isCustomNavigatorNode(value: Record<string, unknown>): boolean {
99
+ return (
100
+ value.implementation === undefined &&
101
+ value.options === undefined &&
102
+ typeof value.navigatorId === 'string' &&
103
+ (value.config === undefined || isNavigatorJsonRecord(value.config))
104
+ );
105
+ }
106
+
107
+ /*** Validate an acyclic, finite JSON object used by a custom navigator adapter. */
108
+ function isNavigatorJsonRecord(value: unknown): boolean {
109
+ return isRecord(value) && isNavigatorJsonValue(value, new Set<object>());
110
+ }
111
+
112
+ /*** Recursively validate the portable manifest value domain and reject cycles. */
113
+ function isNavigatorJsonValue(value: unknown, ancestors: Set<object>): boolean {
114
+ if (value === null || typeof value === 'string' || typeof value === 'boolean') return true;
115
+ if (typeof value === 'number') return Number.isFinite(value);
116
+ if (typeof value !== 'object' || ancestors.has(value)) return false;
117
+
118
+ if (Array.isArray(value)) {
119
+ if (
120
+ Object.keys(value).length !== value.length ||
121
+ Object.getOwnPropertySymbols(value).length > 0
122
+ ) {
123
+ return false;
124
+ }
125
+ } else {
126
+ const prototype = Object.getPrototypeOf(value) as object | null;
127
+ if (prototype !== Object.prototype && prototype !== null) return false;
128
+ if (Reflect.ownKeys(value).some((key) => typeof key !== 'string')) return false;
129
+ }
130
+
131
+ ancestors.add(value);
132
+ const isValid = Object.values(value).every((entry) => isNavigatorJsonValue(entry, ancestors));
133
+ ancestors.delete(value);
134
+ return isValid;
135
+ }
136
+
137
+ /*** Validate optional app-level navigator flow intent. */
138
+ function isNavigatorFlows(value: unknown): boolean {
139
+ return (
140
+ isRecord(value) &&
141
+ isOptionalBoolean(value.onboarding) &&
142
+ isOptionalBoolean(value.authentication)
143
+ );
144
+ }
145
+
146
+ /*** Validate package-owned navigator defaults without requiring a navigator node type. */
147
+ function isNavigatorDefaults(value: unknown): boolean {
148
+ return (
149
+ isRecord(value) &&
150
+ hasOnlyKeys(value, ['tabs', 'stack']) &&
151
+ (value.tabs === undefined ||
152
+ (isRecord(value.tabs) && isTabsImplementationConfig(value.tabs))) &&
153
+ (value.stack === undefined ||
154
+ (isRecord(value.stack) && isStackImplementationConfig(value.stack)))
155
+ );
156
+ }
157
+
158
+ /*** Validate per-platform navigator implementation overrides. */
159
+ function isNavigatorPlatforms(value: unknown): boolean {
160
+ return (
161
+ isRecord(value) &&
162
+ hasOnlyKeys(value, ['android', 'ios', 'web']) &&
163
+ (value.android === undefined || isNavigatorPlatformConfig(value.android)) &&
164
+ (value.ios === undefined || isNavigatorPlatformConfig(value.ios)) &&
165
+ (value.web === undefined || isNavigatorPlatformConfig(value.web))
166
+ );
167
+ }
168
+
169
+ /*** Validate one platform-specific navigator override slice. */
170
+ function isNavigatorPlatformConfig(value: unknown): boolean {
171
+ return (
172
+ isRecord(value) &&
173
+ hasOnlyKeys(value, ['tabs', 'stack']) &&
174
+ (value.tabs === undefined ||
175
+ (isRecord(value.tabs) && isTabsImplementationConfig(value.tabs))) &&
176
+ (value.stack === undefined ||
177
+ (isRecord(value.stack) && isStackImplementationConfig(value.stack)))
178
+ );
179
+ }
@@ -0,0 +1,292 @@
1
+ import {
2
+ CUSTOM_TABS_PRESENTATIONS,
3
+ DRAWER_POSITIONS,
4
+ DRAWER_TYPES,
5
+ FIXED_CUSTOM_TABS_PRESENTATIONS,
6
+ JAVASCRIPT_STACK_PRESENTATIONS,
7
+ JAVASCRIPT_TABS_PRESENTATIONS,
8
+ NATIVE_TABS_MINIMIZE_BEHAVIORS,
9
+ STACK_PRESENTATIONS,
10
+ } from '../navigator';
11
+ import { isOptionalBoolean, isOptionalString, isRecord } from './shared';
12
+
13
+ const CUSTOM_TABS_PRESENTATION_SET = new Set<string>(CUSTOM_TABS_PRESENTATIONS);
14
+ const DRAWER_POSITION_SET = new Set<string>(DRAWER_POSITIONS);
15
+ const DRAWER_TYPE_SET = new Set<string>(DRAWER_TYPES);
16
+ const FIXED_CUSTOM_TABS_PRESENTATION_SET = new Set<string>(FIXED_CUSTOM_TABS_PRESENTATIONS);
17
+ const JAVASCRIPT_STACK_PRESENTATION_SET = new Set<string>(JAVASCRIPT_STACK_PRESENTATIONS);
18
+ const JAVASCRIPT_TABS_PRESENTATION_SET = new Set<string>(JAVASCRIPT_TABS_PRESENTATIONS);
19
+ const NATIVE_TABS_MINIMIZE_BEHAVIOR_SET = new Set<string>(NATIVE_TABS_MINIMIZE_BEHAVIORS);
20
+ const STACK_PRESENTATION_SET = new Set<string>(STACK_PRESENTATIONS);
21
+
22
+ /*** Validate stable native, JavaScript, or alpha Experimental Stack desired state. */
23
+ export function isStackImplementationConfig(value: Record<string, unknown>): boolean {
24
+ if (
25
+ !hasNoDefinedKeys(value, [
26
+ 'native',
27
+ 'web',
28
+ 'presentation',
29
+ 'responsive',
30
+ 'customPresentationId',
31
+ 'minimizeBehavior',
32
+ 'bottomAccessory',
33
+ ])
34
+ ) {
35
+ return false;
36
+ }
37
+ if (value.implementation === undefined || value.implementation === 'native') {
38
+ return value.options === undefined || isStackScreenOptions(value.options);
39
+ }
40
+ if (value.implementation === 'javascript') {
41
+ return value.options === undefined || isJavaScriptStackScreenOptions(value.options);
42
+ }
43
+ return (
44
+ value.implementation === 'experimental' &&
45
+ (value.options === undefined || isStackHeaderOptions(value.options))
46
+ );
47
+ }
48
+
49
+ /*** Validate the serializable native-stack screen option subset. */
50
+ export function isStackScreenOptions(value: unknown): boolean {
51
+ if (
52
+ !isRecord(value) ||
53
+ !hasOnlyKeys(value, [
54
+ 'title',
55
+ 'headerShown',
56
+ 'headerTransparent',
57
+ 'headerBackVisible',
58
+ 'presentation',
59
+ 'sheetAllowedDetents',
60
+ 'sheetGrabberVisible',
61
+ ]) ||
62
+ !isStackHeaderOptionsShape(value) ||
63
+ (value.presentation !== undefined &&
64
+ (typeof value.presentation !== 'string' || !STACK_PRESENTATION_SET.has(value.presentation)))
65
+ ) {
66
+ return false;
67
+ }
68
+
69
+ if (value.presentation !== 'formSheet') {
70
+ return value.sheetAllowedDetents === undefined && value.sheetGrabberVisible === undefined;
71
+ }
72
+ return (
73
+ (value.sheetAllowedDetents === undefined || isSheetAllowedDetents(value.sheetAllowedDetents)) &&
74
+ isOptionalBoolean(value.sheetGrabberVisible)
75
+ );
76
+ }
77
+
78
+ /*** Validate the finite serializable Drawer option subset. */
79
+ export function isDrawerNavigatorOptions(value: unknown): boolean {
80
+ return (
81
+ isRecord(value) &&
82
+ hasOnlyKeys(value, ['drawerPosition', 'drawerType', 'swipeEnabled', 'headerShown']) &&
83
+ (value.drawerPosition === undefined ||
84
+ (typeof value.drawerPosition === 'string' &&
85
+ DRAWER_POSITION_SET.has(value.drawerPosition))) &&
86
+ (value.drawerType === undefined ||
87
+ (typeof value.drawerType === 'string' && DRAWER_TYPE_SET.has(value.drawerType))) &&
88
+ isOptionalBoolean(value.swipeEnabled) &&
89
+ isOptionalBoolean(value.headerShown)
90
+ );
91
+ }
92
+
93
+ /*** Validate tabs implementation desired state, with omitted implementation meaning adaptive. */
94
+ export function isTabsImplementationConfig(value: Record<string, unknown>): boolean {
95
+ if (value.implementation === undefined || value.implementation === 'adaptive') {
96
+ return isAdaptiveTabsConfig(value);
97
+ }
98
+ if (value.implementation === 'native') return isFullNativeTabsConfig(value);
99
+ if (value.implementation === 'javascript') return isJavaScriptTabsConfig(value);
100
+ return (
101
+ value.implementation === 'custom' &&
102
+ hasNoDefinedKeys(value, ['options', 'native', 'web', 'minimizeBehavior', 'bottomAccessory']) &&
103
+ isCustomTabsPresentationConfig(value)
104
+ );
105
+ }
106
+
107
+ /*** Validate a reference into the app-owned screen registry. */
108
+ export function isNavigatorScreenReference(value: unknown): boolean {
109
+ return isRecord(value) && hasOnlyKeys(value, ['screenId']) && typeof value.screenId === 'string';
110
+ }
111
+
112
+ /*** Check that a finite configuration object contains no unsupported keys. */
113
+ export function hasOnlyKeys(
114
+ value: Record<string, unknown>,
115
+ allowedKeys: readonly string[],
116
+ ): boolean {
117
+ const allowed = new Set(allowedKeys);
118
+ return Object.keys(value).every((key) => allowed.has(key));
119
+ }
120
+
121
+ /*** Validate JavaScript Stack options without accepting native-only presentations. */
122
+ function isJavaScriptStackScreenOptions(value: unknown): boolean {
123
+ return (
124
+ isRecord(value) &&
125
+ hasOnlyKeys(value, [
126
+ 'title',
127
+ 'headerShown',
128
+ 'headerTransparent',
129
+ 'headerBackVisible',
130
+ 'presentation',
131
+ ]) &&
132
+ isStackHeaderOptionsShape(value) &&
133
+ (value.presentation === undefined ||
134
+ (typeof value.presentation === 'string' &&
135
+ JAVASCRIPT_STACK_PRESENTATION_SET.has(value.presentation)))
136
+ );
137
+ }
138
+
139
+ /*** Validate the portable header subset supported by Experimental Stack. */
140
+ function isStackHeaderOptions(value: unknown): boolean {
141
+ return (
142
+ isRecord(value) &&
143
+ hasOnlyKeys(value, ['title', 'headerShown', 'headerTransparent', 'headerBackVisible']) &&
144
+ isStackHeaderOptionsShape(value)
145
+ );
146
+ }
147
+
148
+ /*** Validate shared stack header field values after branch-specific key filtering. */
149
+ function isStackHeaderOptionsShape(value: Record<string, unknown>): boolean {
150
+ return (
151
+ isOptionalString(value.title) &&
152
+ isOptionalBoolean(value.headerShown) &&
153
+ isOptionalBoolean(value.headerTransparent) &&
154
+ isOptionalBoolean(value.headerBackVisible)
155
+ );
156
+ }
157
+
158
+ /*** Validate sorted, finite native sheet detents or the fit-to-content sentinel. */
159
+ function isSheetAllowedDetents(value: unknown): boolean {
160
+ if (value === 'fitToContents') return true;
161
+ if (!Array.isArray(value) || value.length === 0) return false;
162
+ return value.every(
163
+ (detent, index) =>
164
+ typeof detent === 'number' &&
165
+ Number.isFinite(detent) &&
166
+ detent > 0 &&
167
+ detent <= 1 &&
168
+ (index === 0 || detent > value[index - 1]),
169
+ );
170
+ }
171
+
172
+ /*** Validate an adaptive tabs branch and reject implementation-specific conflicts. */
173
+ function isAdaptiveTabsConfig(value: Record<string, unknown>): boolean {
174
+ return (
175
+ hasNoDefinedKeys(value, [
176
+ 'options',
177
+ 'presentation',
178
+ 'responsive',
179
+ 'customPresentationId',
180
+ 'minimizeBehavior',
181
+ 'bottomAccessory',
182
+ ]) &&
183
+ (value.native === undefined || isNativeTabsConfig(value.native)) &&
184
+ (value.web === undefined || isCustomTabsWebConfig(value.web))
185
+ );
186
+ }
187
+
188
+ /*** Validate the top-level native tabs implementation. */
189
+ function isFullNativeTabsConfig(value: Record<string, unknown>): boolean {
190
+ return (
191
+ hasNoDefinedKeys(value, [
192
+ 'options',
193
+ 'native',
194
+ 'web',
195
+ 'presentation',
196
+ 'responsive',
197
+ 'customPresentationId',
198
+ ]) && isNativeTabsFields(value)
199
+ );
200
+ }
201
+
202
+ /*** Validate the top-level JavaScript tabs implementation. */
203
+ function isJavaScriptTabsConfig(value: Record<string, unknown>): boolean {
204
+ return (
205
+ hasNoDefinedKeys(value, [
206
+ 'options',
207
+ 'native',
208
+ 'web',
209
+ 'responsive',
210
+ 'customPresentationId',
211
+ 'minimizeBehavior',
212
+ 'bottomAccessory',
213
+ ]) &&
214
+ (value.presentation === undefined ||
215
+ (typeof value.presentation === 'string' &&
216
+ JAVASCRIPT_TABS_PRESENTATION_SET.has(value.presentation)))
217
+ );
218
+ }
219
+
220
+ /*** Validate the native branch used by adaptive tabs. */
221
+ function isNativeTabsConfig(value: unknown): boolean {
222
+ return (
223
+ isRecord(value) &&
224
+ hasOnlyKeys(value, ['implementation', 'minimizeBehavior', 'bottomAccessory']) &&
225
+ value.implementation === 'native' &&
226
+ isNativeTabsFields(value)
227
+ );
228
+ }
229
+
230
+ /*** Validate fields supported by the native tabs branch. */
231
+ function isNativeTabsFields(value: Record<string, unknown>): boolean {
232
+ return (
233
+ (value.minimizeBehavior === undefined ||
234
+ (typeof value.minimizeBehavior === 'string' &&
235
+ NATIVE_TABS_MINIMIZE_BEHAVIOR_SET.has(value.minimizeBehavior))) &&
236
+ (value.bottomAccessory === undefined || isNavigatorScreenReference(value.bottomAccessory))
237
+ );
238
+ }
239
+
240
+ /*** Validate the implementation-free custom-tabs Web branch inside adaptive tabs. */
241
+ function isCustomTabsWebConfig(value: unknown): boolean {
242
+ return (
243
+ isRecord(value) &&
244
+ hasOnlyKeys(value, ['presentation', 'responsive', 'customPresentationId']) &&
245
+ isCustomTabsPresentationConfig(value)
246
+ );
247
+ }
248
+
249
+ /*** Validate custom-tab presentation and its conditional serializable configuration. */
250
+ function isCustomTabsPresentationConfig(value: Record<string, unknown>): boolean {
251
+ if (
252
+ typeof value.presentation !== 'string' ||
253
+ !CUSTOM_TABS_PRESENTATION_SET.has(value.presentation)
254
+ ) {
255
+ return false;
256
+ }
257
+ if (value.responsive !== undefined && !isResponsiveTabsPresentation(value.responsive)) {
258
+ return false;
259
+ }
260
+ if (!isOptionalString(value.customPresentationId)) return false;
261
+ if (FIXED_CUSTOM_TABS_PRESENTATION_SET.has(value.presentation)) {
262
+ return value.responsive === undefined && value.customPresentationId === undefined;
263
+ }
264
+ if (value.presentation === 'responsive') {
265
+ return value.responsive !== undefined && value.customPresentationId === undefined;
266
+ }
267
+ return (
268
+ value.responsive === undefined &&
269
+ typeof value.customPresentationId === 'string' &&
270
+ value.customPresentationId.length > 0
271
+ );
272
+ }
273
+
274
+ /*** Validate semantic compact/medium/expanded custom-tab presentation mapping. */
275
+ function isResponsiveTabsPresentation(value: unknown): boolean {
276
+ return (
277
+ isRecord(value) &&
278
+ hasOnlyKeys(value, ['compact', 'medium', 'expanded']) &&
279
+ typeof value.compact === 'string' &&
280
+ FIXED_CUSTOM_TABS_PRESENTATION_SET.has(value.compact) &&
281
+ (value.medium === undefined ||
282
+ (typeof value.medium === 'string' && FIXED_CUSTOM_TABS_PRESENTATION_SET.has(value.medium))) &&
283
+ typeof value.expanded === 'string' &&
284
+ FIXED_CUSTOM_TABS_PRESENTATION_SET.has(value.expanded)
285
+ );
286
+ }
287
+
288
+ /*** Check that mutually exclusive branch fields are absent. */
289
+ function hasNoDefinedKeys(value: Record<string, unknown>, keys: readonly string[]): boolean {
290
+ const prohibited = new Set(keys);
291
+ return !Object.entries(value).some(([key, entry]) => prohibited.has(key) && entry !== undefined);
292
+ }