react-native-navigation 8.8.6 → 8.8.7

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 (126) hide show
  1. package/android/src/main/java/com/reactnativenavigation/NavigationApplication.java +3 -0
  2. package/android/src/main/java/com/reactnativenavigation/NavigationPackage.kt +27 -8
  3. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRow.kt +262 -0
  4. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowAttacher.kt +205 -0
  5. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowConfigStore.kt +32 -0
  6. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowLayout.kt +139 -0
  7. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowModule.kt +37 -0
  8. package/android/src/main/java/com/reactnativenavigation/customrow/BottomTabsCustomRowOptions.kt +68 -0
  9. package/android/src/main/java/com/reactnativenavigation/options/BottomTabOptions.java +4 -1
  10. package/android/src/main/java/com/reactnativenavigation/react/ReactView.java +13 -0
  11. package/android/src/main/java/com/reactnativenavigation/react/events/ComponentType.java +2 -1
  12. package/android/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabPresenter.java +28 -0
  13. package/android/src/main/java/com/reactnativenavigation/viewcontrollers/bottomtabs/BottomTabsController.java +59 -0
  14. package/android/src/main/java/com/reactnativenavigation/views/bottomtabs/BottomTabs.java +76 -0
  15. package/android/src/main/java/com/reactnativenavigation/views/bottomtabs/CustomBottomTabItemView.kt +73 -0
  16. package/android/src/main/java/com/reactnativenavigation/views/stack/topbar/titlebar/TitleBarReactButtonView.java +27 -11
  17. package/android/src/test/java/com/reactnativenavigation/views/TitleAndButtonsContainerTest.kt +15 -1
  18. package/android/src/test/java/com/reactnativenavigation/views/TitleBarReactButtonViewTest.java +135 -0
  19. package/ios/ARCHITECTURE.md +5 -0
  20. package/ios/BottomTabPresenter.h +7 -0
  21. package/ios/BottomTabPresenter.mm +27 -0
  22. package/ios/RNNAppDelegate.h +16 -0
  23. package/ios/RNNAppDelegate.mm +73 -0
  24. package/ios/RNNBottomTabOptions.h +2 -0
  25. package/ios/RNNBottomTabOptions.mm +5 -1
  26. package/ios/RNNBottomTabsController.h +2 -0
  27. package/ios/RNNBottomTabsController.mm +209 -1
  28. package/ios/RNNBottomTabsCustomRow.h +57 -0
  29. package/ios/RNNBottomTabsCustomRow.mm +252 -0
  30. package/ios/RNNBottomTabsCustomRowOptions.h +42 -0
  31. package/ios/RNNBottomTabsCustomRowOptions.mm +37 -0
  32. package/ios/RNNBottomTabsOptions.h +2 -0
  33. package/ios/RNNBottomTabsOptions.mm +2 -0
  34. package/ios/RNNComponentViewCreator.h +2 -1
  35. package/ios/RNNCustomTabBarItemView.h +26 -0
  36. package/ios/RNNCustomTabBarItemView.mm +83 -0
  37. package/ios/RNNReactRootViewCreator.mm +1 -0
  38. package/ios/RNNViewControllerFactory.mm +1 -0
  39. package/ios/ReactNativeNavigation.xcodeproj/project.pbxproj +24 -0
  40. package/lib/module/ARCHITECTURE.md +30 -0
  41. package/lib/module/Navigation.js +34 -1
  42. package/lib/module/Navigation.js.map +1 -1
  43. package/lib/module/NavigationDelegate.js +21 -0
  44. package/lib/module/NavigationDelegate.js.map +1 -1
  45. package/lib/module/adapters/AndroidCustomRowForwarder.js +75 -0
  46. package/lib/module/adapters/AndroidCustomRowForwarder.js.map +1 -0
  47. package/lib/module/commands/Commands.js +8 -0
  48. package/lib/module/commands/Commands.js.map +1 -1
  49. package/lib/module/index.js +1 -0
  50. package/lib/module/index.js.map +1 -1
  51. package/lib/module/interfaces/Options.js.map +1 -1
  52. package/lib/module/linking/DeferredLinkQueue.js +52 -0
  53. package/lib/module/linking/DeferredLinkQueue.js.map +1 -0
  54. package/lib/module/linking/DeferredLinkQueue.test.js +54 -0
  55. package/lib/module/linking/DeferredLinkQueue.test.js.map +1 -0
  56. package/lib/module/linking/LinkingHandler.js +139 -0
  57. package/lib/module/linking/LinkingHandler.js.map +1 -0
  58. package/lib/module/linking/LinkingHandler.test.js +384 -0
  59. package/lib/module/linking/LinkingHandler.test.js.map +1 -0
  60. package/lib/module/linking/ModalLayoutBuilder.js +56 -0
  61. package/lib/module/linking/ModalLayoutBuilder.js.map +1 -0
  62. package/lib/module/linking/ModalLayoutBuilder.test.js +154 -0
  63. package/lib/module/linking/ModalLayoutBuilder.test.js.map +1 -0
  64. package/lib/module/linking/RouteMatcher.js +104 -0
  65. package/lib/module/linking/RouteMatcher.js.map +1 -0
  66. package/lib/module/linking/RouteMatcher.test.js +164 -0
  67. package/lib/module/linking/RouteMatcher.test.js.map +1 -0
  68. package/lib/module/linking/URLParser.js +56 -0
  69. package/lib/module/linking/URLParser.js.map +1 -0
  70. package/lib/module/linking/URLParser.test.js +100 -0
  71. package/lib/module/linking/URLParser.test.js.map +1 -0
  72. package/lib/module/linking/types.js +4 -0
  73. package/lib/module/linking/types.js.map +1 -0
  74. package/lib/typescript/Navigation.d.ts +22 -0
  75. package/lib/typescript/Navigation.d.ts.map +1 -1
  76. package/lib/typescript/NavigationDelegate.d.ts +13 -0
  77. package/lib/typescript/NavigationDelegate.d.ts.map +1 -1
  78. package/lib/typescript/adapters/AndroidCustomRowForwarder.d.ts +23 -0
  79. package/lib/typescript/adapters/AndroidCustomRowForwarder.d.ts.map +1 -0
  80. package/lib/typescript/commands/Commands.d.ts +1 -0
  81. package/lib/typescript/commands/Commands.d.ts.map +1 -1
  82. package/lib/typescript/index.d.ts +1 -0
  83. package/lib/typescript/index.d.ts.map +1 -1
  84. package/lib/typescript/interfaces/Options.d.ts +85 -0
  85. package/lib/typescript/interfaces/Options.d.ts.map +1 -1
  86. package/lib/typescript/linking/DeferredLinkQueue.d.ts +26 -0
  87. package/lib/typescript/linking/DeferredLinkQueue.d.ts.map +1 -0
  88. package/lib/typescript/linking/DeferredLinkQueue.test.d.ts +2 -0
  89. package/lib/typescript/linking/DeferredLinkQueue.test.d.ts.map +1 -0
  90. package/lib/typescript/linking/LinkingHandler.d.ts +71 -0
  91. package/lib/typescript/linking/LinkingHandler.d.ts.map +1 -0
  92. package/lib/typescript/linking/LinkingHandler.test.d.ts +2 -0
  93. package/lib/typescript/linking/LinkingHandler.test.d.ts.map +1 -0
  94. package/lib/typescript/linking/ModalLayoutBuilder.d.ts +21 -0
  95. package/lib/typescript/linking/ModalLayoutBuilder.d.ts.map +1 -0
  96. package/lib/typescript/linking/ModalLayoutBuilder.test.d.ts +2 -0
  97. package/lib/typescript/linking/ModalLayoutBuilder.test.d.ts.map +1 -0
  98. package/lib/typescript/linking/RouteMatcher.d.ts +23 -0
  99. package/lib/typescript/linking/RouteMatcher.d.ts.map +1 -0
  100. package/lib/typescript/linking/RouteMatcher.test.d.ts +2 -0
  101. package/lib/typescript/linking/RouteMatcher.test.d.ts.map +1 -0
  102. package/lib/typescript/linking/URLParser.d.ts +16 -0
  103. package/lib/typescript/linking/URLParser.d.ts.map +1 -0
  104. package/lib/typescript/linking/URLParser.test.d.ts +2 -0
  105. package/lib/typescript/linking/URLParser.test.d.ts.map +1 -0
  106. package/lib/typescript/linking/types.d.ts +107 -0
  107. package/lib/typescript/linking/types.d.ts.map +1 -0
  108. package/package.json +1 -1
  109. package/src/ARCHITECTURE.md +30 -0
  110. package/src/Navigation.ts +36 -1
  111. package/src/NavigationDelegate.ts +22 -0
  112. package/src/adapters/AndroidCustomRowForwarder.ts +83 -0
  113. package/src/commands/Commands.ts +15 -0
  114. package/src/index.ts +1 -0
  115. package/src/interfaces/Options.ts +87 -0
  116. package/src/linking/DeferredLinkQueue.test.ts +60 -0
  117. package/src/linking/DeferredLinkQueue.ts +55 -0
  118. package/src/linking/LinkingHandler.test.ts +332 -0
  119. package/src/linking/LinkingHandler.ts +169 -0
  120. package/src/linking/ModalLayoutBuilder.test.ts +105 -0
  121. package/src/linking/ModalLayoutBuilder.ts +60 -0
  122. package/src/linking/RouteMatcher.test.ts +128 -0
  123. package/src/linking/RouteMatcher.ts +126 -0
  124. package/src/linking/URLParser.test.ts +105 -0
  125. package/src/linking/URLParser.ts +62 -0
  126. package/src/linking/types.ts +115 -0
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Props that React treats specially and must never be forwarded as
5
+ * regular component props. If a URL contains query parameters or path
6
+ * parameters using these names, they are dropped from `passProps`
7
+ * (with a dev-mode warning) to avoid crashing the screen.
8
+ */
9
+ const RESERVED_PROP_NAMES = new Set(['ref', 'key']);
10
+
11
+ /**
12
+ * Builds the default modal layout for a matched deep link.
13
+ *
14
+ * Behavior:
15
+ * - Wraps the matched chain in a `stack`. Even a single-component match
16
+ * gets wrapped so the screen has a topBar where the user can mount a
17
+ * close button via `topBar.leftButtons`.
18
+ * - Each segment becomes a `component` child of the stack, in match order.
19
+ * The first segment is the root of the stack; subsequent segments are
20
+ * pushed on top.
21
+ * - Path params for each segment are merged with query params and passed
22
+ * as `passProps` to that segment's component. Path params win on key
23
+ * collision. React-reserved keys (`ref`, `key`) are filtered out.
24
+ */
25
+ export class ModalLayoutBuilder {
26
+ build(match) {
27
+ return {
28
+ stack: {
29
+ children: match.path.map(segment => ({
30
+ component: {
31
+ name: segment.screen,
32
+ passProps: this.mergeProps(segment, match.queryParams)
33
+ }
34
+ }))
35
+ }
36
+ };
37
+ }
38
+ mergeProps(segment, queryParams) {
39
+ const merged = {
40
+ ...queryParams,
41
+ ...segment.params
42
+ };
43
+ const safe = {};
44
+ for (const key of Object.keys(merged)) {
45
+ if (RESERVED_PROP_NAMES.has(key)) {
46
+ if (__DEV__) {
47
+ console.warn(`[RNN linking] Dropping reserved prop "${key}" from passProps for screen "${segment.screen}". ` + `Rename the URL parameter to avoid conflicting with React-reserved names.`);
48
+ }
49
+ continue;
50
+ }
51
+ safe[key] = merged[key];
52
+ }
53
+ return safe;
54
+ }
55
+ }
56
+ //# sourceMappingURL=ModalLayoutBuilder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["RESERVED_PROP_NAMES","Set","ModalLayoutBuilder","build","match","stack","children","path","map","segment","component","name","screen","passProps","mergeProps","queryParams","merged","params","safe","key","Object","keys","has","__DEV__","console","warn"],"sourceRoot":"../../../src","sources":["linking/ModalLayoutBuilder.ts"],"mappings":";;AAGA;AACA;AACA;AACA;AACA;AACA;AACA,MAAMA,mBAAmB,GAAG,IAAIC,GAAG,CAAC,CAAC,KAAK,EAAE,KAAK,CAAC,CAAC;;AAEnD;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMC,kBAAkB,CAAC;EACvBC,KAAKA,CAACC,KAAiB,EAAU;IACtC,OAAO;MACLC,KAAK,EAAE;QACLC,QAAQ,EAAEF,KAAK,CAACG,IAAI,CAACC,GAAG,CAAEC,OAAO,KAAM;UACrCC,SAAS,EAAE;YACTC,IAAI,EAAEF,OAAO,CAACG,MAAM;YACpBC,SAAS,EAAE,IAAI,CAACC,UAAU,CAACL,OAAO,EAAEL,KAAK,CAACW,WAAW;UACvD;QACF,CAAC,CAAC;MACJ;IACF,CAAC;EACH;EAEQD,UAAUA,CAChBL,OAA0B,EAC1BM,WAAmC,EACX;IACxB,MAAMC,MAA8B,GAAG;MAAE,GAAGD,WAAW;MAAE,GAAGN,OAAO,CAACQ;IAAO,CAAC;IAC5E,MAAMC,IAA4B,GAAG,CAAC,CAAC;IACvC,KAAK,MAAMC,GAAG,IAAIC,MAAM,CAACC,IAAI,CAACL,MAAM,CAAC,EAAE;MACrC,IAAIhB,mBAAmB,CAACsB,GAAG,CAACH,GAAG,CAAC,EAAE;QAChC,IAAII,OAAO,EAAE;UACXC,OAAO,CAACC,IAAI,CACV,yCAAyCN,GAAG,gCAAgCV,OAAO,CAACG,MAAM,KAAK,GAC7F,0EACJ,CAAC;QACH;QACA;MACF;MACAM,IAAI,CAACC,GAAG,CAAC,GAAGH,MAAM,CAACG,GAAG,CAAC;IACzB;IACA,OAAOD,IAAI;EACb;AACF","ignoreList":[]}
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+
3
+ import { ModalLayoutBuilder } from "./ModalLayoutBuilder.js";
4
+ describe('ModalLayoutBuilder', () => {
5
+ let uut;
6
+ beforeEach(() => {
7
+ uut = new ModalLayoutBuilder();
8
+ });
9
+ it('wraps a single-segment match in a stack', () => {
10
+ const layout = uut.build({
11
+ url: 'myapp://home',
12
+ path: [{
13
+ screen: 'Home',
14
+ params: {}
15
+ }],
16
+ queryParams: {}
17
+ });
18
+ expect(layout).toEqual({
19
+ stack: {
20
+ children: [{
21
+ component: {
22
+ name: 'Home',
23
+ passProps: {}
24
+ }
25
+ }]
26
+ }
27
+ });
28
+ });
29
+ it('builds a push chain for multi-segment matches', () => {
30
+ const layout = uut.build({
31
+ url: 'myapp://settings/notifications',
32
+ path: [{
33
+ screen: 'Settings',
34
+ params: {}
35
+ }, {
36
+ screen: 'Notifications',
37
+ params: {}
38
+ }],
39
+ queryParams: {}
40
+ });
41
+ expect(layout.stack?.children).toEqual([{
42
+ component: {
43
+ name: 'Settings',
44
+ passProps: {}
45
+ }
46
+ }, {
47
+ component: {
48
+ name: 'Notifications',
49
+ passProps: {}
50
+ }
51
+ }]);
52
+ });
53
+ it('passes path params as passProps', () => {
54
+ const layout = uut.build({
55
+ url: 'myapp://user/42',
56
+ path: [{
57
+ screen: 'Profile',
58
+ params: {
59
+ id: '42'
60
+ }
61
+ }],
62
+ queryParams: {}
63
+ });
64
+ expect(layout.stack?.children?.[0].component?.passProps).toEqual({
65
+ id: '42'
66
+ });
67
+ });
68
+ it('merges query params into every segment, with path params taking precedence', () => {
69
+ const layout = uut.build({
70
+ url: 'myapp://user/42?id=99&source=push',
71
+ path: [{
72
+ screen: 'Profile',
73
+ params: {
74
+ id: '42'
75
+ }
76
+ }],
77
+ queryParams: {
78
+ id: '99',
79
+ source: 'push'
80
+ }
81
+ });
82
+ expect(layout.stack?.children?.[0].component?.passProps).toEqual({
83
+ id: '42',
84
+ source: 'push'
85
+ });
86
+ });
87
+ it('applies query params to every segment of a nested match', () => {
88
+ const layout = uut.build({
89
+ url: 'myapp://settings/notifications?source=push',
90
+ path: [{
91
+ screen: 'Settings',
92
+ params: {}
93
+ }, {
94
+ screen: 'Notifications',
95
+ params: {}
96
+ }],
97
+ queryParams: {
98
+ source: 'push'
99
+ }
100
+ });
101
+ expect(layout.stack?.children?.[0].component?.passProps).toEqual({
102
+ source: 'push'
103
+ });
104
+ expect(layout.stack?.children?.[1].component?.passProps).toEqual({
105
+ source: 'push'
106
+ });
107
+ });
108
+ it('filters the React-reserved "ref" query param from passProps', () => {
109
+ const layout = uut.build({
110
+ url: 'myapp://user/42?ref=notification&utm=push',
111
+ path: [{
112
+ screen: 'Profile',
113
+ params: {
114
+ id: '42'
115
+ }
116
+ }],
117
+ queryParams: {
118
+ ref: 'notification',
119
+ utm: 'push'
120
+ }
121
+ });
122
+ expect(layout.stack?.children?.[0].component?.passProps).toEqual({
123
+ id: '42',
124
+ utm: 'push'
125
+ });
126
+ });
127
+ it('filters the React-reserved "key" query param from passProps', () => {
128
+ const layout = uut.build({
129
+ url: 'myapp://home?key=abc',
130
+ path: [{
131
+ screen: 'Home',
132
+ params: {}
133
+ }],
134
+ queryParams: {
135
+ key: 'abc'
136
+ }
137
+ });
138
+ expect(layout.stack?.children?.[0].component?.passProps).toEqual({});
139
+ });
140
+ it('filters reserved keys when they appear as path params too', () => {
141
+ const layout = uut.build({
142
+ url: 'myapp://r/abc',
143
+ path: [{
144
+ screen: 'Home',
145
+ params: {
146
+ ref: 'abc'
147
+ }
148
+ }],
149
+ queryParams: {}
150
+ });
151
+ expect(layout.stack?.children?.[0].component?.passProps).toEqual({});
152
+ });
153
+ });
154
+ //# sourceMappingURL=ModalLayoutBuilder.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["ModalLayoutBuilder","describe","uut","beforeEach","it","layout","build","url","path","screen","params","queryParams","expect","toEqual","stack","children","component","name","passProps","id","source","ref","utm","key"],"sourceRoot":"../../../src","sources":["linking/ModalLayoutBuilder.test.ts"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Compiles a `screens` config into a tree of `RouteNode`s and resolves
5
+ * parsed paths to a matched `RouteMatch`.
6
+ *
7
+ * Matching is depth-first, in insertion order. The first node whose pattern
8
+ * (and nested children) consume the entire path wins.
9
+ */
10
+ export class RouteMatcher {
11
+ routeTree = null;
12
+ buildRouteTree(screens) {
13
+ const nodes = [];
14
+ for (const [screenName, config] of Object.entries(screens)) {
15
+ nodes.push(this.buildNode(screenName, config));
16
+ }
17
+ return nodes;
18
+ }
19
+ setRouteTree(tree) {
20
+ this.routeTree = tree;
21
+ }
22
+
23
+ /**
24
+ * Match a parsed path (e.g. `"user/42"`) against the configured tree.
25
+ * Returns the matched chain or `null` when no route matches.
26
+ */
27
+ match(path, url, queryParams) {
28
+ if (!this.routeTree) return null;
29
+ const pathSegments = path.split('/').filter(s => s.length > 0);
30
+ const result = this.matchRecursive(pathSegments, 0, this.routeTree);
31
+ if (!result) return null;
32
+ return {
33
+ url,
34
+ path: result,
35
+ queryParams
36
+ };
37
+ }
38
+ buildNode(screenName, config) {
39
+ if (typeof config === 'string') {
40
+ return {
41
+ screen: screenName,
42
+ pattern: config,
43
+ segments: this.splitPattern(config),
44
+ children: []
45
+ };
46
+ }
47
+ const children = config.screens ? this.buildRouteTree(config.screens) : [];
48
+ return {
49
+ screen: screenName,
50
+ pattern: config.path ?? null,
51
+ segments: config.path ? this.splitPattern(config.path) : [],
52
+ children
53
+ };
54
+ }
55
+ splitPattern(pattern) {
56
+ return pattern.split('/').filter(s => s.length > 0);
57
+ }
58
+ matchRecursive(pathSegments, offset, nodes) {
59
+ for (const node of nodes) {
60
+ const result = this.tryMatchNode(pathSegments, offset, node);
61
+ if (result) return result;
62
+ }
63
+ return null;
64
+ }
65
+ tryMatchNode(pathSegments, offset, node) {
66
+ if (node.pattern === null && node.segments.length === 0) {
67
+ if (node.children.length === 0) return null;
68
+ const childMatch = this.matchRecursive(pathSegments, offset, node.children);
69
+ if (!childMatch) return null;
70
+ return [{
71
+ screen: node.screen,
72
+ params: {}
73
+ }, ...childMatch];
74
+ }
75
+ const patternSegments = node.segments;
76
+ if (offset + patternSegments.length > pathSegments.length) {
77
+ return null;
78
+ }
79
+ const params = {};
80
+ for (let i = 0; i < patternSegments.length; i++) {
81
+ const pattern = patternSegments[i];
82
+ const actual = pathSegments[offset + i];
83
+ if (pattern.startsWith(':')) {
84
+ params[pattern.slice(1)] = actual;
85
+ } else if (pattern !== actual) {
86
+ return null;
87
+ }
88
+ }
89
+ const newOffset = offset + patternSegments.length;
90
+ const segment = {
91
+ screen: node.screen,
92
+ params
93
+ };
94
+ if (newOffset === pathSegments.length) {
95
+ return [segment];
96
+ }
97
+ if (node.children.length > 0) {
98
+ const childMatch = this.matchRecursive(pathSegments, newOffset, node.children);
99
+ if (childMatch) return [segment, ...childMatch];
100
+ }
101
+ return null;
102
+ }
103
+ }
104
+ //# sourceMappingURL=RouteMatcher.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["RouteMatcher","routeTree","buildRouteTree","screens","nodes","screenName","config","Object","entries","push","buildNode","setRouteTree","tree","match","path","url","queryParams","pathSegments","split","filter","s","length","result","matchRecursive","screen","pattern","segments","splitPattern","children","offset","node","tryMatchNode","childMatch","params","patternSegments","i","actual","startsWith","slice","newOffset","segment"],"sourceRoot":"../../../src","sources":["linking/RouteMatcher.ts"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,164 @@
1
+ "use strict";
2
+
3
+ import { RouteMatcher } from "./RouteMatcher.js";
4
+ describe('RouteMatcher', () => {
5
+ let uut;
6
+ beforeEach(() => {
7
+ uut = new RouteMatcher();
8
+ });
9
+ function configure(screens) {
10
+ uut.setRouteTree(uut.buildRouteTree(screens));
11
+ }
12
+ it('matches a flat route', () => {
13
+ configure({
14
+ Home: 'home'
15
+ });
16
+ expect(uut.match('home', 'myapp://home', {})).toEqual({
17
+ url: 'myapp://home',
18
+ path: [{
19
+ screen: 'Home',
20
+ params: {}
21
+ }],
22
+ queryParams: {}
23
+ });
24
+ });
25
+ it('extracts a single path parameter', () => {
26
+ configure({
27
+ Profile: 'user/:id'
28
+ });
29
+ expect(uut.match('user/42', 'myapp://user/42', {})).toEqual({
30
+ url: 'myapp://user/42',
31
+ path: [{
32
+ screen: 'Profile',
33
+ params: {
34
+ id: '42'
35
+ }
36
+ }],
37
+ queryParams: {}
38
+ });
39
+ });
40
+ it('extracts multiple path parameters', () => {
41
+ configure({
42
+ Post: 'user/:userId/post/:postId'
43
+ });
44
+ const result = uut.match('user/5/post/99', 'myapp://user/5/post/99', {});
45
+ expect(result?.path[0].params).toEqual({
46
+ userId: '5',
47
+ postId: '99'
48
+ });
49
+ });
50
+ it('returns null when no route matches', () => {
51
+ configure({
52
+ Home: 'home'
53
+ });
54
+ expect(uut.match('unknown', 'myapp://unknown', {})).toBeNull();
55
+ });
56
+ it('returns null when path is longer than any pattern', () => {
57
+ configure({
58
+ Home: 'home'
59
+ });
60
+ expect(uut.match('home/extra', 'myapp://home/extra', {})).toBeNull();
61
+ });
62
+ it('matches the first registered route on ambiguity', () => {
63
+ configure({
64
+ Profile: 'user/:id',
65
+ User: 'user/:userId'
66
+ });
67
+ const result = uut.match('user/42', 'myapp://user/42', {});
68
+ expect(result?.path[0].screen).toBe('Profile');
69
+ });
70
+ it('matches a nested route as a chain', () => {
71
+ configure({
72
+ Settings: {
73
+ path: 'settings',
74
+ screens: {
75
+ Notifications: 'notifications'
76
+ }
77
+ }
78
+ });
79
+ expect(uut.match('settings/notifications', 'myapp://settings/notifications', {})).toEqual({
80
+ url: 'myapp://settings/notifications',
81
+ path: [{
82
+ screen: 'Settings',
83
+ params: {}
84
+ }, {
85
+ screen: 'Notifications',
86
+ params: {}
87
+ }],
88
+ queryParams: {}
89
+ });
90
+ });
91
+ it('matches a parent-only nested route', () => {
92
+ configure({
93
+ Settings: {
94
+ path: 'settings',
95
+ screens: {
96
+ Notifications: 'notifications'
97
+ }
98
+ }
99
+ });
100
+ expect(uut.match('settings', 'myapp://settings', {})).toEqual({
101
+ url: 'myapp://settings',
102
+ path: [{
103
+ screen: 'Settings',
104
+ params: {}
105
+ }],
106
+ queryParams: {}
107
+ });
108
+ });
109
+ it('supports grouping nodes that consume no segments', () => {
110
+ configure({
111
+ Main: {
112
+ screens: {
113
+ Feed: 'feed',
114
+ Search: 'search'
115
+ }
116
+ }
117
+ });
118
+ const result = uut.match('feed', 'myapp://feed', {});
119
+ expect(result?.path).toEqual([{
120
+ screen: 'Main',
121
+ params: {}
122
+ }, {
123
+ screen: 'Feed',
124
+ params: {}
125
+ }]);
126
+ });
127
+ it('passes query params through to the match result', () => {
128
+ configure({
129
+ Home: 'home'
130
+ });
131
+ const result = uut.match('home', 'myapp://home?ref=push', {
132
+ ref: 'push'
133
+ });
134
+ expect(result?.queryParams).toEqual({
135
+ ref: 'push'
136
+ });
137
+ });
138
+ it('returns null when no route tree has been configured', () => {
139
+ expect(uut.match('home', 'myapp://home', {})).toBeNull();
140
+ });
141
+ it('matches nested params at multiple levels', () => {
142
+ configure({
143
+ Org: {
144
+ path: 'org/:orgId',
145
+ screens: {
146
+ Project: 'project/:projectId'
147
+ }
148
+ }
149
+ });
150
+ const result = uut.match('org/1/project/2', 'myapp://org/1/project/2', {});
151
+ expect(result?.path).toEqual([{
152
+ screen: 'Org',
153
+ params: {
154
+ orgId: '1'
155
+ }
156
+ }, {
157
+ screen: 'Project',
158
+ params: {
159
+ projectId: '2'
160
+ }
161
+ }]);
162
+ });
163
+ });
164
+ //# sourceMappingURL=RouteMatcher.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["RouteMatcher","describe","uut","beforeEach","configure","screens","setRouteTree","buildRouteTree","it","Home","expect","match","toEqual","url","path","screen","params","queryParams","Profile","id","Post","result","userId","postId","toBeNull","User","toBe","Settings","Notifications","Main","Feed","Search","ref","Org","Project","orgId","projectId"],"sourceRoot":"../../../src","sources":["linking/RouteMatcher.test.ts"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,56 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Parses a URL string against a list of configured prefixes.
5
+ *
6
+ * Responsibilities:
7
+ * - Strip the longest matching prefix (or report no match).
8
+ * - Drop any `#fragment` portion.
9
+ * - Decode each path segment so literal patterns compare against decoded
10
+ * text (e.g. `hello%20world` matches a `hello world` pattern segment).
11
+ * - Parse the query string into a plain key/value map.
12
+ */
13
+ export class URLParser {
14
+ stripPrefix(url, prefixes) {
15
+ const sorted = [...prefixes].sort((a, b) => b.length - a.length);
16
+ for (const prefix of sorted) {
17
+ if (url.startsWith(prefix)) {
18
+ return url.slice(prefix.length);
19
+ }
20
+ }
21
+ return null;
22
+ }
23
+ parse(url, prefixes) {
24
+ const stripped = this.stripPrefix(url, prefixes);
25
+ if (stripped === null) {
26
+ return null;
27
+ }
28
+ const withoutFragment = stripped.split('#', 1)[0];
29
+ const [pathPart, queryPart] = withoutFragment.split('?', 2);
30
+ const path = pathPart.split('/').filter(s => s.length > 0).map(segment => safeDecode(segment)).join('/');
31
+ const queryParams = {};
32
+ if (queryPart) {
33
+ for (const pair of queryPart.split('&')) {
34
+ if (!pair) continue;
35
+ const eqIndex = pair.indexOf('=');
36
+ const rawKey = eqIndex === -1 ? pair : pair.slice(0, eqIndex);
37
+ const rawValue = eqIndex === -1 ? '' : pair.slice(eqIndex + 1);
38
+ const key = safeDecode(rawKey);
39
+ if (!key) continue;
40
+ queryParams[key] = safeDecode(rawValue);
41
+ }
42
+ }
43
+ return {
44
+ path,
45
+ queryParams
46
+ };
47
+ }
48
+ }
49
+ function safeDecode(value) {
50
+ try {
51
+ return decodeURIComponent(value);
52
+ } catch {
53
+ return value;
54
+ }
55
+ }
56
+ //# sourceMappingURL=URLParser.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["URLParser","stripPrefix","url","prefixes","sorted","sort","a","b","length","prefix","startsWith","slice","parse","stripped","withoutFragment","split","pathPart","queryPart","path","filter","s","map","segment","safeDecode","join","queryParams","pair","eqIndex","indexOf","rawKey","rawValue","key","value","decodeURIComponent"],"sourceRoot":"../../../src","sources":["linking/URLParser.ts"],"mappings":";;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAMA,SAAS,CAAC;EACdC,WAAWA,CAACC,GAAW,EAAEC,QAAkB,EAAiB;IACjE,MAAMC,MAAM,GAAG,CAAC,GAAGD,QAAQ,CAAC,CAACE,IAAI,CAAC,CAACC,CAAC,EAAEC,CAAC,KAAKA,CAAC,CAACC,MAAM,GAAGF,CAAC,CAACE,MAAM,CAAC;IAChE,KAAK,MAAMC,MAAM,IAAIL,MAAM,EAAE;MAC3B,IAAIF,GAAG,CAACQ,UAAU,CAACD,MAAM,CAAC,EAAE;QAC1B,OAAOP,GAAG,CAACS,KAAK,CAACF,MAAM,CAACD,MAAM,CAAC;MACjC;IACF;IACA,OAAO,IAAI;EACb;EAEOI,KAAKA,CAACV,GAAW,EAAEC,QAAkB,EAAoB;IAC9D,MAAMU,QAAQ,GAAG,IAAI,CAACZ,WAAW,CAACC,GAAG,EAAEC,QAAQ,CAAC;IAChD,IAAIU,QAAQ,KAAK,IAAI,EAAE;MACrB,OAAO,IAAI;IACb;IAEA,MAAMC,eAAe,GAAGD,QAAQ,CAACE,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;IACjD,MAAM,CAACC,QAAQ,EAAEC,SAAS,CAAC,GAAGH,eAAe,CAACC,KAAK,CAAC,GAAG,EAAE,CAAC,CAAC;IAE3D,MAAMG,IAAI,GAAGF,QAAQ,CAClBD,KAAK,CAAC,GAAG,CAAC,CACVI,MAAM,CAAEC,CAAC,IAAKA,CAAC,CAACZ,MAAM,GAAG,CAAC,CAAC,CAC3Ba,GAAG,CAAEC,OAAO,IAAKC,UAAU,CAACD,OAAO,CAAC,CAAC,CACrCE,IAAI,CAAC,GAAG,CAAC;IAEZ,MAAMC,WAAmC,GAAG,CAAC,CAAC;IAC9C,IAAIR,SAAS,EAAE;MACb,KAAK,MAAMS,IAAI,IAAIT,SAAS,CAACF,KAAK,CAAC,GAAG,CAAC,EAAE;QACvC,IAAI,CAACW,IAAI,EAAE;QACX,MAAMC,OAAO,GAAGD,IAAI,CAACE,OAAO,CAAC,GAAG,CAAC;QACjC,MAAMC,MAAM,GAAGF,OAAO,KAAK,CAAC,CAAC,GAAGD,IAAI,GAAGA,IAAI,CAACf,KAAK,CAAC,CAAC,EAAEgB,OAAO,CAAC;QAC7D,MAAMG,QAAQ,GAAGH,OAAO,KAAK,CAAC,CAAC,GAAG,EAAE,GAAGD,IAAI,CAACf,KAAK,CAACgB,OAAO,GAAG,CAAC,CAAC;QAC9D,MAAMI,GAAG,GAAGR,UAAU,CAACM,MAAM,CAAC;QAC9B,IAAI,CAACE,GAAG,EAAE;QACVN,WAAW,CAACM,GAAG,CAAC,GAAGR,UAAU,CAACO,QAAQ,CAAC;MACzC;IACF;IAEA,OAAO;MAAEZ,IAAI;MAAEO;IAAY,CAAC;EAC9B;AACF;AAEA,SAASF,UAAUA,CAACS,KAAa,EAAU;EACzC,IAAI;IACF,OAAOC,kBAAkB,CAACD,KAAK,CAAC;EAClC,CAAC,CAAC,MAAM;IACN,OAAOA,KAAK;EACd;AACF","ignoreList":[]}
@@ -0,0 +1,100 @@
1
+ "use strict";
2
+
3
+ import { URLParser } from "./URLParser.js";
4
+ describe('URLParser', () => {
5
+ let uut;
6
+ beforeEach(() => {
7
+ uut = new URLParser();
8
+ });
9
+ describe('stripPrefix', () => {
10
+ it('returns the remainder after the matching prefix', () => {
11
+ expect(uut.stripPrefix('myapp://home', ['myapp://'])).toBe('home');
12
+ });
13
+ it('returns null when no prefix matches', () => {
14
+ expect(uut.stripPrefix('other://home', ['myapp://'])).toBeNull();
15
+ });
16
+ it('chooses the longest matching prefix', () => {
17
+ const result = uut.stripPrefix('https://myapp.com/v2/home', ['https://myapp.com', 'https://myapp.com/v2']);
18
+ expect(result).toBe('/home');
19
+ });
20
+ });
21
+ describe('parse', () => {
22
+ const prefixes = ['myapp://', 'https://myapp.com'];
23
+ it('returns null when no prefix matches', () => {
24
+ expect(uut.parse('other://home', prefixes)).toBeNull();
25
+ });
26
+ it('parses a simple path', () => {
27
+ expect(uut.parse('myapp://home', prefixes)).toEqual({
28
+ path: 'home',
29
+ queryParams: {}
30
+ });
31
+ });
32
+ it('trims leading and trailing slashes', () => {
33
+ expect(uut.parse('myapp:///home/', prefixes)).toEqual({
34
+ path: 'home',
35
+ queryParams: {}
36
+ });
37
+ });
38
+ it('parses query parameters', () => {
39
+ expect(uut.parse('myapp://search?q=hello&page=2', prefixes)).toEqual({
40
+ path: 'search',
41
+ queryParams: {
42
+ q: 'hello',
43
+ page: '2'
44
+ }
45
+ });
46
+ });
47
+ it('decodes URL-encoded path segments', () => {
48
+ expect(uut.parse('myapp://hello%20world', prefixes)).toEqual({
49
+ path: 'hello world',
50
+ queryParams: {}
51
+ });
52
+ });
53
+ it('decodes URL-encoded query parameter keys and values', () => {
54
+ expect(uut.parse('myapp://search?q=hello%20world&a%20b=c', prefixes)).toEqual({
55
+ path: 'search',
56
+ queryParams: {
57
+ q: 'hello world',
58
+ 'a b': 'c'
59
+ }
60
+ });
61
+ });
62
+ it('strips fragments before parsing the query', () => {
63
+ expect(uut.parse('myapp://home?x=1#section', prefixes)).toEqual({
64
+ path: 'home',
65
+ queryParams: {
66
+ x: '1'
67
+ }
68
+ });
69
+ });
70
+ it('strips fragments when there is no query string', () => {
71
+ expect(uut.parse('myapp://home#section', prefixes)).toEqual({
72
+ path: 'home',
73
+ queryParams: {}
74
+ });
75
+ });
76
+ it('treats query keys without values as empty strings', () => {
77
+ expect(uut.parse('myapp://home?flag', prefixes)).toEqual({
78
+ path: 'home',
79
+ queryParams: {
80
+ flag: ''
81
+ }
82
+ });
83
+ });
84
+ it('handles malformed encoding without throwing', () => {
85
+ expect(uut.parse('myapp://%E0%A4%A', prefixes)).toEqual({
86
+ path: '%E0%A4%A',
87
+ queryParams: {}
88
+ });
89
+ });
90
+ it('keeps "=" inside values intact', () => {
91
+ expect(uut.parse('myapp://search?token=a=b=c', prefixes)).toEqual({
92
+ path: 'search',
93
+ queryParams: {
94
+ token: 'a=b=c'
95
+ }
96
+ });
97
+ });
98
+ });
99
+ });
100
+ //# sourceMappingURL=URLParser.test.js.map