@equinor/fusion-framework-plugin-context-navigation 0.1.0 → 0.1.2
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.
- package/dist/esm/ContextNavigationConfigurator.js +178 -0
- package/dist/esm/ContextNavigationConfigurator.js.map +1 -0
- package/dist/esm/__tests__/active-app-navigation-events.test.js +115 -0
- package/dist/esm/__tests__/active-app-navigation-events.test.js.map +1 -0
- package/dist/esm/__tests__/apply-navigation.test.js +152 -0
- package/dist/esm/__tests__/apply-navigation.test.js.map +1 -0
- package/dist/esm/__tests__/enable.test.js +91 -0
- package/dist/esm/__tests__/enable.test.js.map +1 -0
- package/dist/esm/__tests__/guard-handlers.test.js +82 -0
- package/dist/esm/__tests__/guard-handlers.test.js.map +1 -0
- package/dist/esm/__tests__/plugin.test.js +483 -0
- package/dist/esm/__tests__/plugin.test.js.map +1 -0
- package/dist/esm/__tests__/reconcile.test.js +164 -0
- package/dist/esm/__tests__/reconcile.test.js.map +1 -0
- package/dist/esm/adapters/create-path-adapter.js +96 -0
- package/dist/esm/adapters/create-path-adapter.js.map +1 -0
- package/dist/esm/adapters/create-query-adapter.js +83 -0
- package/dist/esm/adapters/create-query-adapter.js.map +1 -0
- package/dist/esm/adapters/custom-adapter/create-custom-adapter.js +116 -0
- package/dist/esm/adapters/custom-adapter/create-custom-adapter.js.map +1 -0
- package/dist/esm/adapters/custom-adapter/index.js +5 -0
- package/dist/esm/adapters/custom-adapter/index.js.map +1 -0
- package/dist/esm/adapters/custom-adapter/normalize-string-result.js +37 -0
- package/dist/esm/adapters/custom-adapter/normalize-string-result.js.map +1 -0
- package/dist/esm/adapters/custom-adapter/to-app-relative.js +34 -0
- package/dist/esm/adapters/custom-adapter/to-app-relative.js.map +1 -0
- package/dist/esm/adapters/custom-adapter/to-full-path.js +28 -0
- package/dist/esm/adapters/custom-adapter/to-full-path.js.map +1 -0
- package/dist/esm/adapters/index.js +4 -0
- package/dist/esm/adapters/index.js.map +1 -0
- package/dist/esm/adapters/types.js +2 -0
- package/dist/esm/adapters/types.js.map +1 -0
- package/dist/esm/apply-navigation.js +102 -0
- package/dist/esm/apply-navigation.js.map +1 -0
- package/dist/esm/constants/context-query-param-key.js +8 -0
- package/dist/esm/constants/context-query-param-key.js.map +1 -0
- package/dist/esm/constants/index.js +3 -0
- package/dist/esm/constants/index.js.map +1 -0
- package/dist/esm/constants/uuid-pattern.js +8 -0
- package/dist/esm/constants/uuid-pattern.js.map +1 -0
- package/dist/esm/create-context-navigation-plugin.js +124 -0
- package/dist/esm/create-context-navigation-plugin.js.map +1 -0
- package/dist/esm/enable-context-navigation.js +40 -0
- package/dist/esm/enable-context-navigation.js.map +1 -0
- package/dist/esm/events.js +2 -0
- package/dist/esm/events.js.map +1 -0
- package/dist/esm/guard-handlers/consume-own-nav-token.js +29 -0
- package/dist/esm/guard-handlers/consume-own-nav-token.js.map +1 -0
- package/dist/esm/guard-handlers/guard-tick-deps.js +2 -0
- package/dist/esm/guard-handlers/guard-tick-deps.js.map +1 -0
- package/dist/esm/guard-handlers/guard-tick-payload.js +2 -0
- package/dist/esm/guard-handlers/guard-tick-payload.js.map +1 -0
- package/dist/esm/guard-handlers/handle-push-mode-guard.js +44 -0
- package/dist/esm/guard-handlers/handle-push-mode-guard.js.map +1 -0
- package/dist/esm/guard-handlers/handle-replace-mode-guard.js +40 -0
- package/dist/esm/guard-handlers/handle-replace-mode-guard.js.map +1 -0
- package/dist/esm/guard-handlers/index.js +5 -0
- package/dist/esm/guard-handlers/index.js.map +1 -0
- package/dist/esm/guard-handlers/is-in-app-scope.js +19 -0
- package/dist/esm/guard-handlers/is-in-app-scope.js.map +1 -0
- package/dist/esm/helpers/get-current-url.js +11 -0
- package/dist/esm/helpers/get-current-url.js.map +1 -0
- package/dist/esm/helpers/index.js +6 -0
- package/dist/esm/helpers/index.js.map +1 -0
- package/dist/esm/helpers/normalize-path-from-url.js +14 -0
- package/dist/esm/helpers/normalize-path-from-url.js.map +1 -0
- package/dist/esm/helpers/normalize-path.js +30 -0
- package/dist/esm/helpers/normalize-path.js.map +1 -0
- package/dist/esm/helpers/resolve-adapter.js +23 -0
- package/dist/esm/helpers/resolve-adapter.js.map +1 -0
- package/dist/esm/helpers/strip-query-params.js +14 -0
- package/dist/esm/helpers/strip-query-params.js.map +1 -0
- package/dist/esm/index.js +13 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/operators/active-app-navigation-events.js +52 -0
- package/dist/esm/operators/active-app-navigation-events.js.map +1 -0
- package/dist/esm/reconcile.js +76 -0
- package/dist/esm/reconcile.js.map +1 -0
- package/dist/esm/sources/app-first-source.js +41 -0
- package/dist/esm/sources/app-first-source.js.map +1 -0
- package/dist/esm/sources/context-first-source.js +47 -0
- package/dist/esm/sources/context-first-source.js.map +1 -0
- package/dist/esm/sources/index.js +4 -0
- package/dist/esm/sources/index.js.map +1 -0
- package/dist/esm/sources/types.js +16 -0
- package/dist/esm/sources/types.js.map +1 -0
- package/dist/esm/types.js +2 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/utils/has-custom-context-generators.js +14 -0
- package/dist/esm/utils/has-custom-context-generators.js.map +1 -0
- package/dist/esm/utils/legacy-app-navigation-fix.js +45 -0
- package/dist/esm/utils/legacy-app-navigation-fix.js.map +1 -0
- package/dist/esm/utils/resolve-context-from-url.js +37 -0
- package/dist/esm/utils/resolve-context-from-url.js.map +1 -0
- package/dist/esm/utils/url/build-app-route.js +20 -0
- package/dist/esm/utils/url/build-app-route.js.map +1 -0
- package/dist/esm/utils/url/build-context-url-for-strategy.js +43 -0
- package/dist/esm/utils/url/build-context-url-for-strategy.js.map +1 -0
- package/dist/esm/utils/url/index.js +7 -0
- package/dist/esm/utils/url/index.js.map +1 -0
- package/dist/esm/utils/url/parse-app-route.js +47 -0
- package/dist/esm/utils/url/parse-app-route.js.map +1 -0
- package/dist/esm/utils/url/resolve-context-id-from-url.js +29 -0
- package/dist/esm/utils/url/resolve-context-id-from-url.js.map +1 -0
- package/dist/esm/utils/url/strip-context-query-param.js +20 -0
- package/dist/esm/utils/url/strip-context-query-param.js.map +1 -0
- package/dist/esm/version.js +3 -0
- package/dist/esm/version.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/dist/types/ContextNavigationConfigurator.d.ts +125 -0
- package/dist/types/ContextNavigationConfigurator.d.ts.map +1 -0
- package/dist/types/__tests__/active-app-navigation-events.test.d.ts +2 -0
- package/dist/types/__tests__/active-app-navigation-events.test.d.ts.map +1 -0
- package/dist/types/__tests__/apply-navigation.test.d.ts +2 -0
- package/dist/types/__tests__/apply-navigation.test.d.ts.map +1 -0
- package/dist/types/__tests__/enable.test.d.ts +2 -0
- package/dist/types/__tests__/enable.test.d.ts.map +1 -0
- package/dist/types/__tests__/guard-handlers.test.d.ts +2 -0
- package/dist/types/__tests__/guard-handlers.test.d.ts.map +1 -0
- package/dist/types/__tests__/plugin.test.d.ts +2 -0
- package/dist/types/__tests__/plugin.test.d.ts.map +1 -0
- package/dist/types/__tests__/reconcile.test.d.ts +2 -0
- package/dist/types/__tests__/reconcile.test.d.ts.map +1 -0
- package/dist/types/adapters/create-path-adapter.d.ts +27 -0
- package/dist/types/adapters/create-path-adapter.d.ts.map +1 -0
- package/dist/types/adapters/create-query-adapter.d.ts +34 -0
- package/dist/types/adapters/create-query-adapter.d.ts.map +1 -0
- package/dist/types/adapters/custom-adapter/create-custom-adapter.d.ts +29 -0
- package/dist/types/adapters/custom-adapter/create-custom-adapter.d.ts.map +1 -0
- package/dist/types/adapters/custom-adapter/index.d.ts +5 -0
- package/dist/types/adapters/custom-adapter/index.d.ts.map +1 -0
- package/dist/types/adapters/custom-adapter/normalize-string-result.d.ts +21 -0
- package/dist/types/adapters/custom-adapter/normalize-string-result.d.ts.map +1 -0
- package/dist/types/adapters/custom-adapter/to-app-relative.d.ts +25 -0
- package/dist/types/adapters/custom-adapter/to-app-relative.d.ts.map +1 -0
- package/dist/types/adapters/custom-adapter/to-full-path.d.ts +22 -0
- package/dist/types/adapters/custom-adapter/to-full-path.d.ts.map +1 -0
- package/dist/types/adapters/index.d.ts +5 -0
- package/dist/types/adapters/index.d.ts.map +1 -0
- package/dist/types/adapters/types.d.ts +73 -0
- package/dist/types/adapters/types.d.ts.map +1 -0
- package/dist/types/apply-navigation.d.ts +88 -0
- package/dist/types/apply-navigation.d.ts.map +1 -0
- package/dist/types/constants/context-query-param-key.d.ts +8 -0
- package/dist/types/constants/context-query-param-key.d.ts.map +1 -0
- package/dist/types/constants/index.d.ts +3 -0
- package/dist/types/constants/index.d.ts.map +1 -0
- package/dist/types/constants/uuid-pattern.d.ts +8 -0
- package/dist/types/constants/uuid-pattern.d.ts.map +1 -0
- package/dist/types/create-context-navigation-plugin.d.ts +58 -0
- package/dist/types/create-context-navigation-plugin.d.ts.map +1 -0
- package/dist/types/enable-context-navigation.d.ts +21 -0
- package/dist/types/enable-context-navigation.d.ts.map +1 -0
- package/dist/types/events.d.ts +16 -0
- package/dist/types/events.d.ts.map +1 -0
- package/dist/types/guard-handlers/consume-own-nav-token.d.ts +21 -0
- package/dist/types/guard-handlers/consume-own-nav-token.d.ts.map +1 -0
- package/dist/types/guard-handlers/guard-tick-deps.d.ts +20 -0
- package/dist/types/guard-handlers/guard-tick-deps.d.ts.map +1 -0
- package/dist/types/guard-handlers/guard-tick-payload.d.ts +15 -0
- package/dist/types/guard-handlers/guard-tick-payload.d.ts.map +1 -0
- package/dist/types/guard-handlers/handle-push-mode-guard.d.ts +24 -0
- package/dist/types/guard-handlers/handle-push-mode-guard.d.ts.map +1 -0
- package/dist/types/guard-handlers/handle-replace-mode-guard.d.ts +19 -0
- package/dist/types/guard-handlers/handle-replace-mode-guard.d.ts.map +1 -0
- package/dist/types/guard-handlers/index.d.ts +7 -0
- package/dist/types/guard-handlers/index.d.ts.map +1 -0
- package/dist/types/guard-handlers/is-in-app-scope.d.ts +15 -0
- package/dist/types/guard-handlers/is-in-app-scope.d.ts.map +1 -0
- package/dist/types/helpers/get-current-url.d.ts +10 -0
- package/dist/types/helpers/get-current-url.d.ts.map +1 -0
- package/dist/types/helpers/index.d.ts +6 -0
- package/dist/types/helpers/index.d.ts.map +1 -0
- package/dist/types/helpers/normalize-path-from-url.d.ts +11 -0
- package/dist/types/helpers/normalize-path-from-url.d.ts.map +1 -0
- package/dist/types/helpers/normalize-path.d.ts +14 -0
- package/dist/types/helpers/normalize-path.d.ts.map +1 -0
- package/dist/types/helpers/resolve-adapter.d.ts +14 -0
- package/dist/types/helpers/resolve-adapter.d.ts.map +1 -0
- package/dist/types/helpers/strip-query-params.d.ts +12 -0
- package/dist/types/helpers/strip-query-params.d.ts.map +1 -0
- package/dist/types/index.d.ts +9 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/operators/active-app-navigation-events.d.ts +47 -0
- package/dist/types/operators/active-app-navigation-events.d.ts.map +1 -0
- package/dist/types/reconcile.d.ts +43 -0
- package/dist/types/reconcile.d.ts.map +1 -0
- package/dist/types/sources/app-first-source.d.ts +14 -0
- package/dist/types/sources/app-first-source.d.ts.map +1 -0
- package/dist/types/sources/context-first-source.d.ts +15 -0
- package/dist/types/sources/context-first-source.d.ts.map +1 -0
- package/dist/types/sources/index.d.ts +4 -0
- package/dist/types/sources/index.d.ts.map +1 -0
- package/dist/types/sources/types.d.ts +61 -0
- package/dist/types/sources/types.d.ts.map +1 -0
- package/dist/types/types.d.ts +126 -0
- package/dist/types/types.d.ts.map +1 -0
- package/dist/types/utils/has-custom-context-generators.d.ts +13 -0
- package/dist/types/utils/has-custom-context-generators.d.ts.map +1 -0
- package/dist/types/utils/legacy-app-navigation-fix.d.ts +33 -0
- package/dist/types/utils/legacy-app-navigation-fix.d.ts.map +1 -0
- package/dist/types/utils/resolve-context-from-url.d.ts +20 -0
- package/dist/types/utils/resolve-context-from-url.d.ts.map +1 -0
- package/dist/types/utils/url/build-app-route.d.ts +11 -0
- package/dist/types/utils/url/build-app-route.d.ts.map +1 -0
- package/dist/types/utils/url/build-context-url-for-strategy.d.ts +14 -0
- package/dist/types/utils/url/build-context-url-for-strategy.d.ts.map +1 -0
- package/dist/types/utils/url/index.d.ts +8 -0
- package/dist/types/utils/url/index.d.ts.map +1 -0
- package/dist/types/utils/url/parse-app-route.d.ts +20 -0
- package/dist/types/utils/url/parse-app-route.d.ts.map +1 -0
- package/dist/types/utils/url/resolve-context-id-from-url.d.ts +8 -0
- package/dist/types/utils/url/resolve-context-id-from-url.d.ts.map +1 -0
- package/dist/types/utils/url/strip-context-query-param.d.ts +12 -0
- package/dist/types/utils/url/strip-context-query-param.d.ts.map +1 -0
- package/dist/types/version.d.ts +2 -0
- package/dist/types/version.d.ts.map +1 -0
- package/package.json +5 -5
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { reconcile } from '../reconcile';
|
|
3
|
+
function makeContext(id) {
|
|
4
|
+
// Test double — only id and title are read by the code under test
|
|
5
|
+
return { id, title: `Context ${id}` };
|
|
6
|
+
}
|
|
7
|
+
function makeAdapter(overrides = {}) {
|
|
8
|
+
return {
|
|
9
|
+
id: 'test-adapter',
|
|
10
|
+
canHandle: () => true,
|
|
11
|
+
encode: ({ context, currentURL }) => {
|
|
12
|
+
// Guard — adapter cannot encode without a context item
|
|
13
|
+
if (!context)
|
|
14
|
+
return null;
|
|
15
|
+
return new URL(`/apps/my-app/${context.id}`, currentURL.origin);
|
|
16
|
+
},
|
|
17
|
+
decode: () => null,
|
|
18
|
+
...overrides,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function makeDeps(overrides = {}) {
|
|
22
|
+
// Test doubles — only the methods exercised by reconcile are provided
|
|
23
|
+
return {
|
|
24
|
+
event: {
|
|
25
|
+
dispatchEvent: vi.fn(async () => ({ canceled: false })),
|
|
26
|
+
},
|
|
27
|
+
navigation: {
|
|
28
|
+
path: { pathname: '/apps/my-app', search: '' },
|
|
29
|
+
navigate: vi.fn(),
|
|
30
|
+
},
|
|
31
|
+
config: {
|
|
32
|
+
portalName: 'Test',
|
|
33
|
+
origin: 'https://example.com',
|
|
34
|
+
adapters: [makeAdapter()],
|
|
35
|
+
enableUrlGuard: false,
|
|
36
|
+
debug: false,
|
|
37
|
+
sourceFactory: () => ({ subscribe: vi.fn() }),
|
|
38
|
+
navigationOptions: { replace: true },
|
|
39
|
+
},
|
|
40
|
+
eventSource: { name: 'contextNavigation', version: '0.0.0' },
|
|
41
|
+
ownNavTokens: new Set(),
|
|
42
|
+
log: vi.fn(),
|
|
43
|
+
...overrides,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
describe('reconcile', () => {
|
|
47
|
+
// Test double — only the context property is accessed by the code under test
|
|
48
|
+
const appModules = {
|
|
49
|
+
context: { currentContext: null },
|
|
50
|
+
};
|
|
51
|
+
let deps;
|
|
52
|
+
beforeEach(() => {
|
|
53
|
+
deps = makeDeps();
|
|
54
|
+
});
|
|
55
|
+
it('dispatches no-context skip when contextState is undefined', () => {
|
|
56
|
+
const entry = { appKey: 'my-app', appModules, contextState: undefined };
|
|
57
|
+
reconcile(entry, deps);
|
|
58
|
+
expect(deps.event.dispatchEvent).toHaveBeenCalledWith('onContextNavigationSkipped', expect.objectContaining({ detail: { appKey: 'my-app', reason: 'no-context' } }));
|
|
59
|
+
});
|
|
60
|
+
it('navigates to nullContextUrl when contextState is null and config provides it', () => {
|
|
61
|
+
const nullContextUrl = vi.fn(() => '/apps/my-app');
|
|
62
|
+
deps = makeDeps();
|
|
63
|
+
deps.config = { ...deps.config, nullContextUrl };
|
|
64
|
+
Object.assign(deps.navigation, {
|
|
65
|
+
path: { pathname: '/apps/my-app/old-ctx', search: '' },
|
|
66
|
+
});
|
|
67
|
+
const entry = { appKey: 'my-app', appModules, contextState: null };
|
|
68
|
+
reconcile(entry, deps);
|
|
69
|
+
expect(nullContextUrl).toHaveBeenCalledWith(expect.objectContaining({ appKey: 'my-app' }));
|
|
70
|
+
expect(deps.navigation.navigate).toHaveBeenCalledWith(expect.objectContaining({ pathname: '/apps/my-app' }), expect.anything());
|
|
71
|
+
expect(deps.ownNavTokens.has('/apps/my-app')).toBe(true);
|
|
72
|
+
});
|
|
73
|
+
it('does not navigate when nullContextUrl produces the same path', () => {
|
|
74
|
+
const nullContextUrl = vi.fn(() => '/apps/my-app');
|
|
75
|
+
deps = makeDeps();
|
|
76
|
+
deps.config = { ...deps.config, nullContextUrl };
|
|
77
|
+
const entry = { appKey: 'my-app', appModules, contextState: null };
|
|
78
|
+
reconcile(entry, deps);
|
|
79
|
+
expect(deps.navigation.navigate).not.toHaveBeenCalled();
|
|
80
|
+
});
|
|
81
|
+
it('dispatches no-adapter skip when no adapter matches', () => {
|
|
82
|
+
const noMatchAdapter = makeAdapter({ canHandle: () => false });
|
|
83
|
+
deps = makeDeps();
|
|
84
|
+
deps.config = { ...deps.config, adapters: [noMatchAdapter] };
|
|
85
|
+
const entry = {
|
|
86
|
+
appKey: 'my-app',
|
|
87
|
+
appModules,
|
|
88
|
+
contextState: makeContext('ctx-1'),
|
|
89
|
+
};
|
|
90
|
+
reconcile(entry, deps);
|
|
91
|
+
expect(deps.event.dispatchEvent).toHaveBeenCalledWith('onContextNavigationSkipped', expect.objectContaining({ detail: { appKey: 'my-app', reason: 'no-adapter' } }));
|
|
92
|
+
});
|
|
93
|
+
it('dispatches adapter-resolved event and triggers navigation', async () => {
|
|
94
|
+
const entry = {
|
|
95
|
+
appKey: 'my-app',
|
|
96
|
+
appModules,
|
|
97
|
+
contextState: makeContext('ctx-1'),
|
|
98
|
+
};
|
|
99
|
+
reconcile(entry, deps);
|
|
100
|
+
await vi.waitFor(() => {
|
|
101
|
+
expect(deps.event.dispatchEvent).toHaveBeenCalledWith('onContextNavigationAdapterResolved', expect.objectContaining({ detail: { appKey: 'my-app', adapterId: 'test-adapter' } }));
|
|
102
|
+
expect(deps.navigation.navigate).toHaveBeenCalledWith(expect.objectContaining({ pathname: '/apps/my-app/ctx-1' }), { replace: true });
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
it('strips all query params on app switch', async () => {
|
|
106
|
+
// Adapter that carries currentURL.search through to its output
|
|
107
|
+
const adapter = makeAdapter({
|
|
108
|
+
encode: ({ context, currentURL }) => {
|
|
109
|
+
// Guard — adapter cannot encode without a context item
|
|
110
|
+
if (!context)
|
|
111
|
+
return null;
|
|
112
|
+
const url = new URL(`/apps/new-app/${context.id}`, currentURL.origin);
|
|
113
|
+
url.search = currentURL.search;
|
|
114
|
+
return url;
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
deps = makeDeps();
|
|
118
|
+
deps.config = { ...deps.config, adapters: [adapter] };
|
|
119
|
+
Object.assign(deps.navigation, {
|
|
120
|
+
path: { pathname: '/apps/old-app/ctx-1', search: '?tab=settings&$contextId=ctx-1' },
|
|
121
|
+
});
|
|
122
|
+
const entry = {
|
|
123
|
+
appKey: 'new-app',
|
|
124
|
+
appModules,
|
|
125
|
+
contextState: makeContext('ctx-2'),
|
|
126
|
+
};
|
|
127
|
+
reconcile(entry, deps, { isAppSwitch: true });
|
|
128
|
+
await vi.waitFor(() => {
|
|
129
|
+
const navigatedURL = vi.mocked(deps.navigation.navigate).mock.calls[0]?.[0];
|
|
130
|
+
// All query params from the previous app should be gone
|
|
131
|
+
expect(navigatedURL.search).toBe('');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
it('preserves query params on context change within the same app', async () => {
|
|
135
|
+
const adapter = makeAdapter({
|
|
136
|
+
encode: ({ context, currentURL }) => {
|
|
137
|
+
// Guard — adapter cannot encode without a context item
|
|
138
|
+
if (!context)
|
|
139
|
+
return null;
|
|
140
|
+
const url = new URL(`/apps/my-app/${context.id}`, currentURL.origin);
|
|
141
|
+
url.search = currentURL.search;
|
|
142
|
+
return url;
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
deps = makeDeps();
|
|
146
|
+
deps.config = { ...deps.config, adapters: [adapter] };
|
|
147
|
+
Object.assign(deps.navigation, {
|
|
148
|
+
path: { pathname: '/apps/my-app/ctx-1', search: '?tab=settings' },
|
|
149
|
+
});
|
|
150
|
+
const entry = {
|
|
151
|
+
appKey: 'my-app',
|
|
152
|
+
appModules,
|
|
153
|
+
contextState: makeContext('ctx-2'),
|
|
154
|
+
};
|
|
155
|
+
// Context change within the same app — query params should survive
|
|
156
|
+
reconcile(entry, deps);
|
|
157
|
+
await vi.waitFor(() => {
|
|
158
|
+
const navigatedURL = vi.mocked(deps.navigation.navigate).mock.calls[0]?.[0];
|
|
159
|
+
// App-specific param should survive on context change
|
|
160
|
+
expect(navigatedURL.searchParams.get('tab')).toBe('settings');
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
//# sourceMappingURL=reconcile.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reconcile.test.js","sourceRoot":"","sources":["../../../src/__tests__/reconcile.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;AAK9D,OAAO,EAAE,SAAS,EAAsB,MAAM,cAAc,CAAC;AAK7D,SAAS,WAAW,CAAC,EAAU;IAC7B,kEAAkE;IAClE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,EAAE,EAA4B,CAAC;AAClE,CAAC;AAED,SAAS,WAAW,CAAC,SAAS,GAAsC,EAAE;IACpE,OAAO;QACL,EAAE,EAAE,cAAc;QAClB,SAAS,EAAE,GAAG,EAAE,CAAC,IAAI;QACrB,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,EAAE;YAClC,uDAAuD;YACvD,IAAI,CAAC,OAAO;gBAAE,OAAO,IAAI,CAAC;YAC1B,OAAO,IAAI,GAAG,CAAC,gBAAgB,OAAO,CAAC,EAAE,EAAE,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;QAClE,CAAC;QACD,MAAM,EAAE,GAAG,EAAE,CAAC,IAAI;QAClB,GAAG,SAAS;KACb,CAAC;AACJ,CAAC;AAED,SAAS,QAAQ,CAAC,SAAS,GAA2B,EAAE;IACtD,sEAAsE;IACtE,OAAO;QACL,KAAK,EAAE;YACL,aAAa,EAAE,EAAE,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC,CAAC;SACnB;QACtC,UAAU,EAAE;YACV,IAAI,EAAE,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,EAAE,EAAE,EAAE;YAC9C,QAAQ,EAAE,EAAE,CAAC,EAAE,EAAE;SACwB;QAC3C,MAAM,EAAE;YACN,UAAU,EAAE,MAAM;YAClB,MAAM,EAAE,qBAAqB;YAC7B,QAAQ,EAAE,CAAC,WAAW,EAAE,CAAC;YACzB,cAAc,EAAE,KAAK;YACrB,KAAK,EAAE,KAAK;YACZ,aAAa,EAAE,GAAG,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,CAAU;YACtD,iBAAiB,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE;SACrC;QACD,WAAW,EAAE,EAAE,IAAI,EAAE,mBAAmB,EAAE,OAAO,EAAE,OAAO,EAAE;QAC5D,YAAY,EAAE,IAAI,GAAG,EAAyB;QAC9C,GAAG,EAAE,EAAE,CAAC,EAAE,EAAE;QACZ,GAAG,SAAS;KACb,CAAC;AACJ,CAAC;AAED,QAAQ,CAAC,WAAW,EAAE,GAAG,EAAE;IACzB,6EAA6E;IAC7E,MAAM,UAAU,GAAG;QACjB,OAAO,EAAE,EAAE,cAAc,EAAE,IAAI,EAAE;KACgB,CAAC;IACpD,IAAI,IAAmB,CAAC;IAExB,UAAU,CAAC,GAAG,EAAE;QACd,IAAI,GAAG,QAAQ,EAAE,CAAC;IACpB,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2DAA2D,EAAE,GAAG,EAAE;QACnE,MAAM,KAAK,GAA0B,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,YAAY,EAAE,SAAS,EAAE,CAAC;QAE/F,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QAEvB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,oBAAoB,CACnD,4BAA4B,EAC5B,MAAM,CAAC,gBAAgB,CAAC,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,EAAE,CAAC,CAChF,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8EAA8E,EAAE,GAAG,EAAE;QACtF,MAAM,cAAc,GAAG,EAAE,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,cAAc,CAAC,CAAC;QACnD,IAAI,GAAG,QAAQ,EAAE,CAAC;QAClB,IAAI,CAAC,MAAM,GAAG,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,cAAc,EAAE,CAAC;QACjD,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE;YAC7B,IAAI,EAAE,EAAE,QAAQ,EAAE,sBAAsB,EAAE,MAAM,EAAE,EAAE,EAAE;SACvD,CAAC,CAAC;QAEH,MAAM,KAAK,GAA0B,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;QAE1F,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QAEvB,MAAM,CAAC,cAAc,CAAC,CAAC,oBAAoB,CAAC,MAAM,CAAC,gBAAgB,CAAC,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC;QAC3F,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,oBAAoB,CACnD,MAAM,CAAC,gBAAgB,CAAC,EAAE,QAAQ,EAAE,cAAc,EAAE,CAAC,EACrD,MAAM,CAAC,QAAQ,EAAE,CAClB,CAAC;QACF,MAAM,CAAC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC3D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8DAA8D,EAAE,GAAG,EAAE;QACtE,MAAM,cAAc,GAAG,EAAE,CAAC,EAAE,CAAC,GAAG,EAAE,CAAC,cAAc,CAAC,CAAC;QACnD,IAAI,GAAG,QAAQ,EAAE,CAAC;QAClB,IAAI,CAAC,MAAM,GAAG,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,cAAc,EAAE,CAAC;QAEjD,MAAM,KAAK,GAA0B,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC;QAE1F,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QAEvB,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,gBAAgB,EAAE,CAAC;IAC1D,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,oDAAoD,EAAE,GAAG,EAAE;QAC5D,MAAM,cAAc,GAAG,WAAW,CAAC,EAAE,SAAS,EAAE,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC,CAAC;QAC/D,IAAI,GAAG,QAAQ,EAAE,CAAC;QAClB,IAAI,CAAC,MAAM,GAAG,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC,cAAc,CAAC,EAAE,CAAC;QAE7D,MAAM,KAAK,GAA0B;YACnC,MAAM,EAAE,QAAQ;YAChB,UAAU;YACV,YAAY,EAAE,WAAW,CAAC,OAAO,CAAC;SACnC,CAAC;QAEF,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QAEvB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,oBAAoB,CACnD,4BAA4B,EAC5B,MAAM,CAAC,gBAAgB,CAAC,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,EAAE,CAAC,CAChF,CAAC;IACJ,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,2DAA2D,EAAE,KAAK,IAAI,EAAE;QACzE,MAAM,KAAK,GAA0B;YACnC,MAAM,EAAE,QAAQ;YAChB,UAAU;YACV,YAAY,EAAE,WAAW,CAAC,OAAO,CAAC;SACnC,CAAC;QAEF,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QAEvB,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE;YACpB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,oBAAoB,CACnD,oCAAoC,EACpC,MAAM,CAAC,gBAAgB,CAAC,EAAE,MAAM,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,EAAE,cAAc,EAAE,EAAE,CAAC,CACrF,CAAC;YACF,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,oBAAoB,CACnD,MAAM,CAAC,gBAAgB,CAAC,EAAE,QAAQ,EAAE,oBAAoB,EAAE,CAAC,EAC3D,EAAE,OAAO,EAAE,IAAI,EAAE,CAClB,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,uCAAuC,EAAE,KAAK,IAAI,EAAE;QACrD,+DAA+D;QAC/D,MAAM,OAAO,GAAG,WAAW,CAAC;YAC1B,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,EAAE;gBAClC,uDAAuD;gBACvD,IAAI,CAAC,OAAO;oBAAE,OAAO,IAAI,CAAC;gBAC1B,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,iBAAiB,OAAO,CAAC,EAAE,EAAE,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;gBACtE,GAAG,CAAC,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC;gBAC/B,OAAO,GAAG,CAAC;YACb,CAAC;SACF,CAAC,CAAC;QACH,IAAI,GAAG,QAAQ,EAAE,CAAC;QAClB,IAAI,CAAC,MAAM,GAAG,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;QACtD,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE;YAC7B,IAAI,EAAE,EAAE,QAAQ,EAAE,qBAAqB,EAAE,MAAM,EAAE,gCAAgC,EAAE;SACpF,CAAC,CAAC;QAEH,MAAM,KAAK,GAA0B;YACnC,MAAM,EAAE,SAAS;YACjB,UAAU;YACV,YAAY,EAAE,WAAW,CAAC,OAAO,CAAC;SACnC,CAAC;QAEF,SAAS,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;QAE9C,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE;YACpB,MAAM,YAAY,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAQ,CAAC;YACnF,wDAAwD;YACxD,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;QACvC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,8DAA8D,EAAE,KAAK,IAAI,EAAE;QAC5E,MAAM,OAAO,GAAG,WAAW,CAAC;YAC1B,MAAM,EAAE,CAAC,EAAE,OAAO,EAAE,UAAU,EAAE,EAAE,EAAE;gBAClC,uDAAuD;gBACvD,IAAI,CAAC,OAAO;oBAAE,OAAO,IAAI,CAAC;gBAC1B,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,gBAAgB,OAAO,CAAC,EAAE,EAAE,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;gBACrE,GAAG,CAAC,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC;gBAC/B,OAAO,GAAG,CAAC;YACb,CAAC;SACF,CAAC,CAAC;QACH,IAAI,GAAG,QAAQ,EAAE,CAAC;QAClB,IAAI,CAAC,MAAM,GAAG,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE,CAAC;QACtD,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,EAAE;YAC7B,IAAI,EAAE,EAAE,QAAQ,EAAE,oBAAoB,EAAE,MAAM,EAAE,eAAe,EAAE;SAClE,CAAC,CAAC;QAEH,MAAM,KAAK,GAA0B;YACnC,MAAM,EAAE,QAAQ;YAChB,UAAU;YACV,YAAY,EAAE,WAAW,CAAC,OAAO,CAAC;SACnC,CAAC;QAEF,mEAAmE;QACnE,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;QAEvB,MAAM,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE;YACpB,MAAM,YAAY,GAAG,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAQ,CAAC;YACnF,sDAAsD;YACtD,MAAM,CAAC,YAAY,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QAChE,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { hasCustomContextGenerators } from '../utils/has-custom-context-generators';
|
|
2
|
+
import { stripContextQueryParam } from '../utils/url/strip-context-query-param';
|
|
3
|
+
import { parseAppRoute } from '../utils/url/parse-app-route';
|
|
4
|
+
import { UUID_PATTERN } from '../constants';
|
|
5
|
+
import { buildAppRoute } from '../utils/url/build-app-route';
|
|
6
|
+
/**
|
|
7
|
+
* Path adapter — encodes context identity as the first path segment after
|
|
8
|
+
* the app basename.
|
|
9
|
+
*
|
|
10
|
+
* **Intent:** This is the legacy/default strategy for Fusion apps. It places
|
|
11
|
+
* the context UUID immediately after `/apps/{appKey}/` as a path segment,
|
|
12
|
+
* making context visible in the URL structure. This convention predates the
|
|
13
|
+
* query strategy and remains the fallback for apps that don't declare a
|
|
14
|
+
* routing strategy.
|
|
15
|
+
*
|
|
16
|
+
* **URL shape:** `/apps/{appKey}/{contextId}/...`
|
|
17
|
+
*
|
|
18
|
+
* **Selection logic:**
|
|
19
|
+
* 1. Immediately bails if the app has custom generators registered
|
|
20
|
+
* (those apps are owned by the custom adapter).
|
|
21
|
+
* 2. Matches when `routingStrategy === 'path'` (explicit opt-in).
|
|
22
|
+
* 3. Matches when no strategy is declared (default fallback).
|
|
23
|
+
*
|
|
24
|
+
* **Context detection:** Uses `parseAppRoute` (URLPattern-based) to parse
|
|
25
|
+
* the path into named segments, and UUID pattern matching to distinguish
|
|
26
|
+
* context ids from route names.
|
|
27
|
+
*
|
|
28
|
+
* @returns A static adapter object
|
|
29
|
+
*/
|
|
30
|
+
export function createPathAdapter() {
|
|
31
|
+
return {
|
|
32
|
+
id: 'path',
|
|
33
|
+
/**
|
|
34
|
+
* Self-selection predicate.
|
|
35
|
+
*
|
|
36
|
+
* Rejects apps with custom generators (they belong to the custom adapter).
|
|
37
|
+
* Accepts apps with explicit `'path'` strategy or no declared strategy
|
|
38
|
+
* (the path adapter serves as the system-wide default fallback).
|
|
39
|
+
*/
|
|
40
|
+
canHandle({ appContext, routingStrategy, currentURL }) {
|
|
41
|
+
// Custom generators mean the app owns its URL shape — the custom adapter handles it, not us
|
|
42
|
+
if (hasCustomContextGenerators(appContext)) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
// Not an app route (e.g. portal chrome) — nothing for this adapter to encode context into
|
|
46
|
+
if (!parseAppRoute(currentURL.pathname)) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
const declared = routingStrategy;
|
|
50
|
+
// App explicitly opted into path-segment routing — take ownership
|
|
51
|
+
if (declared === 'path') {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
// No strategy declared — path adapter is the system-wide default for legacy apps
|
|
55
|
+
if (declared === undefined || declared === null) {
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
},
|
|
60
|
+
/**
|
|
61
|
+
* Encode context into the URL as a path segment.
|
|
62
|
+
*
|
|
63
|
+
* - **Null context:** removes the context segment to produce a bare app route.
|
|
64
|
+
* - **Active context:** places/replaces the context id as the segment after the app key.
|
|
65
|
+
*
|
|
66
|
+
* Sub-routes after the context segment are intentionally dropped — a context
|
|
67
|
+
* change resets the app to its root view to avoid landing in an invalid state.
|
|
68
|
+
*/
|
|
69
|
+
encode({ context, currentURL }) {
|
|
70
|
+
const match = parseAppRoute(currentURL.pathname);
|
|
71
|
+
// Pathname doesn't match the /apps/{key}/... shape — not a Fusion app route, bail out
|
|
72
|
+
if (!match)
|
|
73
|
+
return null;
|
|
74
|
+
const targetPath = context === null ? buildAppRoute(match.appKey) : buildAppRoute(match.appKey, context.id);
|
|
75
|
+
const url = new URL(targetPath, currentURL.origin);
|
|
76
|
+
url.search = currentURL.search;
|
|
77
|
+
stripContextQueryParam(url);
|
|
78
|
+
// Hash is intentionally not preserved — context changes reset app to root view
|
|
79
|
+
return url;
|
|
80
|
+
},
|
|
81
|
+
/**
|
|
82
|
+
* Extract context id from the path segment after the app key, if it matches a UUID.
|
|
83
|
+
*
|
|
84
|
+
* Returns `null` for non-UUID segments (route names, missing segments).
|
|
85
|
+
*/
|
|
86
|
+
decode(url) {
|
|
87
|
+
const match = parseAppRoute(url.pathname);
|
|
88
|
+
// Only treat the segment as a context id if it's a valid UUID — route names can occupy the same slot
|
|
89
|
+
if (match?.contextId && UUID_PATTERN.test(match.contextId)) {
|
|
90
|
+
return match.contextId;
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=create-path-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-path-adapter.js","sourceRoot":"","sources":["../../../src/adapters/create-path-adapter.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,0BAA0B,EAAE,MAAM,wCAAwC,CAAC;AACpF,OAAO,EAAE,sBAAsB,EAAE,MAAM,wCAAwC,CAAC;AAChF,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,aAAa,EAAE,MAAM,8BAA8B,CAAC;AAE7D;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AACH,MAAM,UAAU,iBAAiB;IAC/B,OAAO;QACL,EAAE,EAAE,MAAM;QAEV;;;;;;WAMG;QACH,SAAS,CAAC,EAAE,UAAU,EAAE,eAAe,EAAE,UAAU,EAA4B;YAC7E,4FAA4F;YAC5F,IAAI,0BAA0B,CAAC,UAAU,CAAC,EAAE,CAAC;gBAC3C,OAAO,KAAK,CAAC;YACf,CAAC;YAED,0FAA0F;YAC1F,IAAI,CAAC,aAAa,CAAC,UAAU,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACxC,OAAO,KAAK,CAAC;YACf,CAAC;YAED,MAAM,QAAQ,GAAG,eAAe,CAAC;YACjC,kEAAkE;YAClE,IAAI,QAAQ,KAAK,MAAM,EAAE,CAAC;gBACxB,OAAO,IAAI,CAAC;YACd,CAAC;YACD,iFAAiF;YACjF,IAAI,QAAQ,KAAK,SAAS,IAAI,QAAQ,KAAK,IAAI,EAAE,CAAC;gBAChD,OAAO,IAAI,CAAC;YACd,CAAC;YAED,OAAO,KAAK,CAAC;QACf,CAAC;QAED;;;;;;;;WAQG;QACH,MAAM,CAAC,EAAE,OAAO,EAAE,UAAU,EAAoD;YAC9E,MAAM,KAAK,GAAG,aAAa,CAAC,UAAU,CAAC,QAAQ,CAAC,CAAC;YACjD,sFAAsF;YACtF,IAAI,CAAC,KAAK;gBAAE,OAAO,IAAI,CAAC;YAExB,MAAM,UAAU,GACd,OAAO,KAAK,IAAI,CAAC,CAAC,CAAC,aAAa,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,KAAK,CAAC,MAAM,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;YAE3F,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,UAAU,EAAE,UAAU,CAAC,MAAM,CAAC,CAAC;YACnD,GAAG,CAAC,MAAM,GAAG,UAAU,CAAC,MAAM,CAAC;YAC/B,sBAAsB,CAAC,GAAG,CAAC,CAAC;YAC5B,+EAA+E;YAC/E,OAAO,GAAG,CAAC;QACb,CAAC;QAED;;;;WAIG;QACH,MAAM,CAAC,GAAQ;YACb,MAAM,KAAK,GAAG,aAAa,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YAC1C,qGAAqG;YACrG,IAAI,KAAK,EAAE,SAAS,IAAI,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC;gBAC3D,OAAO,KAAK,CAAC,SAAS,CAAC;YACzB,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { CONTEXT_QUERY_PARAM_KEY } from '../constants';
|
|
2
|
+
/**
|
|
3
|
+
* Query adapter — encodes context identity as a URL query parameter.
|
|
4
|
+
*
|
|
5
|
+
* **Intent:** This is the preferred strategy for new apps. It keeps the
|
|
6
|
+
* pathname clean for app routing while storing context in a predictable
|
|
7
|
+
* query param (`?$contextId={uuid}`). This avoids conflicts with
|
|
8
|
+
* React Router path segments and makes deep-links copy-pasteable without
|
|
9
|
+
* affecting route matching.
|
|
10
|
+
*
|
|
11
|
+
* **URL shape:** `/apps/{appKey}/route?$contextId={id}`
|
|
12
|
+
*
|
|
13
|
+
* **Selection:** Matches only when the app explicitly declares
|
|
14
|
+
* `routingStrategy: 'query'` on its context provider. Does not
|
|
15
|
+
* fall through as a default — that role belongs to the path adapter.
|
|
16
|
+
*
|
|
17
|
+
* **Query string handling:** Clones the full current URL (`currentURL.href`)
|
|
18
|
+
* so all existing query parameters are preserved. Only the context param
|
|
19
|
+
* is added/removed/replaced.
|
|
20
|
+
*
|
|
21
|
+
* @param paramName - Query parameter name to use (default `$contextId`)
|
|
22
|
+
* @returns A static adapter object (not a factory — no runtime binding needed)
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* // Default param name
|
|
27
|
+
* const adapter = createQueryAdapter();
|
|
28
|
+
*
|
|
29
|
+
* // Custom param name for portal-specific needs
|
|
30
|
+
* const adapter = createQueryAdapter('contextId');
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function createQueryAdapter(paramName = CONTEXT_QUERY_PARAM_KEY) {
|
|
34
|
+
return {
|
|
35
|
+
id: 'query',
|
|
36
|
+
/**
|
|
37
|
+
* Matches apps that explicitly opted into query-parameter context routing.
|
|
38
|
+
*
|
|
39
|
+
* Only returns `true` when `routingStrategy === 'query'` — there is no
|
|
40
|
+
* implicit fallback. Apps without a declared strategy use the path adapter.
|
|
41
|
+
*/
|
|
42
|
+
canHandle({ routingStrategy }) {
|
|
43
|
+
return routingStrategy === 'query';
|
|
44
|
+
},
|
|
45
|
+
/**
|
|
46
|
+
* Encode context into the URL's query string.
|
|
47
|
+
*
|
|
48
|
+
* - **Null context:** removes the context param (clears deep-link).
|
|
49
|
+
* - **Active context:** sets the param to the context id.
|
|
50
|
+
*
|
|
51
|
+
* After mutation, the `$` character is restored from `%24` encoding
|
|
52
|
+
* because `URLSearchParams` percent-encodes `$` but we want it
|
|
53
|
+
* human-readable in the address bar as a namespace marker.
|
|
54
|
+
*
|
|
55
|
+
* Hash fragment is intentionally cleared — context changes reset the app
|
|
56
|
+
* to its root view, so any hash pointing to sub-route sections would be invalid.
|
|
57
|
+
*/
|
|
58
|
+
encode({ context, currentURL }) {
|
|
59
|
+
const url = new URL(currentURL.href);
|
|
60
|
+
// Null context means the user deselected context — remove the param to produce a clean URL
|
|
61
|
+
if (context === null) {
|
|
62
|
+
url.searchParams.delete(paramName);
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
url.searchParams.set(paramName, context.id);
|
|
66
|
+
}
|
|
67
|
+
// Restore $ from %24 — $ is a reserved namespace prefix we want visible in URLs
|
|
68
|
+
url.search = url.search.replace(/%24/gi, '$');
|
|
69
|
+
// Clear hash — context change resets app to root view
|
|
70
|
+
url.hash = '';
|
|
71
|
+
return url;
|
|
72
|
+
},
|
|
73
|
+
/**
|
|
74
|
+
* Extract the context id from the query parameter.
|
|
75
|
+
*
|
|
76
|
+
* @returns The context id string, or `null` if the param is absent.
|
|
77
|
+
*/
|
|
78
|
+
decode(url) {
|
|
79
|
+
return url.searchParams.get(paramName) ?? null;
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=create-query-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-query-adapter.js","sourceRoot":"","sources":["../../../src/adapters/create-query-adapter.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,uBAAuB,EAAE,MAAM,cAAc,CAAC;AAEvD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AACH,MAAM,UAAU,kBAAkB,CAAC,SAAS,GAAG,uBAAuB;IACpE,OAAO;QACL,EAAE,EAAE,OAAO;QAEX;;;;;WAKG;QACH,SAAS,CAAC,EAAE,eAAe,EAA4B;YACrD,OAAO,eAAe,KAAK,OAAO,CAAC;QACrC,CAAC;QAED;;;;;;;;;;;;WAYG;QACH,MAAM,CAAC,EAAE,OAAO,EAAE,UAAU,EAAoD;YAC9E,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;YAErC,2FAA2F;YAC3F,IAAI,OAAO,KAAK,IAAI,EAAE,CAAC;gBACrB,GAAG,CAAC,YAAY,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;YACrC,CAAC;iBAAM,CAAC;gBACN,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,SAAS,EAAE,OAAO,CAAC,EAAE,CAAC,CAAC;YAC9C,CAAC;YAED,gFAAgF;YAChF,GAAG,CAAC,MAAM,GAAG,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;YAE9C,sDAAsD;YACtD,GAAG,CAAC,IAAI,GAAG,EAAE,CAAC;YAEd,OAAO,GAAG,CAAC;QACb,CAAC;QAED;;;;WAIG;QACH,MAAM,CAAC,GAAQ;YACb,OAAO,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,IAAI,CAAC;QACjD,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { hasCustomContextGenerators } from '../../utils/has-custom-context-generators';
|
|
2
|
+
import { stripContextQueryParam } from '../../utils/url/strip-context-query-param';
|
|
3
|
+
import { normalizeStringResult } from './normalize-string-result';
|
|
4
|
+
import { toAppRelative } from './to-app-relative';
|
|
5
|
+
import { toFullPath } from './to-full-path';
|
|
6
|
+
/**
|
|
7
|
+
* Custom adapter factory — delegates URL encoding/decoding to app-provided hooks.
|
|
8
|
+
*
|
|
9
|
+
* **Intent:** Some apps define their own URL shape for context (e.g.
|
|
10
|
+
* `/route-a/{contextId}` instead of the standard `/{contextId}/route-a`).
|
|
11
|
+
* These apps register `extractContextIdFromPath` and `generatePathFromContext`
|
|
12
|
+
* on their context provider. This adapter bridges those app hooks into the
|
|
13
|
+
* navigation handler's adapter contract so the reconciler can drive navigation
|
|
14
|
+
* without knowing the app's specific URL layout.
|
|
15
|
+
*
|
|
16
|
+
* **Selection:** The factory returns a bound adapter only when the app has
|
|
17
|
+
* both generator hooks registered (checked via {@link hasCustomContextGenerators}).
|
|
18
|
+
* Otherwise it returns `null`, letting lower-priority adapters (query, path) match.
|
|
19
|
+
*
|
|
20
|
+
* **Coordinate system:** All app hooks receive/return app-relative paths.
|
|
21
|
+
* The adapter handles stripping and prepending the `/apps/{appKey}` prefix
|
|
22
|
+
* transparently via {@link toAppRelative} and {@link toFullPath}.
|
|
23
|
+
*
|
|
24
|
+
* **Query string preservation:** The adapter always carries forward
|
|
25
|
+
* `currentURL.search` to the output URL, ensuring query parameters
|
|
26
|
+
* (e.g. `?routingStrategy=custom`) survive navigation.
|
|
27
|
+
*
|
|
28
|
+
* @returns A {@link ContextNavigationAdapterFactory} that produces a custom adapter when the
|
|
29
|
+
* app context exposes both `extractContextIdFromPath` and `generatePathFromContext`,
|
|
30
|
+
* or `null` to fall through to the next adapter in the resolution chain.
|
|
31
|
+
*/
|
|
32
|
+
export function createCustomAdapter() {
|
|
33
|
+
return ({ appContext, appKey }) => {
|
|
34
|
+
// This app hasn't registered custom context hooks — let a lower-priority adapter handle it
|
|
35
|
+
if (!hasCustomContextGenerators(appContext)) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const appBasename = `/apps/${appKey}`;
|
|
39
|
+
const rawExtract = appContext.extractContextIdFromPath;
|
|
40
|
+
const rawGenerate = appContext.generatePathFromContext;
|
|
41
|
+
// Both hooks must be present; if one is missing the custom URL contract can't be fulfilled
|
|
42
|
+
if (!rawExtract || !rawGenerate) {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
// Capture narrowed references — TypeScript can't narrow across the closure boundary
|
|
46
|
+
const extract = rawExtract;
|
|
47
|
+
const generate = rawGenerate;
|
|
48
|
+
return {
|
|
49
|
+
id: 'custom',
|
|
50
|
+
/** Always true — selection already happened in the factory guard above. */
|
|
51
|
+
canHandle() {
|
|
52
|
+
return true;
|
|
53
|
+
},
|
|
54
|
+
/**
|
|
55
|
+
* Encode a context into the URL using the app's path generator.
|
|
56
|
+
*
|
|
57
|
+
* - **Null context:** navigates to the app root (clears context from URL).
|
|
58
|
+
* - **Existing context in URL:** replaces it with the new context id,
|
|
59
|
+
* preferring the app generator's output, falling back to string replacement.
|
|
60
|
+
* - **No existing context:** delegates entirely to the app generator.
|
|
61
|
+
*
|
|
62
|
+
* Returns `null` when the generator cannot produce a path (signals the
|
|
63
|
+
* reconciler to skip navigation).
|
|
64
|
+
*/
|
|
65
|
+
encode({ context, currentURL, }) {
|
|
66
|
+
// Null context means the user deselected context — navigate to the app root
|
|
67
|
+
if (context === null) {
|
|
68
|
+
const url = new URL(appBasename, currentURL.origin);
|
|
69
|
+
url.search = currentURL.search;
|
|
70
|
+
// Remove query-adapter param — custom apps encode context in the path
|
|
71
|
+
stripContextQueryParam(url);
|
|
72
|
+
// Hash is intentionally not preserved — returning to root app view
|
|
73
|
+
return url;
|
|
74
|
+
}
|
|
75
|
+
const appRelativePath = toAppRelative(currentURL.pathname, appBasename);
|
|
76
|
+
const existingContextId = normalizeStringResult(extract(appRelativePath)) ?? undefined;
|
|
77
|
+
let generatedPath;
|
|
78
|
+
// Context already lives in the URL — prefer the generator's output, but substitute the
|
|
79
|
+
// id directly as a last resort to avoid a no-op navigation
|
|
80
|
+
if (existingContextId) {
|
|
81
|
+
// Context already embedded in the URL — let the generator rewrite it, or fall back to
|
|
82
|
+
// a simple string replacement of the old id with the new one
|
|
83
|
+
generatedPath =
|
|
84
|
+
normalizeStringResult(generate(context, appRelativePath)) ??
|
|
85
|
+
appRelativePath.replace(existingContextId, context.id);
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
// No context in the current URL — ask the generator where to place the new context id
|
|
89
|
+
generatedPath = normalizeStringResult(generate(context, appRelativePath));
|
|
90
|
+
}
|
|
91
|
+
// Generator returned nothing — can't produce a valid URL; skip navigation
|
|
92
|
+
if (!generatedPath) {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
const fullPath = toFullPath(generatedPath, appBasename);
|
|
96
|
+
const url = new URL(fullPath, currentURL.origin);
|
|
97
|
+
url.search = currentURL.search;
|
|
98
|
+
// Remove query-adapter param — custom apps encode context in the path
|
|
99
|
+
stripContextQueryParam(url);
|
|
100
|
+
// Hash is intentionally not preserved — context changes reset app to root view
|
|
101
|
+
return url;
|
|
102
|
+
},
|
|
103
|
+
/**
|
|
104
|
+
* Decode a context id from the URL using the app's path extractor.
|
|
105
|
+
*
|
|
106
|
+
* Converts the browser URL to an app-relative path, then delegates
|
|
107
|
+
* to the app's extract hook. Returns `null` when no context is present.
|
|
108
|
+
*/
|
|
109
|
+
decode(url) {
|
|
110
|
+
const appRelativePath = toAppRelative(url.pathname, appBasename);
|
|
111
|
+
return normalizeStringResult(extract(appRelativePath)) ?? null;
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=create-custom-adapter.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"create-custom-adapter.js","sourceRoot":"","sources":["../../../../src/adapters/custom-adapter/create-custom-adapter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../src/adapters/custom-adapter/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAC9D,OAAO,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AAClE,OAAO,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize legacy app generator outputs to a plain string.
|
|
3
|
+
*
|
|
4
|
+
* Legacy apps may return their path values as either a bare string or
|
|
5
|
+
* wrapped in an array (e.g. `['path']` or `[]`). This function unifies
|
|
6
|
+
* both shapes into `string | undefined` so downstream code doesn't need
|
|
7
|
+
* to branch on format.
|
|
8
|
+
*
|
|
9
|
+
* @param value - Raw return value from an app generator hook
|
|
10
|
+
* @returns The resolved string, or `undefined` if the value is empty/invalid
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* normalizeStringResult('/foo/bar'); // '/foo/bar'
|
|
15
|
+
* normalizeStringResult(['/foo/bar']); // '/foo/bar'
|
|
16
|
+
* normalizeStringResult([]); // undefined
|
|
17
|
+
* normalizeStringResult(''); // undefined
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function normalizeStringResult(value) {
|
|
21
|
+
// A non-empty string can be returned directly — the most common case for modern apps
|
|
22
|
+
if (typeof value === 'string' && value.length > 0) {
|
|
23
|
+
return value;
|
|
24
|
+
}
|
|
25
|
+
// Legacy apps may wrap their return value in an array; unwrap the first element if present
|
|
26
|
+
if (Array.isArray(value)) {
|
|
27
|
+
const first = value[0];
|
|
28
|
+
// Non-empty string at index 0 is the only valid array payload
|
|
29
|
+
if (typeof first === 'string' && first.length > 0) {
|
|
30
|
+
return first;
|
|
31
|
+
}
|
|
32
|
+
// Array was empty or held a non-string — treat as no result
|
|
33
|
+
return undefined;
|
|
34
|
+
}
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
//# sourceMappingURL=normalize-string-result.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"normalize-string-result.js","sourceRoot":"","sources":["../../../../src/adapters/custom-adapter/normalize-string-result.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,UAAU,qBAAqB,CAAC,KAAc;IAClD,qFAAqF;IACrF,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAClD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,2FAA2F;IAC3F,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;QACvB,8DAA8D;QAC9D,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAClD,OAAO,KAAK,CAAC;QACf,CAAC;QACD,4DAA4D;QAC5D,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Strips the app basename prefix from a full pathname to produce an
|
|
3
|
+
* app-relative path.
|
|
4
|
+
*
|
|
5
|
+
* App-provided hooks (`extractContextIdFromPath`, `generatePathFromContext`)
|
|
6
|
+
* operate on app-relative paths — they don't know about the portal's
|
|
7
|
+
* `/apps/{appKey}` prefix. This function converts a browser-absolute
|
|
8
|
+
* pathname into the app's local coordinate system before calling those hooks.
|
|
9
|
+
*
|
|
10
|
+
* Always returns a path starting with `/` so app hooks receive consistent input.
|
|
11
|
+
*
|
|
12
|
+
* @param fullPathname - Absolute browser pathname (e.g. `/apps/my-app/route-a`)
|
|
13
|
+
* @param appBasename - The app's base path (e.g. `/apps/my-app`)
|
|
14
|
+
* @returns App-relative path (e.g. `/route-a`), or the input unchanged if
|
|
15
|
+
* the basename doesn't match (defensive fallback)
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* ```ts
|
|
19
|
+
* toAppRelative('/apps/my-app/route-a/ctx', '/apps/my-app'); // '/route-a/ctx'
|
|
20
|
+
* toAppRelative('/apps/my-app', '/apps/my-app'); // '/'
|
|
21
|
+
* toAppRelative('/other/path', '/apps/my-app'); // '/other/path'
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export function toAppRelative(fullPathname, appBasename) {
|
|
25
|
+
// Basename matches the pathname — the app is at its root, return '/' as the relative path
|
|
26
|
+
if (fullPathname === appBasename || fullPathname.startsWith(`${appBasename}/`)) {
|
|
27
|
+
const relative = fullPathname.slice(appBasename.length);
|
|
28
|
+
// Ensure the relative path always starts with '/' for consistent input to app hooks
|
|
29
|
+
return relative.startsWith('/') ? relative : `/${relative}`;
|
|
30
|
+
}
|
|
31
|
+
// Basename not found in pathname — return as-is; the app hook will see the full path
|
|
32
|
+
return fullPathname;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=to-app-relative.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"to-app-relative.js","sourceRoot":"","sources":["../../../../src/adapters/custom-adapter/to-app-relative.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,MAAM,UAAU,aAAa,CAAC,YAAoB,EAAE,WAAmB;IACrE,0FAA0F;IAC1F,IAAI,YAAY,KAAK,WAAW,IAAI,YAAY,CAAC,UAAU,CAAC,GAAG,WAAW,GAAG,CAAC,EAAE,CAAC;QAC/E,MAAM,QAAQ,GAAG,YAAY,CAAC,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;QACxD,oFAAoF;QACpF,OAAO,QAAQ,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,QAAQ,EAAE,CAAC;IAC9D,CAAC;IACD,qFAAqF;IACrF,OAAO,YAAY,CAAC;AACtB,CAAC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Prepends the app basename to an app-relative path to produce a full
|
|
3
|
+
* browser-absolute pathname.
|
|
4
|
+
*
|
|
5
|
+
* Inverse of {@link toAppRelative}. After an app hook generates a new
|
|
6
|
+
* app-relative path (with context encoded), this function converts it
|
|
7
|
+
* back into a full pathname suitable for browser navigation.
|
|
8
|
+
*
|
|
9
|
+
* Handles edge cases where either side may or may not have a leading/trailing slash.
|
|
10
|
+
*
|
|
11
|
+
* @param appRelativePath - App-relative path (e.g. `/route-a/ctx-id`)
|
|
12
|
+
* @param appBasename - The app's base path (e.g. `/apps/my-app`)
|
|
13
|
+
* @returns Full browser pathname (e.g. `/apps/my-app/route-a/ctx-id`)
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* toFullPath('/route-a/ctx-id', '/apps/my-app'); // '/apps/my-app/route-a/ctx-id'
|
|
18
|
+
* toFullPath('route-a/ctx-id', '/apps/my-app/'); // '/apps/my-app/route-a/ctx-id'
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function toFullPath(appRelativePath, appBasename) {
|
|
22
|
+
// Strip trailing slash from base so we can unconditionally prepend a leading slash from rel
|
|
23
|
+
const base = appBasename.endsWith('/') ? appBasename.slice(0, -1) : appBasename;
|
|
24
|
+
// Ensure rel always starts with '/' before concatenation to avoid double- or missing-slash
|
|
25
|
+
const rel = appRelativePath.startsWith('/') ? appRelativePath : `/${appRelativePath}`;
|
|
26
|
+
return `${base}${rel}`;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=to-full-path.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"to-full-path.js","sourceRoot":"","sources":["../../../../src/adapters/custom-adapter/to-full-path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,UAAU,CAAC,eAAuB,EAAE,WAAmB;IACrE,4FAA4F;IAC5F,MAAM,IAAI,GAAG,WAAW,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC;IAChF,2FAA2F;IAC3F,MAAM,GAAG,GAAG,eAAe,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,eAAe,CAAC,CAAC,CAAC,IAAI,eAAe,EAAE,CAAC;IACtF,OAAO,GAAG,IAAI,GAAG,GAAG,EAAE,CAAC;AACzB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/adapters/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAC5D,OAAO,EAAE,mBAAmB,EAAE,MAAM,kBAAkB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../../src/adapters/types.ts"],"names":[],"mappings":""}
|