@hubspot/ui-extensions 0.12.3 → 0.12.4

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 (64) hide show
  1. package/dist/__tests__/crm/utils/fetchAssociations.spec.js +2 -1
  2. package/dist/crm/hooks/useAssociations.d.ts +2 -10
  3. package/dist/crm/hooks/useAssociations.js +2 -1
  4. package/dist/crm/utils/fetchAssociations.d.ts +0 -8
  5. package/dist/crm/utils/fetchAssociations.js +0 -10
  6. package/dist/experimental/pages/components/index.d.ts +1 -0
  7. package/dist/experimental/pages/components/index.js +1 -0
  8. package/dist/experimental/pages/components/page-routes.d.ts +83 -0
  9. package/dist/experimental/pages/components/page-routes.js +66 -0
  10. package/dist/experimental/pages/create-page-router.d.ts +35 -0
  11. package/dist/experimental/pages/create-page-router.js +123 -0
  12. package/dist/experimental/pages/create-page-router.test.d.ts +1 -0
  13. package/dist/experimental/pages/create-page-router.test.js +296 -0
  14. package/dist/experimental/pages/hooks.d.ts +8 -0
  15. package/dist/experimental/pages/hooks.js +15 -0
  16. package/dist/experimental/pages/index.d.ts +5 -3
  17. package/dist/experimental/pages/index.js +4 -3
  18. package/dist/experimental/pages/internal/app-page-route-context.d.ts +16 -0
  19. package/dist/experimental/pages/internal/app-page-route-context.js +12 -0
  20. package/dist/experimental/pages/internal/convert-page-routes-react-elements.d.ts +9 -0
  21. package/dist/experimental/pages/internal/convert-page-routes-react-elements.js +138 -0
  22. package/dist/experimental/pages/internal/page-router-internal-types.d.ts +40 -0
  23. package/dist/experimental/pages/internal/page-router-internal-types.js +5 -0
  24. package/dist/experimental/pages/internal/trie-router.d.ts +31 -0
  25. package/dist/experimental/pages/internal/trie-router.js +141 -0
  26. package/dist/experimental/pages/internal/trie-router.test.d.ts +1 -0
  27. package/dist/experimental/pages/internal/trie-router.test.js +263 -0
  28. package/dist/experimental/pages/internal/useAppPageLocation.d.ts +1 -0
  29. package/dist/experimental/pages/internal/useAppPageLocation.js +13 -0
  30. package/dist/experimental/pages/types.d.ts +29 -0
  31. package/dist/experimental/pages/types.js +1 -0
  32. package/dist/internal/hook-utils.d.ts +7 -6
  33. package/dist/internal/hook-utils.js +8 -9
  34. package/dist/pages/index.d.ts +1 -0
  35. package/dist/pages/index.js +1 -0
  36. package/dist/shared/remoteComponents.d.ts +24 -0
  37. package/dist/shared/remoteComponents.js +28 -0
  38. package/dist/shared/types/components/button.d.ts +2 -2
  39. package/dist/shared/types/components/image.d.ts +2 -2
  40. package/dist/shared/types/components/inputs.d.ts +7 -1
  41. package/dist/shared/types/components/link.d.ts +2 -2
  42. package/dist/shared/types/pages/components/index.d.ts +3 -1
  43. package/dist/shared/types/pages/components/page-breadcrumbs.d.ts +34 -0
  44. package/dist/shared/types/pages/components/page-breadcrumbs.js +1 -0
  45. package/dist/shared/types/pages/components/page-header.d.ts +82 -0
  46. package/dist/shared/types/pages/components/page-header.js +1 -0
  47. package/dist/shared/types/pages/components/page-link.d.ts +4 -2
  48. package/dist/shared/types/pages/components/page-title.d.ts +12 -0
  49. package/dist/shared/types/pages/components/page-title.js +1 -0
  50. package/dist/shared/types/pages/index.d.ts +1 -0
  51. package/dist/shared/types/pages/index.js +1 -0
  52. package/dist/shared/types/pages.d.ts +1 -0
  53. package/dist/shared/types/pages.js +1 -0
  54. package/dist/shared/types/worker-globals.d.ts +3 -12
  55. package/dist/testing/internal/mocks/index.d.ts +14 -6
  56. package/dist/testing/internal/mocks/index.js +19 -5
  57. package/dist/testing/internal/mocks/mock-app-page-location.d.ts +7 -0
  58. package/dist/testing/internal/mocks/mock-app-page-location.js +35 -0
  59. package/dist/testing/internal/mocks/mock-hooks.js +12 -7
  60. package/dist/testing/render.js +7 -6
  61. package/dist/testing/types.d.ts +19 -3
  62. package/dist/utils/pagination.d.ts +17 -0
  63. package/dist/utils/pagination.js +10 -0
  64. package/package.json +1 -1
@@ -0,0 +1,296 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { describe, expect, it } from 'vitest';
3
+ import { Box, Text } from "../../index.js";
4
+ import { createRenderer } from "../../testing/index.js";
5
+ import { PageRoutes } from "./components/page-routes.js";
6
+ import { createPageRouter } from "./create-page-router.js";
7
+ import { usePageRoute } from "./hooks.js";
8
+ const HomePage = () => _jsx(Text, { testId: "page", children: "home" });
9
+ const DocsPage = () => _jsx(Text, { testId: "page", children: "docs" });
10
+ const SupportIndexPage = () => _jsx(Text, { testId: "page", children: "support-index" });
11
+ const ContactUsPage = () => _jsx(Text, { testId: "page", children: "contact-us" });
12
+ const FAQPage = () => _jsx(Text, { testId: "page", children: "faq" });
13
+ const NotFoundPage = () => _jsx(Text, { testId: "page", children: "not-found-custom" });
14
+ const RouteInfoDisplay = () => {
15
+ const { routeId, path, params } = usePageRoute();
16
+ return (_jsxs(_Fragment, { children: [_jsx(Text, { testId: "route-id", children: routeId ?? '' }), _jsx(Text, { testId: "route-path", children: path }), _jsx(Text, { testId: "route-params", children: JSON.stringify(params) })] }));
17
+ };
18
+ const ContactDetailsPage = () => (_jsxs(_Fragment, { children: [_jsx(Text, { testId: "page", children: "contact-details" }), _jsx(RouteInfoDisplay, {})] }));
19
+ describe('createPageRouter', () => {
20
+ describe('basic routing', () => {
21
+ it('renders the index route at the default location', () => {
22
+ const { render, findByTestId } = createRenderer('home');
23
+ const PageRouter = createPageRouter(_jsxs(PageRoutes, { children: [_jsx(PageRoutes.IndexRoute, { component: HomePage }), _jsx(PageRoutes.Route, { path: "/docs", component: DocsPage })] }));
24
+ render(_jsx(PageRouter, {}));
25
+ expect(findByTestId(Text, 'page').text).toEqual('home');
26
+ });
27
+ it('renders a named route when path matches', () => {
28
+ const { mocks, render, findByTestId } = createRenderer('home');
29
+ mocks.setPageLocation({ path: '/docs', params: {} });
30
+ const PageRouter = createPageRouter(_jsxs(PageRoutes, { children: [_jsx(PageRoutes.IndexRoute, { component: HomePage }), _jsx(PageRoutes.Route, { path: "/docs", component: DocsPage })] }));
31
+ render(_jsx(PageRouter, {}));
32
+ expect(findByTestId(Text, 'page').text).toEqual('docs');
33
+ });
34
+ it('shows "Page not found" when no route matches', () => {
35
+ const { mocks, render, find } = createRenderer('home');
36
+ mocks.setPageLocation({ path: '/unknown', params: {} });
37
+ const PageRouter = createPageRouter(_jsx(PageRoutes, { children: _jsx(PageRoutes.IndexRoute, { component: HomePage }) }));
38
+ render(_jsx(PageRouter, {}));
39
+ expect(find(Text, (node) => node.text === 'This app page does not exist.')).toBeTruthy();
40
+ });
41
+ });
42
+ describe('route exclusivity', () => {
43
+ it('renders only the index route and not other routes at the default location', () => {
44
+ const { render, findAll, findByTestId, maybeFind } = createRenderer('home');
45
+ const PageRouter = createPageRouter(_jsxs(PageRoutes, { children: [_jsx(PageRoutes.IndexRoute, { component: HomePage }), _jsx(PageRoutes.Route, { path: "/docs", component: DocsPage }), _jsx(PageRoutes.Route, { path: "/support", component: SupportIndexPage })] }));
46
+ render(_jsx(PageRouter, {}));
47
+ expect(findAll(Text, { testId: 'page' })).toHaveLength(1);
48
+ expect(findByTestId(Text, 'page').text).toEqual('home');
49
+ expect(maybeFind(Text, (node) => node.text === 'docs')).toBeNull();
50
+ expect(maybeFind(Text, (node) => node.text === 'support-index')).toBeNull();
51
+ });
52
+ it('renders only the matched named route and not the index or other routes', () => {
53
+ const { mocks, render, findAll, findByTestId, maybeFind } = createRenderer('home');
54
+ mocks.setPageLocation({ path: '/docs', params: {} });
55
+ const PageRouter = createPageRouter(_jsxs(PageRoutes, { children: [_jsx(PageRoutes.IndexRoute, { component: HomePage }), _jsx(PageRoutes.Route, { path: "/docs", component: DocsPage }), _jsx(PageRoutes.Route, { path: "/support", component: SupportIndexPage })] }));
56
+ render(_jsx(PageRouter, {}));
57
+ expect(findAll(Text, { testId: 'page' })).toHaveLength(1);
58
+ expect(findByTestId(Text, 'page').text).toEqual('docs');
59
+ expect(maybeFind(Text, (node) => node.text === 'home')).toBeNull();
60
+ expect(maybeFind(Text, (node) => node.text === 'support-index')).toBeNull();
61
+ });
62
+ it('renders only the matched nested route and not sibling or parent-level routes', () => {
63
+ const { mocks, render, findAll, findByTestId, maybeFind } = createRenderer('home');
64
+ mocks.setPageLocation({ path: '/support/faq', params: {} });
65
+ const PageRouter = createPageRouter(_jsxs(PageRoutes, { children: [_jsx(PageRoutes.IndexRoute, { component: HomePage }), _jsxs(PageRoutes, { path: "/support", children: [_jsx(PageRoutes.IndexRoute, { component: SupportIndexPage }), _jsx(PageRoutes.Route, { path: "/contact-us", component: ContactUsPage }), _jsx(PageRoutes.Route, { path: "/faq", component: FAQPage })] })] }));
66
+ render(_jsx(PageRouter, {}));
67
+ expect(findAll(Text, { testId: 'page' })).toHaveLength(1);
68
+ expect(findByTestId(Text, 'page').text).toEqual('faq');
69
+ expect(maybeFind(Text, (node) => node.text === 'home')).toBeNull();
70
+ expect(maybeFind(Text, (node) => node.text === 'support-index')).toBeNull();
71
+ expect(maybeFind(Text, (node) => node.text === 'contact-us')).toBeNull();
72
+ });
73
+ it('renders only one route at a time when location changes', () => {
74
+ const { mocks, render, findAll, findByTestId, maybeFind } = createRenderer('home');
75
+ const PageRouter = createPageRouter(_jsxs(PageRoutes, { children: [_jsx(PageRoutes.IndexRoute, { component: HomePage }), _jsx(PageRoutes.Route, { path: "/docs", component: DocsPage }), _jsx(PageRoutes.Route, { path: "/support", component: SupportIndexPage })] }));
76
+ render(_jsx(PageRouter, {}));
77
+ expect(findAll(Text, { testId: 'page' })).toHaveLength(1);
78
+ expect(findByTestId(Text, 'page').text).toEqual('home');
79
+ expect(maybeFind(Text, (node) => node.text === 'docs')).toBeNull();
80
+ mocks.setPageLocation({ path: '/docs', params: {} });
81
+ expect(findAll(Text, { testId: 'page' })).toHaveLength(1);
82
+ expect(findByTestId(Text, 'page').text).toEqual('docs');
83
+ expect(maybeFind(Text, (node) => node.text === 'home')).toBeNull();
84
+ mocks.setPageLocation({ path: '/support', params: {} });
85
+ expect(findAll(Text, { testId: 'page' })).toHaveLength(1);
86
+ expect(findByTestId(Text, 'page').text).toEqual('support-index');
87
+ expect(maybeFind(Text, (node) => node.text === 'docs')).toBeNull();
88
+ });
89
+ });
90
+ describe('nested routes', () => {
91
+ const createNestedRouter = () => createPageRouter(_jsxs(PageRoutes, { children: [_jsx(PageRoutes.IndexRoute, { component: HomePage }), _jsxs(PageRoutes, { path: "/support", children: [_jsx(PageRoutes.IndexRoute, { component: SupportIndexPage }), _jsx(PageRoutes.Route, { path: "/contact-us", component: ContactUsPage }), _jsx(PageRoutes.Route, { path: "/faq", component: FAQPage })] })] }));
92
+ it('renders nested index route', () => {
93
+ const { mocks, render, findByTestId } = createRenderer('home');
94
+ mocks.setPageLocation({ path: '/support', params: {} });
95
+ const PageRouter = createNestedRouter();
96
+ render(_jsx(PageRouter, {}));
97
+ expect(findByTestId(Text, 'page').text).toEqual('support-index');
98
+ });
99
+ it('renders nested named route', () => {
100
+ const { mocks, render, findByTestId } = createRenderer('home');
101
+ mocks.setPageLocation({ path: '/support/contact-us', params: {} });
102
+ const PageRouter = createNestedRouter();
103
+ render(_jsx(PageRouter, {}));
104
+ expect(findByTestId(Text, 'page').text).toEqual('contact-us');
105
+ });
106
+ it('renders a different nested named route', () => {
107
+ const { mocks, render, findByTestId } = createRenderer('home');
108
+ mocks.setPageLocation({ path: '/support/faq', params: {} });
109
+ const PageRouter = createNestedRouter();
110
+ render(_jsx(PageRouter, {}));
111
+ expect(findByTestId(Text, 'page').text).toEqual('faq');
112
+ });
113
+ });
114
+ describe('path parameters', () => {
115
+ it('extracts path params from the route pattern', () => {
116
+ const { mocks, render, findByTestId } = createRenderer('home');
117
+ mocks.setPageLocation({ path: '/contacts/abc-123', params: {} });
118
+ const PageRouter = createPageRouter(_jsx(PageRoutes, { children: _jsx(PageRoutes.Route, { path: "/contacts/:contactId", component: ContactDetailsPage }) }));
119
+ render(_jsx(PageRouter, {}));
120
+ expect(findByTestId(Text, 'page').text).toEqual('contact-details');
121
+ expect(findByTestId(Text, 'route-params').text).toEqual('{"contactId":"abc-123"}');
122
+ });
123
+ it('merges path params with appPageLocation params', () => {
124
+ const { mocks, render, findByTestId } = createRenderer('home');
125
+ mocks.setPageLocation({
126
+ path: '/contacts/abc-123',
127
+ params: { tab: 'activity' },
128
+ });
129
+ const PageRouter = createPageRouter(_jsx(PageRoutes, { children: _jsx(PageRoutes.Route, { path: "/contacts/:contactId", component: ContactDetailsPage }) }));
130
+ render(_jsx(PageRouter, {}));
131
+ expect(findByTestId(Text, 'route-params').text).toEqual('{"contactId":"abc-123","tab":"activity"}');
132
+ });
133
+ it('gives appPageLocation params precedence over path params on conflict', () => {
134
+ const { mocks, render, findByTestId } = createRenderer('home');
135
+ mocks.setPageLocation({
136
+ path: '/contacts/from-path',
137
+ params: { contactId: 'from-query' },
138
+ });
139
+ const PageRouter = createPageRouter(_jsx(PageRoutes, { children: _jsx(PageRoutes.Route, { path: "/contacts/:contactId", component: ContactDetailsPage }) }));
140
+ render(_jsx(PageRouter, {}));
141
+ expect(findByTestId(Text, 'route-params').text).toEqual('{"contactId":"from-query"}');
142
+ });
143
+ });
144
+ describe('layout components', () => {
145
+ it('wraps matched route content in the layout component', () => {
146
+ const { render, findByTestId } = createRenderer('home');
147
+ const AppLayout = ({ children }) => (_jsx(Box, { testId: "layout-app", children: children }));
148
+ const PageRouter = createPageRouter(_jsx(PageRoutes, { layoutComponent: AppLayout, children: _jsx(PageRoutes.IndexRoute, { component: HomePage }) }));
149
+ render(_jsx(PageRouter, {}));
150
+ expect(findByTestId(Box, 'layout-app')).toBeTruthy();
151
+ expect(findByTestId(Text, 'page').text).toEqual('home');
152
+ });
153
+ it('nests layout components from outer to inner', () => {
154
+ const { mocks, render, findByTestId } = createRenderer('home');
155
+ const OuterLayout = ({ children }) => (_jsx(Box, { testId: "layout-outer", children: children }));
156
+ const InnerLayout = ({ children }) => (_jsx(Box, { testId: "layout-inner", children: children }));
157
+ mocks.setPageLocation({ path: '/section/page-a', params: {} });
158
+ const PageRouter = createPageRouter(_jsxs(PageRoutes, { layoutComponent: OuterLayout, children: [_jsx(PageRoutes.IndexRoute, { component: HomePage }), _jsx(PageRoutes, { path: "/section", layoutComponent: InnerLayout, children: _jsx(PageRoutes.Route, { path: "/page-a", component: DocsPage }) })] }));
159
+ render(_jsx(PageRouter, {}));
160
+ const outerLayout = findByTestId(Box, 'layout-outer');
161
+ const innerLayout = outerLayout.find(Box, { testId: 'layout-inner' });
162
+ expect(outerLayout).toBeTruthy();
163
+ expect(innerLayout).toBeTruthy();
164
+ expect(findByTestId(Text, 'page').text).toEqual('docs');
165
+ });
166
+ });
167
+ describe('catch-all and wildcard routes', () => {
168
+ it('renders AnyRoute for unmatched paths', () => {
169
+ const { mocks, render, findByTestId } = createRenderer('home');
170
+ mocks.setPageLocation({ path: '/does-not-exist', params: {} });
171
+ const PageRouter = createPageRouter(_jsxs(PageRoutes, { children: [_jsx(PageRoutes.IndexRoute, { component: HomePage }), _jsx(PageRoutes.Route, { path: "/docs", component: DocsPage }), _jsx(PageRoutes.AnyRoute, { component: NotFoundPage })] }));
172
+ render(_jsx(PageRouter, {}));
173
+ expect(findByTestId(Text, 'page').text).toEqual('not-found-custom');
174
+ });
175
+ it('renders the wildcard route and captures the remaining path', () => {
176
+ const { mocks, render, findByTestId } = createRenderer('home');
177
+ mocks.setPageLocation({
178
+ path: '/files/documents/report.pdf',
179
+ params: {},
180
+ });
181
+ const WildcardDisplay = () => {
182
+ const { params } = usePageRoute();
183
+ return (_jsxs(_Fragment, { children: [_jsx(Text, { testId: "page", children: "file-browser" }), _jsx(Text, { testId: "wildcard", children: params['*'] })] }));
184
+ };
185
+ const PageRouter = createPageRouter(_jsxs(PageRoutes, { children: [_jsx(PageRoutes.IndexRoute, { component: HomePage }), _jsx(PageRoutes.Route, { path: "/files/*", component: WildcardDisplay })] }));
186
+ render(_jsx(PageRouter, {}));
187
+ expect(findByTestId(Text, 'page').text).toEqual('file-browser');
188
+ expect(findByTestId(Text, 'wildcard').text).toEqual('documents/report.pdf');
189
+ });
190
+ });
191
+ describe('route IDs', () => {
192
+ it('provides routeId via usePageRoute when route has an id', () => {
193
+ const { render, findByTestId } = createRenderer('home');
194
+ const PageWithRouteInfo = () => _jsx(RouteInfoDisplay, {});
195
+ const PageRouter = createPageRouter(_jsxs(PageRoutes, { children: [_jsx(PageRoutes.IndexRoute, { id: "home", component: PageWithRouteInfo }), _jsx(PageRoutes.Route, { id: "docs", path: "/docs", component: PageWithRouteInfo })] }));
196
+ render(_jsx(PageRouter, {}));
197
+ expect(findByTestId(Text, 'route-id').text).toEqual('home');
198
+ });
199
+ it('provides the correct routeId for a named route', () => {
200
+ const { mocks, render, findByTestId } = createRenderer('home');
201
+ mocks.setPageLocation({ path: '/docs', params: {} });
202
+ const PageWithRouteInfo = () => _jsx(RouteInfoDisplay, {});
203
+ const PageRouter = createPageRouter(_jsxs(PageRoutes, { children: [_jsx(PageRoutes.IndexRoute, { id: "home", component: PageWithRouteInfo }), _jsx(PageRoutes.Route, { id: "docs", path: "/docs", component: PageWithRouteInfo })] }));
204
+ render(_jsx(PageRouter, {}));
205
+ expect(findByTestId(Text, 'route-id').text).toEqual('docs');
206
+ });
207
+ it('throws on duplicate route IDs', () => {
208
+ expect(() => createPageRouter(_jsxs(PageRoutes, { children: [_jsx(PageRoutes.IndexRoute, { id: "same-id", component: HomePage }), _jsx(PageRoutes.Route, { id: "same-id", path: "/other", component: DocsPage })] }))).toThrowError('Duplicate route id: "same-id"');
209
+ });
210
+ });
211
+ describe('location reactivity', () => {
212
+ it('re-renders with new content when location changes after mount', () => {
213
+ const { mocks, render, findByTestId } = createRenderer('home');
214
+ const PageRouter = createPageRouter(_jsxs(PageRoutes, { children: [_jsx(PageRoutes.IndexRoute, { component: HomePage }), _jsx(PageRoutes.Route, { path: "/docs", component: DocsPage }), _jsx(PageRoutes.Route, { path: "/support", component: SupportIndexPage })] }));
215
+ render(_jsx(PageRouter, {}));
216
+ expect(findByTestId(Text, 'page').text).toEqual('home');
217
+ mocks.setPageLocation({ path: '/docs', params: {} });
218
+ expect(findByTestId(Text, 'page').text).toEqual('docs');
219
+ mocks.setPageLocation({ path: '/support', params: {} });
220
+ expect(findByTestId(Text, 'page').text).toEqual('support-index');
221
+ });
222
+ it('updates route params when location params change', () => {
223
+ const { mocks, render, findByTestId } = createRenderer('home');
224
+ const PageRouter = createPageRouter(_jsx(PageRoutes, { children: _jsx(PageRoutes.Route, { path: "/contacts/:contactId", component: ContactDetailsPage }) }));
225
+ mocks.setPageLocation({ path: '/contacts/111', params: {} });
226
+ render(_jsx(PageRouter, {}));
227
+ expect(findByTestId(Text, 'route-params').text).toEqual('{"contactId":"111"}');
228
+ mocks.setPageLocation({ path: '/contacts/222', params: {} });
229
+ expect(findByTestId(Text, 'route-params').text).toEqual('{"contactId":"222"}');
230
+ });
231
+ it('transitions from a matched route to "Page not found"', () => {
232
+ const { mocks, render, findByTestId, find } = createRenderer('home');
233
+ const PageRouter = createPageRouter(_jsx(PageRoutes, { children: _jsx(PageRoutes.IndexRoute, { component: HomePage }) }));
234
+ render(_jsx(PageRouter, {}));
235
+ expect(findByTestId(Text, 'page').text).toEqual('home');
236
+ mocks.setPageLocation({ path: '/missing', params: {} });
237
+ expect(find(Text, (node) => node.text === 'This app page does not exist.')).toBeTruthy();
238
+ });
239
+ });
240
+ describe('conditional routes', () => {
241
+ it('excludes a route when condition is false', () => {
242
+ const { mocks, render, find } = createRenderer('home');
243
+ mocks.setPageLocation({ path: '/docs', params: {} });
244
+ const docsEnabled = false;
245
+ const PageRouter = createPageRouter(_jsxs(PageRoutes, { children: [_jsx(PageRoutes.IndexRoute, { component: HomePage }), docsEnabled && (_jsx(PageRoutes.Route, { path: "/docs", component: DocsPage }))] }));
246
+ render(_jsx(PageRouter, {}));
247
+ expect(find(Text, (node) => node.text === 'This app page does not exist.')).toBeTruthy();
248
+ });
249
+ it('includes a route when condition is true', () => {
250
+ const { mocks, render, findByTestId } = createRenderer('home');
251
+ mocks.setPageLocation({ path: '/docs', params: {} });
252
+ const docsEnabled = true;
253
+ const PageRouter = createPageRouter(_jsxs(PageRoutes, { children: [_jsx(PageRoutes.IndexRoute, { component: HomePage }), docsEnabled && (_jsx(PageRoutes.Route, { path: "/docs", component: DocsPage }))] }));
254
+ render(_jsx(PageRouter, {}));
255
+ expect(findByTestId(Text, 'page').text).toEqual('docs');
256
+ });
257
+ it('handles null children', () => {
258
+ const { render, findByTestId } = createRenderer('home');
259
+ const PageRouter = createPageRouter(_jsxs(PageRoutes, { children: [_jsx(PageRoutes.IndexRoute, { component: HomePage }), null] }));
260
+ render(_jsx(PageRouter, {}));
261
+ expect(findByTestId(Text, 'page').text).toEqual('home');
262
+ });
263
+ it('skips conditional nested routes when falsy', () => {
264
+ const { mocks, render, find } = createRenderer('home');
265
+ mocks.setPageLocation({ path: '/support', params: {} });
266
+ const supportEnabled = false;
267
+ const PageRouter = createPageRouter(_jsxs(PageRoutes, { children: [_jsx(PageRoutes.IndexRoute, { component: HomePage }), supportEnabled && (_jsxs(PageRoutes, { path: "/support", children: [_jsx(PageRoutes.IndexRoute, { component: SupportIndexPage }), _jsx(PageRoutes.Route, { path: "/contact-us", component: ContactUsPage })] }))] }));
268
+ render(_jsx(PageRouter, {}));
269
+ expect(find(Text, (node) => node.text === 'This app page does not exist.')).toBeTruthy();
270
+ });
271
+ it('supports routes generated from an array via .map()', () => {
272
+ const { mocks, render, findByTestId } = createRenderer('home');
273
+ mocks.setPageLocation({ path: '/support', params: {} });
274
+ const rootPages = {
275
+ '/': HomePage,
276
+ '/docs': DocsPage,
277
+ '/support': SupportIndexPage,
278
+ };
279
+ const PageRouter = createPageRouter(_jsx(PageRoutes, { children: Object.entries(rootPages).map(([path, PageComponent]) => (_jsx(PageRoutes.Route, { path: path, component: PageComponent }, path))) }));
280
+ render(_jsx(PageRouter, {}));
281
+ expect(findByTestId(Text, 'page').text).toEqual('support-index');
282
+ });
283
+ });
284
+ describe('invalid routes', () => {
285
+ it('throws for an invalid top-level element', () => {
286
+ expect(() => createPageRouter(_jsx("div", {}))).toThrowError('Invalid React node for page routes: <div />');
287
+ });
288
+ it('throws for an invalid child element', () => {
289
+ expect(() => createPageRouter(_jsxs(PageRoutes, { children: [_jsx(PageRoutes.IndexRoute, { component: HomePage }), _jsx("span", {})] }))).toThrowError('Invalid React node for page routes: <span />');
290
+ });
291
+ it('throws for a named component child', () => {
292
+ const Sidebar = () => _jsx("div", { children: "sidebar" });
293
+ expect(() => createPageRouter(_jsxs(PageRoutes, { children: [_jsx(PageRoutes.IndexRoute, { component: HomePage }), _jsx(Sidebar, {})] }))).toThrowError('Invalid React node for page routes: <Sidebar />');
294
+ });
295
+ });
296
+ });
@@ -0,0 +1,8 @@
1
+ import type { MatchedPageRoute } from './types.ts';
2
+ /**
3
+ * The hook that provides the current page route to the component.
4
+ *
5
+ * @returns The current page route.
6
+ * @experimental This hook is experimental. Avoid using it in production due to potential breaking changes. Your feedback is valuable for improvements. Stay tuned for updates.
7
+ */
8
+ export declare const usePageRoute: () => MatchedPageRoute;
@@ -0,0 +1,15 @@
1
+ import { useContext } from 'react';
2
+ import { AppPageRouteContext } from "./internal/app-page-route-context.js";
3
+ /**
4
+ * The hook that provides the current page route to the component.
5
+ *
6
+ * @returns The current page route.
7
+ * @experimental This hook is experimental. Avoid using it in production due to potential breaking changes. Your feedback is valuable for improvements. Stay tuned for updates.
8
+ */
9
+ export const usePageRoute = () => {
10
+ const matchedPageRoute = useContext(AppPageRouteContext);
11
+ if (!matchedPageRoute) {
12
+ throw new Error('usePageRoute must be used within a <PageRouter> component produced by createPageRouter');
13
+ }
14
+ return matchedPageRoute;
15
+ };
@@ -1,4 +1,6 @@
1
+ export * from './components/index.ts';
2
+ export * from './create-page-router.tsx';
3
+ export * from './hooks.ts';
4
+ export type * from './types.ts';
1
5
  export type * from '../../shared/types/pages/components/index.ts';
2
- export type * from '../../shared/types/pages/app-pages-types.ts';
3
- export declare const PageRoutes: import("../../shared/types/pages/components/page-routes.ts").PageRoutesComponent, usePageRoute: import("../../shared/types/pages/app-pages-types.ts").UsePageRouteHook, createPageRouter: import("../../shared/types/pages/app-pages-types.ts").CreatePageRouterFunction;
4
- export { PageLink } from '../../shared/remoteComponents.tsx';
6
+ export { PageBreadcrumbs, PageHeader, PageLink, PageTitle, } from '../../shared/remoteComponents.tsx';
@@ -1,3 +1,4 @@
1
- import { getWorkerGlobals } from "../../internal/global-utils.js";
2
- export const { PageRoutes, usePageRoute, createPageRouter } = getWorkerGlobals().hsWorkerAPI;
3
- export { PageLink } from "../../shared/remoteComponents.js";
1
+ export * from "./components/index.js";
2
+ export * from "./create-page-router.js";
3
+ export * from "./hooks.js";
4
+ export { PageBreadcrumbs, PageHeader, PageLink, PageTitle, } from "../../shared/remoteComponents.js";
@@ -0,0 +1,16 @@
1
+ import type { MatchedPageRoute } from '../types.ts';
2
+ export declare const AppPageRouteContext: import("react").Context<MatchedPageRoute | null>;
3
+ interface AppPageRouteProviderProps {
4
+ children: React.ReactNode;
5
+ pageRoute: MatchedPageRoute;
6
+ }
7
+ /**
8
+ * The provider that provides the current page route to the component.
9
+ * This component is used internally by the produced page router components.
10
+ *
11
+ * @param children - The children to render.
12
+ * @param pageRoute - The current page route.
13
+ * @returns The provider.
14
+ */
15
+ export declare const AppPageRouteProvider: ({ children, pageRoute, }: AppPageRouteProviderProps) => import("react/jsx-runtime").JSX.Element;
16
+ export {};
@@ -0,0 +1,12 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { createContext } from 'react';
3
+ export const AppPageRouteContext = createContext(null);
4
+ /**
5
+ * The provider that provides the current page route to the component.
6
+ * This component is used internally by the produced page router components.
7
+ *
8
+ * @param children - The children to render.
9
+ * @param pageRoute - The current page route.
10
+ * @returns The provider.
11
+ */
12
+ export const AppPageRouteProvider = ({ children, pageRoute, }) => (_jsx(AppPageRouteContext.Provider, { value: pageRoute, children: children }));
@@ -0,0 +1,9 @@
1
+ import { type ReactNode } from 'react';
2
+ import { type RoutesNode } from './page-router-internal-types.ts';
3
+ /**
4
+ * Converts a react node to a routes node by recursively converting the react node.
5
+ *
6
+ * @param reactNode - The react node to convert.
7
+ * @returns The converted routes node.
8
+ */
9
+ export declare const convertReactPageRoutesElement: (reactNode: ReactNode) => RoutesNode;
@@ -0,0 +1,138 @@
1
+ import { Fragment, isValidElement, Children as ReactChildren, } from 'react';
2
+ import { PageRoutes, } from "../components/page-routes.js";
3
+ import { RouteNodeType, } from "./page-router-internal-types.js";
4
+ const isReactPageRoutesElement = (reactRouteNode) => {
5
+ return isValidElement(reactRouteNode) && reactRouteNode.type === PageRoutes;
6
+ };
7
+ const isReactRouteElement = (reactNode) => isValidElement(reactNode) && reactNode.type === PageRoutes.Route;
8
+ const isReactIndexRouteElement = (reactNode) => isValidElement(reactNode) && reactNode.type === PageRoutes.IndexRoute;
9
+ const isReactAnyRouteElement = (reactNode) => isValidElement(reactNode) && reactNode.type === PageRoutes.AnyRoute;
10
+ const isReactFragmentElement = (reactRouteNode) => isValidElement(reactRouteNode) && reactRouteNode.type === Fragment;
11
+ /**
12
+ * Describes a React node by producing a string representation of the React node.
13
+ *
14
+ * @param reactNode - The react node to describe.
15
+ * @returns The description of the react node.
16
+ */
17
+ const describeReactNode = (reactNode) => {
18
+ if (reactNode == null) {
19
+ return String(reactNode);
20
+ }
21
+ if (typeof reactNode !== 'object') {
22
+ return JSON.stringify(reactNode);
23
+ }
24
+ if (isValidElement(reactNode)) {
25
+ const elementType = reactNode.type;
26
+ const name = typeof elementType === 'string'
27
+ ? elementType
28
+ : typeof elementType === 'function'
29
+ ? elementType.displayName ||
30
+ elementType.name ||
31
+ 'Unknown'
32
+ : 'Unknown';
33
+ return `<${name} />`;
34
+ }
35
+ return JSON.stringify(reactNode);
36
+ };
37
+ class InvalidRoutesReactNodeError extends Error {
38
+ constructor(reactNode) {
39
+ super(`Invalid React node for page routes: ${describeReactNode(reactNode)}`);
40
+ }
41
+ }
42
+ const addRoute = (parentNode, childNode) => {
43
+ if (!parentNode.children) {
44
+ parentNode.children = [];
45
+ }
46
+ parentNode.children.push(childNode);
47
+ };
48
+ /**
49
+ * Converts a routes element to a routes node by recursively converting the routes children.
50
+ *
51
+ * @param reactRoutesElement - The routes element to convert.
52
+ * @returns The converted routes node.
53
+ */
54
+ function convertReactRoutesElement(reactRoutesElement) {
55
+ const { path, layoutComponent, children } = reactRoutesElement.props;
56
+ const routesNode = {
57
+ type: RouteNodeType.Routes,
58
+ path,
59
+ layoutComponent,
60
+ children: null,
61
+ };
62
+ convertChildrenReactRouteElements(routesNode, children);
63
+ return routesNode;
64
+ }
65
+ /**
66
+ * Converts the children of a routes element to a routes node by recursively converting the children.
67
+ *
68
+ * @param parentNode - The parent node to add the children to.
69
+ * @param children - The children to convert.
70
+ * @returns void.
71
+ */
72
+ function convertChildrenReactRouteElements(parentNode, children) {
73
+ ReactChildren.forEach(children, (child) => {
74
+ if (child == null || typeof child === 'boolean') {
75
+ return;
76
+ }
77
+ if (isReactPageRoutesElement(child)) {
78
+ const routesNode = convertReactRoutesElement(child);
79
+ addRoute(parentNode, routesNode);
80
+ }
81
+ else if (isReactRouteElement(child)) {
82
+ const { path, component, id } = child.props;
83
+ const routeNode = {
84
+ type: RouteNodeType.Path,
85
+ path,
86
+ component,
87
+ id,
88
+ };
89
+ addRoute(parentNode, routeNode);
90
+ }
91
+ else if (isReactIndexRouteElement(child)) {
92
+ const { component, id } = child.props;
93
+ const routeNode = {
94
+ type: RouteNodeType.Path,
95
+ path: '/',
96
+ component,
97
+ id,
98
+ };
99
+ addRoute(parentNode, routeNode);
100
+ }
101
+ else if (isReactAnyRouteElement(child)) {
102
+ const { component, id } = child.props;
103
+ const routeNode = {
104
+ type: RouteNodeType.Path,
105
+ path: '*',
106
+ component,
107
+ id,
108
+ };
109
+ addRoute(parentNode, routeNode);
110
+ }
111
+ else if (isReactFragmentElement(child)) {
112
+ convertChildrenReactRouteElements(parentNode, child.props.children);
113
+ }
114
+ else {
115
+ throw new InvalidRoutesReactNodeError(child);
116
+ }
117
+ });
118
+ }
119
+ /**
120
+ * Converts a react node to a routes node by recursively converting the react node.
121
+ *
122
+ * @param reactNode - The react node to convert.
123
+ * @returns The converted routes node.
124
+ */
125
+ export const convertReactPageRoutesElement = (reactNode) => {
126
+ if (isReactPageRoutesElement(reactNode)) {
127
+ return convertReactRoutesElement(reactNode);
128
+ }
129
+ if (isReactFragmentElement(reactNode)) {
130
+ const rootNode = {
131
+ type: RouteNodeType.Routes,
132
+ children: null,
133
+ };
134
+ convertChildrenReactRouteElements(rootNode, reactNode.props.children);
135
+ return rootNode;
136
+ }
137
+ throw new InvalidRoutesReactNodeError(reactNode);
138
+ };
@@ -0,0 +1,40 @@
1
+ import type { ComponentType, ReactNode } from 'react';
2
+ import type { EmptyProps } from '../../../shared/types/shared.ts';
3
+ import type { PageRoutesLayoutComponent } from '../components/page-routes.ts';
4
+ export interface PageRoutesLayoutProps {
5
+ children: ReactNode;
6
+ }
7
+ export declare enum RouteNodeType {
8
+ Routes = "routes",
9
+ Path = "path"
10
+ }
11
+ /**
12
+ * An intermediate representation of the page routes before being added to the trie router.
13
+ */
14
+ export interface RoutesNode {
15
+ type: RouteNodeType.Routes;
16
+ path?: string;
17
+ layoutComponent?: PageRoutesLayoutComponent;
18
+ children: RouteNode[] | null;
19
+ }
20
+ /**
21
+ * An intermediate representation of a route before being added to the trie router.
22
+ */
23
+ export interface RoutePathNode {
24
+ type: RouteNodeType.Path;
25
+ path: string;
26
+ component: ComponentType<EmptyProps>;
27
+ id?: string;
28
+ }
29
+ /**
30
+ * An intermediate representation of a route descriptor node that is used to populate an app page router.
31
+ */
32
+ export type RouteNode = RoutesNode | RoutePathNode;
33
+ /**
34
+ * The data for a page route.
35
+ */
36
+ export interface PageRouteData {
37
+ component: ComponentType<EmptyProps>;
38
+ layouts: PageRoutesLayoutComponent[];
39
+ id?: string;
40
+ }
@@ -0,0 +1,5 @@
1
+ export var RouteNodeType;
2
+ (function (RouteNodeType) {
3
+ RouteNodeType["Routes"] = "routes";
4
+ RouteNodeType["Path"] = "path";
5
+ })(RouteNodeType || (RouteNodeType = {}));
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Trie-based URL router. Routes are split into path segments and are stored in a prefix tree for
3
+ * O(number-of-path-segments) lookup. When multiple route types match, priority is: static > param > wildcard.
4
+ */
5
+ export interface MatchedRoute<TRouteData> {
6
+ data: TRouteData;
7
+ params: Record<string, string>;
8
+ }
9
+ /**
10
+ * The TrieRouter interface defines the methods for matching and adding routes to the router.
11
+ */
12
+ export interface TrieRouter<TRouteData> {
13
+ /**
14
+ * Matches a path to a route and returns the route data and parameters
15
+ */
16
+ matchPath: (path: string) => MatchedRoute<TRouteData> | null;
17
+ /**
18
+ * Adds a route to the router
19
+ */
20
+ addRoute: (path: string, data: TRouteData) => void;
21
+ }
22
+ /**
23
+ * Creates a new TrieRouter instance. After creation, routes can be added to the router using the addRoute method.
24
+ * Routes can be matched using the matchPath method.
25
+ *
26
+ * The order that routes are added does not matter. The router will always match the longest possible route.
27
+ *
28
+ * @param TRouteData - The type of the route data that can be associated with each added route
29
+ * @returns A TrieRouter instance
30
+ */
31
+ export declare const createTrieRouter: <TRouteData = unknown>() => TrieRouter<TRouteData>;