@hamak/ui-shell 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -0
- package/dist/api/tokens/service-tokens.d.ts +12 -0
- package/dist/api/tokens/service-tokens.d.ts.map +1 -1
- package/dist/api/tokens/service-tokens.js +12 -0
- package/dist/api/types/index.d.ts +1 -0
- package/dist/api/types/index.d.ts.map +1 -1
- package/dist/api/types/index.js +1 -0
- package/dist/api/types/view-routing-types.d.ts +115 -0
- package/dist/api/types/view-routing-types.d.ts.map +1 -0
- package/dist/api/types/view-routing-types.js +8 -0
- package/dist/impl/index.d.ts +1 -0
- package/dist/impl/index.d.ts.map +1 -1
- package/dist/impl/index.js +2 -0
- package/dist/impl/view-routing/contribution-registry.d.ts +29 -0
- package/dist/impl/view-routing/contribution-registry.d.ts.map +1 -0
- package/dist/impl/view-routing/contribution-registry.js +101 -0
- package/dist/impl/view-routing/index.d.ts +6 -0
- package/dist/impl/view-routing/index.d.ts.map +1 -0
- package/dist/impl/view-routing/index.js +5 -0
- package/dist/impl/view-routing/route-registry.d.ts +18 -0
- package/dist/impl/view-routing/route-registry.d.ts.map +1 -0
- package/dist/impl/view-routing/route-registry.js +31 -0
- package/dist/impl/view-routing/store-fs-node-ref.d.ts +8 -0
- package/dist/impl/view-routing/store-fs-node-ref.d.ts.map +1 -0
- package/dist/impl/view-routing/store-fs-node-ref.js +12 -0
- package/dist/impl/view-routing/view-resolver-registry.d.ts +20 -0
- package/dist/impl/view-routing/view-resolver-registry.d.ts.map +1 -0
- package/dist/impl/view-routing/view-resolver-registry.js +56 -0
- package/dist/impl/view-routing/view-routing-plugin.d.ts +17 -0
- package/dist/impl/view-routing/view-routing-plugin.d.ts.map +1 -0
- package/dist/impl/view-routing/view-routing-plugin.js +21 -0
- package/dist/templates/index.d.ts +6 -0
- package/dist/templates/index.d.ts.map +1 -0
- package/dist/templates/index.js +5 -0
- package/dist/templates/view-routing.d.ts +65 -0
- package/dist/templates/view-routing.d.ts.map +1 -0
- package/dist/templates/view-routing.js +89 -0
- package/package.json +35 -4
package/README.md
CHANGED
|
@@ -164,6 +164,32 @@ router.subscribe((route) => {
|
|
|
164
164
|
});
|
|
165
165
|
```
|
|
166
166
|
|
|
167
|
+
### Contributed Routes and Store-fs Views
|
|
168
|
+
|
|
169
|
+
Plugins contribute routes and view resolvers through `createViewRoutingPlugin()`:
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
import { ROUTE_REGISTRY_TOKEN, VIEW_RESOLVER_REGISTRY_TOKEN } from '@hamak/ui-shell';
|
|
173
|
+
|
|
174
|
+
// manifest: { name: 'studio', dependsOn: ['view-routing'], ... }
|
|
175
|
+
initialize(ctx) {
|
|
176
|
+
ctx.resolve(ROUTE_REGISTRY_TOKEN).register({ path: '/content/*', component: ContentPage, plugin: 'studio' });
|
|
177
|
+
ctx.resolve(VIEW_RESOLVER_REGISTRY_TOKEN).register({
|
|
178
|
+
id: 'studio-model',
|
|
179
|
+
priority: 10,
|
|
180
|
+
resolve: (ref) => (ref.node.name.endsWith('.dm.json') ? ModelStudio : undefined),
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`<ContentView path fs />` (from `@hamak/ui-shell/react`) observes the store-fs node at `path` and renders the component
|
|
186
|
+
the highest-priority resolver returns, or a default view. It dispatches nothing; loading the node is the host's job.
|
|
187
|
+
|
|
188
|
+
**Which route type?** `RouteContribution` (the route registry) is for hosts that render routes with their own router,
|
|
189
|
+
such as react-router: read `useRouteContributions()` in the layout. `RouteConfig` is the table of the shell's built-in
|
|
190
|
+
`DefaultRouter` (lazy components, guards). Registrations are not removed when a plugin deactivates, so keep the
|
|
191
|
+
`dispose` handle if your plugin can be deactivated.
|
|
192
|
+
|
|
167
193
|
### Layout Management
|
|
168
194
|
|
|
169
195
|
```typescript
|
|
@@ -54,4 +54,16 @@ export declare const ShellViewSlots: {
|
|
|
54
54
|
readonly FOOTER: "shell.footer";
|
|
55
55
|
readonly OVERLAY: "shell.overlay";
|
|
56
56
|
};
|
|
57
|
+
/**
|
|
58
|
+
* Token for the route contribution registry (`IRouteRegistry`)
|
|
59
|
+
*
|
|
60
|
+
* The view-routing tokens use `Symbol.for`, so two copies of this module (a
|
|
61
|
+
* plugin bundled with its own copy of ui-shell) still name the same service.
|
|
62
|
+
* The older tokens above predate that and still use `Symbol()`.
|
|
63
|
+
*/
|
|
64
|
+
export declare const ROUTE_REGISTRY_TOKEN: unique symbol;
|
|
65
|
+
/**
|
|
66
|
+
* Token for the view resolver registry (`IViewResolverRegistry`)
|
|
67
|
+
*/
|
|
68
|
+
export declare const VIEW_RESOLVER_REGISTRY_TOKEN: unique symbol;
|
|
57
69
|
//# sourceMappingURL=service-tokens.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"service-tokens.d.ts","sourceRoot":"","sources":["../../../src/api/tokens/service-tokens.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;GAEG;AACH,eAAO,MAAM,WAAW,eAAkB,CAAC;AAE3C;;GAEG;AACH,eAAO,MAAM,mBAAmB,eAAyB,CAAC;AAE1D;;GAEG;AACH,eAAO,MAAM,qBAAqB,eAA2B,CAAC;AAE9D;;GAEG;AACH,eAAO,MAAM,YAAY,eAAmB,CAAC;AAE7C;;GAEG;AACH,eAAO,MAAM,oBAAoB,eAA0B,CAAC;AAE5D;;GAEG;AACH,eAAO,MAAM,aAAa;;;;;;;;CAQhB,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,WAAW;;;;;;CAMd,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,cAAc;;;;;;CAMjB,CAAC"}
|
|
1
|
+
{"version":3,"file":"service-tokens.d.ts","sourceRoot":"","sources":["../../../src/api/tokens/service-tokens.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;GAEG;AACH,eAAO,MAAM,WAAW,eAAkB,CAAC;AAE3C;;GAEG;AACH,eAAO,MAAM,mBAAmB,eAAyB,CAAC;AAE1D;;GAEG;AACH,eAAO,MAAM,qBAAqB,eAA2B,CAAC;AAE9D;;GAEG;AACH,eAAO,MAAM,YAAY,eAAmB,CAAC;AAE7C;;GAEG;AACH,eAAO,MAAM,oBAAoB,eAA0B,CAAC;AAE5D;;GAEG;AACH,eAAO,MAAM,aAAa;;;;;;;;CAQhB,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,WAAW;;;;;;CAMd,CAAC;AAEX;;GAEG;AACH,eAAO,MAAM,cAAc;;;;;;CAMjB,CAAC;AAEX;;;;;;GAMG;AACH,eAAO,MAAM,oBAAoB,eAA8C,CAAC;AAEhF;;GAEG;AACH,eAAO,MAAM,4BAA4B,eAAqD,CAAC"}
|
|
@@ -54,3 +54,15 @@ export const ShellViewSlots = {
|
|
|
54
54
|
FOOTER: 'shell.footer',
|
|
55
55
|
OVERLAY: 'shell.overlay',
|
|
56
56
|
};
|
|
57
|
+
/**
|
|
58
|
+
* Token for the route contribution registry (`IRouteRegistry`)
|
|
59
|
+
*
|
|
60
|
+
* The view-routing tokens use `Symbol.for`, so two copies of this module (a
|
|
61
|
+
* plugin bundled with its own copy of ui-shell) still name the same service.
|
|
62
|
+
* The older tokens above predate that and still use `Symbol()`.
|
|
63
|
+
*/
|
|
64
|
+
export const ROUTE_REGISTRY_TOKEN = Symbol.for('@hamak/ui-shell:RouteRegistry');
|
|
65
|
+
/**
|
|
66
|
+
* Token for the view resolver registry (`IViewResolverRegistry`)
|
|
67
|
+
*/
|
|
68
|
+
export const VIEW_RESOLVER_REGISTRY_TOKEN = Symbol.for('@hamak/ui-shell:ViewResolverRegistry');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/api/types/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/api/types/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC;AAC9B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,eAAe,CAAC;AAC9B,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC"}
|
package/dist/api/types/index.js
CHANGED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* View routing: routes that plugins contribute, and the resolvers that decide
|
|
3
|
+
* which component displays a store-fs node.
|
|
4
|
+
*
|
|
5
|
+
* The types are framework-neutral: a component is whatever the host renders
|
|
6
|
+
* (`C`, a React `ComponentType` for `@hamak/ui-shell/react`).
|
|
7
|
+
*/
|
|
8
|
+
import type { FileNode } from '@hamak/shared-utils';
|
|
9
|
+
/**
|
|
10
|
+
* A route a plugin contributes.
|
|
11
|
+
*
|
|
12
|
+
* `RouteContribution` is what plugins add to the {@link IRouteRegistry}, for a
|
|
13
|
+
* host whose own router (e.g. react-router) renders them. `RouteConfig` is the
|
|
14
|
+
* route table of the shell's built-in `DefaultRouter` (`ROUTER_TOKEN`), with
|
|
15
|
+
* lazy components and guards. A host using `DefaultRouter` can map
|
|
16
|
+
* contributions into it; a host with its own router reads the registry.
|
|
17
|
+
*/
|
|
18
|
+
export interface RouteContribution<C = unknown> {
|
|
19
|
+
/** Router path pattern, e.g. `/content/*`. Unique within a registry. */
|
|
20
|
+
path: string;
|
|
21
|
+
/** Rendered for the route. */
|
|
22
|
+
component: C;
|
|
23
|
+
/** Human label, e.g. for a generated navigation. */
|
|
24
|
+
title?: string;
|
|
25
|
+
/** Name of the contributing plugin, for reports. */
|
|
26
|
+
plugin?: string;
|
|
27
|
+
/** Anything else the host's router needs. */
|
|
28
|
+
meta?: Record<string, unknown>;
|
|
29
|
+
}
|
|
30
|
+
/** A registration refused because another entry already holds its key. */
|
|
31
|
+
export interface DuplicateContribution<T> {
|
|
32
|
+
/** The path (routes) or id (resolvers) both entries claim. */
|
|
33
|
+
key: string;
|
|
34
|
+
/** The entry that holds the key; it stays registered. */
|
|
35
|
+
existing: T;
|
|
36
|
+
/** The entry that was refused. */
|
|
37
|
+
rejected: T;
|
|
38
|
+
}
|
|
39
|
+
export type Registration<T> = {
|
|
40
|
+
accepted: true;
|
|
41
|
+
dispose: () => void;
|
|
42
|
+
} | {
|
|
43
|
+
accepted: false;
|
|
44
|
+
duplicate: DuplicateContribution<T>;
|
|
45
|
+
};
|
|
46
|
+
export interface IRouteRegistry<C = unknown> {
|
|
47
|
+
/**
|
|
48
|
+
* Add a route. A path that is already registered is refused and reported
|
|
49
|
+
* (the earlier registration stays), never silently replaced. `dispose`
|
|
50
|
+
* removes this registration once; registrations are not removed when the
|
|
51
|
+
* contributing plugin deactivates.
|
|
52
|
+
*/
|
|
53
|
+
register(route: RouteContribution<C>): Registration<RouteContribution<C>>;
|
|
54
|
+
/** Every accepted route, in registration order. */
|
|
55
|
+
list(): readonly RouteContribution<C>[];
|
|
56
|
+
/** The most recent refused registrations (at most 100), oldest first. */
|
|
57
|
+
duplicates(): readonly DuplicateContribution<RouteContribution<C>>[];
|
|
58
|
+
/** Called after every change to `list()`. Returns an unsubscribe function. */
|
|
59
|
+
subscribe(listener: () => void): () => void;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* What a resolver is asked about: one store-fs file, as the store holds it now.
|
|
63
|
+
*/
|
|
64
|
+
export interface StoreFsNodeRef<T = unknown> {
|
|
65
|
+
/** Path segments from the store-fs root. */
|
|
66
|
+
path: readonly string[];
|
|
67
|
+
/** Media type from the file name's suffix (`contentTypeOf`, `@hamak/shared-utils`). */
|
|
68
|
+
contentType: string;
|
|
69
|
+
/** The parsed content, as held in the store (`undefined` until loaded). */
|
|
70
|
+
content: T;
|
|
71
|
+
/** The store-fs node itself: name, schema, state. */
|
|
72
|
+
node: FileNode<T>;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Decides whether it displays a store-fs node. Resolvers are asked in priority
|
|
76
|
+
* order and the first component returned wins.
|
|
77
|
+
*/
|
|
78
|
+
export interface ViewResolver<C = unknown> {
|
|
79
|
+
/** Unique within a registry. */
|
|
80
|
+
id: string;
|
|
81
|
+
/**
|
|
82
|
+
* Higher is asked first; equal priorities keep registration order. Default 0.
|
|
83
|
+
* Read when the resolver is registered: to change it, unregister and register
|
|
84
|
+
* the resolver again.
|
|
85
|
+
*/
|
|
86
|
+
priority?: number;
|
|
87
|
+
/** The component that displays `ref`, or `undefined` / `null` to pass. */
|
|
88
|
+
resolve(ref: StoreFsNodeRef): C | null | undefined;
|
|
89
|
+
/** Name of the contributing plugin, for reports. */
|
|
90
|
+
plugin?: string;
|
|
91
|
+
}
|
|
92
|
+
export interface ResolvedView<C = unknown> {
|
|
93
|
+
resolver: ViewResolver<C>;
|
|
94
|
+
component: C;
|
|
95
|
+
}
|
|
96
|
+
export interface IViewResolverRegistry<C = unknown> {
|
|
97
|
+
/**
|
|
98
|
+
* Add a resolver. An id that is already registered is refused and reported.
|
|
99
|
+
* The returned `dispose` removes the resolver. Its `priority` is read now; a
|
|
100
|
+
* later change to it does not re-order the registry (unregister and register
|
|
101
|
+
* again instead).
|
|
102
|
+
*/
|
|
103
|
+
register(resolver: ViewResolver<C>): Registration<ViewResolver<C>>;
|
|
104
|
+
/** Remove the resolver with this id. Returns whether one was removed. */
|
|
105
|
+
unregister(id: string): boolean;
|
|
106
|
+
/** Every resolver, in the order they are asked. */
|
|
107
|
+
list(): readonly ViewResolver<C>[];
|
|
108
|
+
/** The first resolver's answer for `ref`, or `undefined` when all pass. */
|
|
109
|
+
resolve(ref: StoreFsNodeRef): ResolvedView<C> | undefined;
|
|
110
|
+
/** The most recent refused registrations (at most 100), oldest first. */
|
|
111
|
+
duplicates(): readonly DuplicateContribution<ViewResolver<C>>[];
|
|
112
|
+
/** Called after every change to `list()`. Returns an unsubscribe function. */
|
|
113
|
+
subscribe(listener: () => void): () => void;
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=view-routing-types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"view-routing-types.d.ts","sourceRoot":"","sources":["../../../src/api/types/view-routing-types.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* View routing: routes that plugins contribute, and the resolvers that decide
|
|
3
|
+
* which component displays a store-fs node.
|
|
4
|
+
*
|
|
5
|
+
* The types are framework-neutral: a component is whatever the host renders
|
|
6
|
+
* (`C`, a React `ComponentType` for `@hamak/ui-shell/react`).
|
|
7
|
+
*/
|
|
8
|
+
export {};
|
package/dist/impl/index.d.ts
CHANGED
|
@@ -12,6 +12,7 @@ export * from './providers/index.js';
|
|
|
12
12
|
export * from './utils/index.js';
|
|
13
13
|
export * from './styles/index.js';
|
|
14
14
|
export * from './plugin/index.js';
|
|
15
|
+
export * from './view-routing/index.js';
|
|
15
16
|
import { DefaultShell } from './core/DefaultShell.js';
|
|
16
17
|
import { DefaultLayoutManager } from './core/DefaultLayoutManager.js';
|
|
17
18
|
import type { ShellConfig } from '../api/index.js';
|
package/dist/impl/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/impl/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,cAAc,QAAQ,CAAC;AACvB,cAAc,QAAQ,CAAC;AAGvB,cAAc,QAAQ,CAAC;AAGvB,cAAc,aAAa,CAAC;AAG5B,cAAc,SAAS,CAAC;AAGxB,cAAc,UAAU,CAAC;AAGzB,cAAc,UAAU,CAAC;AAGzB,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAE1C,wBAAgB,WAAW,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,YAAY,CAE9D;AAED,wBAAgB,mBAAmB,IAAI,oBAAoB,CAE1D;AAKD,wBAAgB,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,YAAY,CAK3D;AAED,wBAAgB,UAAU,IAAI,IAAI,CAKjC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/impl/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,cAAc,QAAQ,CAAC;AACvB,cAAc,QAAQ,CAAC;AAGvB,cAAc,QAAQ,CAAC;AAGvB,cAAc,aAAa,CAAC;AAG5B,cAAc,SAAS,CAAC;AAGxB,cAAc,UAAU,CAAC;AAGzB,cAAc,UAAU,CAAC;AAGzB,cAAc,gBAAgB,CAAC;AAG/B,OAAO,EAAE,YAAY,EAAE,MAAM,qBAAqB,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,QAAQ,CAAC;AAE1C,wBAAgB,WAAW,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,YAAY,CAE9D;AAED,wBAAgB,mBAAmB,IAAI,oBAAoB,CAE1D;AAKD,wBAAgB,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,GAAG,YAAY,CAK3D;AAED,wBAAgB,UAAU,IAAI,IAAI,CAKjC"}
|
package/dist/impl/index.js
CHANGED
|
@@ -18,6 +18,8 @@ export * from './utils/index.js';
|
|
|
18
18
|
export * from './styles/index.js';
|
|
19
19
|
// Export plugin integration
|
|
20
20
|
export * from './plugin/index.js';
|
|
21
|
+
// Export route contributions and view resolvers
|
|
22
|
+
export * from './view-routing/index.js';
|
|
21
23
|
// Factory functions
|
|
22
24
|
import { DefaultShell } from './core/DefaultShell.js';
|
|
23
25
|
import { DefaultLayoutManager } from './core/DefaultLayoutManager.js';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { DuplicateContribution, Registration } from '../../api/index.js';
|
|
2
|
+
/** How many refused registrations `duplicates()` keeps (the most recent ones). */
|
|
3
|
+
export declare const MAX_REPORTED_DUPLICATES = 100;
|
|
4
|
+
/**
|
|
5
|
+
* Keyed, ordered contributions with change notification. A second entry for a
|
|
6
|
+
* key is refused and reported; the first registration stays.
|
|
7
|
+
*
|
|
8
|
+
* Registrations are not tied to a plugin's lifecycle: a contributor that can be
|
|
9
|
+
* deactivated keeps its `dispose` handles. A route disposed and registered
|
|
10
|
+
* again goes to the end of the list, so the list follows `dependsOn` order for
|
|
11
|
+
* registrations made during `initialize()`.
|
|
12
|
+
*/
|
|
13
|
+
export declare class ContributionRegistry<T> {
|
|
14
|
+
private readonly keyOf;
|
|
15
|
+
private readonly onChange;
|
|
16
|
+
private readonly records;
|
|
17
|
+
private readonly refused;
|
|
18
|
+
private readonly listeners;
|
|
19
|
+
private readonly onDuplicate;
|
|
20
|
+
constructor(keyOf: (entry: T) => string, kind: string, onDuplicate?: (duplicate: DuplicateContribution<T>) => void, onChange?: () => void);
|
|
21
|
+
add(entry: T): Registration<T>;
|
|
22
|
+
removeKey(key: string): boolean;
|
|
23
|
+
list(): readonly T[];
|
|
24
|
+
duplicates(): readonly DuplicateContribution<T>[];
|
|
25
|
+
subscribe(listener: () => void): () => void;
|
|
26
|
+
private removeRecord;
|
|
27
|
+
private notify;
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=contribution-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contribution-registry.d.ts","sourceRoot":"","sources":["../../../src/impl/view-routing/contribution-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,qBAAqB,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAErE,kFAAkF;AAClF,eAAO,MAAM,uBAAuB,MAAM,CAAC;AAO3C;;;;;;;;GAQG;AACH,qBAAa,oBAAoB,CAAC,CAAC;IAO/B,OAAO,CAAC,QAAQ,CAAC,KAAK;IAGtB,OAAO,CAAC,QAAQ,CAAC,QAAQ;IAT3B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA+B;IACvD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAkC;IAC1D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAyB;IACnD,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAgD;gBAGzD,KAAK,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,MAAM,EAC5C,IAAI,EAAE,MAAM,EACZ,WAAW,CAAC,EAAE,CAAC,SAAS,EAAE,qBAAqB,CAAC,CAAC,CAAC,KAAK,IAAI,EAC1C,QAAQ,GAAE,MAAM,IAAe;IAOlD,GAAG,CAAC,KAAK,EAAE,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC;IAkB9B,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAK/B,IAAI,IAAI,SAAS,CAAC,EAAE;IAIpB,UAAU,IAAI,SAAS,qBAAqB,CAAC,CAAC,CAAC,EAAE;IAIjD,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;IAO3C,OAAO,CAAC,YAAY;IAQpB,OAAO,CAAC,MAAM;CAIf"}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/** How many refused registrations `duplicates()` keeps (the most recent ones). */
|
|
2
|
+
export const MAX_REPORTED_DUPLICATES = 100;
|
|
3
|
+
/**
|
|
4
|
+
* Keyed, ordered contributions with change notification. A second entry for a
|
|
5
|
+
* key is refused and reported; the first registration stays.
|
|
6
|
+
*
|
|
7
|
+
* Registrations are not tied to a plugin's lifecycle: a contributor that can be
|
|
8
|
+
* deactivated keeps its `dispose` handles. A route disposed and registered
|
|
9
|
+
* again goes to the end of the list, so the list follows `dependsOn` order for
|
|
10
|
+
* registrations made during `initialize()`.
|
|
11
|
+
*/
|
|
12
|
+
export class ContributionRegistry {
|
|
13
|
+
constructor(keyOf, kind, onDuplicate, onChange = () => { }) {
|
|
14
|
+
Object.defineProperty(this, "keyOf", {
|
|
15
|
+
enumerable: true,
|
|
16
|
+
configurable: true,
|
|
17
|
+
writable: true,
|
|
18
|
+
value: keyOf
|
|
19
|
+
});
|
|
20
|
+
Object.defineProperty(this, "onChange", {
|
|
21
|
+
enumerable: true,
|
|
22
|
+
configurable: true,
|
|
23
|
+
writable: true,
|
|
24
|
+
value: onChange
|
|
25
|
+
});
|
|
26
|
+
Object.defineProperty(this, "records", {
|
|
27
|
+
enumerable: true,
|
|
28
|
+
configurable: true,
|
|
29
|
+
writable: true,
|
|
30
|
+
value: []
|
|
31
|
+
});
|
|
32
|
+
Object.defineProperty(this, "refused", {
|
|
33
|
+
enumerable: true,
|
|
34
|
+
configurable: true,
|
|
35
|
+
writable: true,
|
|
36
|
+
value: []
|
|
37
|
+
});
|
|
38
|
+
Object.defineProperty(this, "listeners", {
|
|
39
|
+
enumerable: true,
|
|
40
|
+
configurable: true,
|
|
41
|
+
writable: true,
|
|
42
|
+
value: new Set()
|
|
43
|
+
});
|
|
44
|
+
Object.defineProperty(this, "onDuplicate", {
|
|
45
|
+
enumerable: true,
|
|
46
|
+
configurable: true,
|
|
47
|
+
writable: true,
|
|
48
|
+
value: void 0
|
|
49
|
+
});
|
|
50
|
+
this.onDuplicate = onDuplicate ?? ((duplicate) => {
|
|
51
|
+
console.warn(`Duplicate ${kind} '${duplicate.key}' refused; the first registration stays`, duplicate);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
add(entry) {
|
|
55
|
+
const key = this.keyOf(entry);
|
|
56
|
+
const existing = this.records.find((record) => this.keyOf(record.entry) === key);
|
|
57
|
+
if (existing !== undefined) {
|
|
58
|
+
const duplicate = { key, existing: existing.entry, rejected: entry };
|
|
59
|
+
this.refused.push(duplicate);
|
|
60
|
+
if (this.refused.length > MAX_REPORTED_DUPLICATES)
|
|
61
|
+
this.refused.shift();
|
|
62
|
+
this.onDuplicate(duplicate);
|
|
63
|
+
return { accepted: false, duplicate };
|
|
64
|
+
}
|
|
65
|
+
const record = { entry };
|
|
66
|
+
this.records.push(record);
|
|
67
|
+
this.notify();
|
|
68
|
+
// The handle removes this registration only: once it has run, or once the
|
|
69
|
+
// entry was removed and registered again, it does nothing.
|
|
70
|
+
return { accepted: true, dispose: () => this.removeRecord(record) };
|
|
71
|
+
}
|
|
72
|
+
removeKey(key) {
|
|
73
|
+
const record = this.records.find((r) => this.keyOf(r.entry) === key);
|
|
74
|
+
return record !== undefined && this.removeRecord(record);
|
|
75
|
+
}
|
|
76
|
+
list() {
|
|
77
|
+
return this.records.map((record) => record.entry);
|
|
78
|
+
}
|
|
79
|
+
duplicates() {
|
|
80
|
+
return [...this.refused];
|
|
81
|
+
}
|
|
82
|
+
subscribe(listener) {
|
|
83
|
+
this.listeners.add(listener);
|
|
84
|
+
return () => {
|
|
85
|
+
this.listeners.delete(listener);
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
removeRecord(record) {
|
|
89
|
+
const index = this.records.indexOf(record);
|
|
90
|
+
if (index < 0)
|
|
91
|
+
return false;
|
|
92
|
+
this.records.splice(index, 1);
|
|
93
|
+
this.notify();
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
notify() {
|
|
97
|
+
this.onChange();
|
|
98
|
+
for (const listener of [...this.listeners])
|
|
99
|
+
listener();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/impl/view-routing/index.ts"],"names":[],"mappings":"AAAA,cAAc,yBAAyB,CAAC;AACxC,cAAc,kBAAkB,CAAC;AACjC,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC;AACpC,cAAc,uBAAuB,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { DuplicateContribution, IRouteRegistry, Registration, RouteContribution } from '../../api/index.js';
|
|
2
|
+
export type DuplicateReporter<T> = (duplicate: DuplicateContribution<T>) => void;
|
|
3
|
+
/**
|
|
4
|
+
* Routes contributed by plugins, keyed by path.
|
|
5
|
+
*
|
|
6
|
+
* `list()` keeps registration order. Plugins register during `initialize()`,
|
|
7
|
+
* which the microkernel runs in `dependsOn` order, so a route of a plugin comes
|
|
8
|
+
* after the routes of the plugins it depends on.
|
|
9
|
+
*/
|
|
10
|
+
export declare class RouteRegistry<C = unknown> implements IRouteRegistry<C> {
|
|
11
|
+
private readonly routes;
|
|
12
|
+
constructor(onDuplicate?: DuplicateReporter<RouteContribution<C>>);
|
|
13
|
+
register(route: RouteContribution<C>): Registration<RouteContribution<C>>;
|
|
14
|
+
list(): readonly RouteContribution<C>[];
|
|
15
|
+
duplicates(): readonly DuplicateContribution<RouteContribution<C>>[];
|
|
16
|
+
subscribe(listener: () => void): () => void;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=route-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"route-registry.d.ts","sourceRoot":"","sources":["../../../src/impl/view-routing/route-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,qBAAqB,EACrB,cAAc,EACd,YAAY,EACZ,iBAAiB,EAClB,MAAM,WAAW,CAAC;AAGnB,MAAM,MAAM,iBAAiB,CAAC,CAAC,IAAI,CAAC,SAAS,EAAE,qBAAqB,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;AAEjF;;;;;;GAMG;AACH,qBAAa,aAAa,CAAC,CAAC,GAAG,OAAO,CAAE,YAAW,cAAc,CAAC,CAAC,CAAC;IAClE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA6C;gBAExD,WAAW,CAAC,EAAE,iBAAiB,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC;IAIjE,QAAQ,CAAC,KAAK,EAAE,iBAAiB,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC;IAIzE,IAAI,IAAI,SAAS,iBAAiB,CAAC,CAAC,CAAC,EAAE;IAIvC,UAAU,IAAI,SAAS,qBAAqB,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,EAAE;IAIpE,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;CAG5C"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ContributionRegistry } from './contribution-registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* Routes contributed by plugins, keyed by path.
|
|
4
|
+
*
|
|
5
|
+
* `list()` keeps registration order. Plugins register during `initialize()`,
|
|
6
|
+
* which the microkernel runs in `dependsOn` order, so a route of a plugin comes
|
|
7
|
+
* after the routes of the plugins it depends on.
|
|
8
|
+
*/
|
|
9
|
+
export class RouteRegistry {
|
|
10
|
+
constructor(onDuplicate) {
|
|
11
|
+
Object.defineProperty(this, "routes", {
|
|
12
|
+
enumerable: true,
|
|
13
|
+
configurable: true,
|
|
14
|
+
writable: true,
|
|
15
|
+
value: void 0
|
|
16
|
+
});
|
|
17
|
+
this.routes = new ContributionRegistry((route) => route.path, 'route', onDuplicate);
|
|
18
|
+
}
|
|
19
|
+
register(route) {
|
|
20
|
+
return this.routes.add(route);
|
|
21
|
+
}
|
|
22
|
+
list() {
|
|
23
|
+
return this.routes.list();
|
|
24
|
+
}
|
|
25
|
+
duplicates() {
|
|
26
|
+
return this.routes.duplicates();
|
|
27
|
+
}
|
|
28
|
+
subscribe(listener) {
|
|
29
|
+
return this.routes.subscribe(listener);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { type FileSystemNode } from '@hamak/shared-utils';
|
|
2
|
+
import type { StoreFsNodeRef } from '../../api/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* The resolver input for a store-fs node, or `undefined` when the node is not
|
|
5
|
+
* a file. The content type comes from the file name's suffix.
|
|
6
|
+
*/
|
|
7
|
+
export declare function storeFsNodeRef(path: string | readonly string[], node: FileSystemNode | undefined): StoreFsNodeRef | undefined;
|
|
8
|
+
//# sourceMappingURL=store-fs-node-ref.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"store-fs-node-ref.d.ts","sourceRoot":"","sources":["../../../src/impl/view-routing/store-fs-node-ref.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4B,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACpF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEhD;;;GAGG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EAAE,IAAI,EAAE,cAAc,GAAG,SAAS,GAAG,cAAc,GAAG,SAAS,CAK7H"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { contentTypeOf, pathSteps } from '@hamak/shared-utils';
|
|
2
|
+
/**
|
|
3
|
+
* The resolver input for a store-fs node, or `undefined` when the node is not
|
|
4
|
+
* a file. The content type comes from the file name's suffix.
|
|
5
|
+
*/
|
|
6
|
+
export function storeFsNodeRef(path, node) {
|
|
7
|
+
if (node === undefined || node.type !== 'file')
|
|
8
|
+
return undefined;
|
|
9
|
+
const steps = typeof path === 'string' ? pathSteps(path) : [...path];
|
|
10
|
+
const name = node.name ?? steps[steps.length - 1] ?? '';
|
|
11
|
+
return { path: steps, contentType: contentTypeOf(name), content: node.content, node };
|
|
12
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { DuplicateContribution, IViewResolverRegistry, Registration, ResolvedView, StoreFsNodeRef, ViewResolver } from '../../api/index.js';
|
|
2
|
+
import type { DuplicateReporter } from './route-registry.js';
|
|
3
|
+
/**
|
|
4
|
+
* View resolvers, keyed by id and asked by descending `priority`; equal
|
|
5
|
+
* priorities are asked in registration order. A resolver's `priority` is read
|
|
6
|
+
* when it is registered; to change it, unregister and register it again.
|
|
7
|
+
*/
|
|
8
|
+
export declare class ViewResolverRegistry<C = unknown> implements IViewResolverRegistry<C> {
|
|
9
|
+
private readonly resolvers;
|
|
10
|
+
/** The resolvers in asking order; cleared on every change. */
|
|
11
|
+
private ordered;
|
|
12
|
+
constructor(onDuplicate?: DuplicateReporter<ViewResolver<C>>);
|
|
13
|
+
register(resolver: ViewResolver<C>): Registration<ViewResolver<C>>;
|
|
14
|
+
unregister(id: string): boolean;
|
|
15
|
+
list(): readonly ViewResolver<C>[];
|
|
16
|
+
resolve(ref: StoreFsNodeRef): ResolvedView<C> | undefined;
|
|
17
|
+
duplicates(): readonly DuplicateContribution<ViewResolver<C>>[];
|
|
18
|
+
subscribe(listener: () => void): () => void;
|
|
19
|
+
}
|
|
20
|
+
//# sourceMappingURL=view-resolver-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"view-resolver-registry.d.ts","sourceRoot":"","sources":["../../../src/impl/view-routing/view-resolver-registry.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,qBAAqB,EACrB,qBAAqB,EACrB,YAAY,EACZ,YAAY,EACZ,cAAc,EACd,YAAY,EACb,MAAM,WAAW,CAAC;AAEnB,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAE1D;;;;GAIG;AACH,qBAAa,oBAAoB,CAAC,CAAC,GAAG,OAAO,CAAE,YAAW,qBAAqB,CAAC,CAAC,CAAC;IAChF,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAwC;IAClE,8DAA8D;IAC9D,OAAO,CAAC,OAAO,CAAyC;gBAE5C,WAAW,CAAC,EAAE,iBAAiB,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAW5D,QAAQ,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC;IAIlE,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAI/B,IAAI,IAAI,SAAS,YAAY,CAAC,CAAC,CAAC,EAAE;IASlC,OAAO,CAAC,GAAG,EAAE,cAAc,GAAG,YAAY,CAAC,CAAC,CAAC,GAAG,SAAS;IAWzD,UAAU,IAAI,SAAS,qBAAqB,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,EAAE;IAI/D,SAAS,CAAC,QAAQ,EAAE,MAAM,IAAI,GAAG,MAAM,IAAI;CAG5C"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { ContributionRegistry } from './contribution-registry.js';
|
|
2
|
+
/**
|
|
3
|
+
* View resolvers, keyed by id and asked by descending `priority`; equal
|
|
4
|
+
* priorities are asked in registration order. A resolver's `priority` is read
|
|
5
|
+
* when it is registered; to change it, unregister and register it again.
|
|
6
|
+
*/
|
|
7
|
+
export class ViewResolverRegistry {
|
|
8
|
+
constructor(onDuplicate) {
|
|
9
|
+
Object.defineProperty(this, "resolvers", {
|
|
10
|
+
enumerable: true,
|
|
11
|
+
configurable: true,
|
|
12
|
+
writable: true,
|
|
13
|
+
value: void 0
|
|
14
|
+
});
|
|
15
|
+
/** The resolvers in asking order; cleared on every change. */
|
|
16
|
+
Object.defineProperty(this, "ordered", {
|
|
17
|
+
enumerable: true,
|
|
18
|
+
configurable: true,
|
|
19
|
+
writable: true,
|
|
20
|
+
value: void 0
|
|
21
|
+
});
|
|
22
|
+
this.resolvers = new ContributionRegistry((resolver) => resolver.id, 'view resolver', onDuplicate, () => {
|
|
23
|
+
this.ordered = undefined;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
register(resolver) {
|
|
27
|
+
return this.resolvers.add(resolver);
|
|
28
|
+
}
|
|
29
|
+
unregister(id) {
|
|
30
|
+
return this.resolvers.removeKey(id);
|
|
31
|
+
}
|
|
32
|
+
list() {
|
|
33
|
+
this.ordered ?? (this.ordered = this.resolvers
|
|
34
|
+
.list()
|
|
35
|
+
.map((resolver, index) => ({ resolver, index }))
|
|
36
|
+
.sort((a, b) => (b.resolver.priority ?? 0) - (a.resolver.priority ?? 0) || a.index - b.index)
|
|
37
|
+
.map(({ resolver }) => resolver));
|
|
38
|
+
return [...this.ordered];
|
|
39
|
+
}
|
|
40
|
+
resolve(ref) {
|
|
41
|
+
this.list();
|
|
42
|
+
for (const resolver of this.ordered) {
|
|
43
|
+
const component = resolver.resolve(ref);
|
|
44
|
+
if (component !== undefined && component !== null) {
|
|
45
|
+
return { resolver, component };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
duplicates() {
|
|
51
|
+
return this.resolvers.duplicates();
|
|
52
|
+
}
|
|
53
|
+
subscribe(listener) {
|
|
54
|
+
return this.resolvers.subscribe(listener);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { PluginModule } from '@hamak/microkernel-spi';
|
|
2
|
+
import { RouteRegistry, type DuplicateReporter } from './route-registry.js';
|
|
3
|
+
import { ViewResolverRegistry } from './view-resolver-registry.js';
|
|
4
|
+
import type { RouteContribution, ViewResolver } from '../../api/index.js';
|
|
5
|
+
export interface ViewRoutingPluginOptions<C = unknown> {
|
|
6
|
+
routes?: RouteRegistry<C>;
|
|
7
|
+
resolvers?: ViewResolverRegistry<C>;
|
|
8
|
+
onDuplicateRoute?: DuplicateReporter<RouteContribution<C>>;
|
|
9
|
+
onDuplicateResolver?: DuplicateReporter<ViewResolver<C>>;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Provides {@link ROUTE_REGISTRY_TOKEN} and {@link VIEW_RESOLVER_REGISTRY_TOKEN}.
|
|
13
|
+
* Plugins that contribute routes or resolvers list this plugin in `dependsOn`
|
|
14
|
+
* and register from their own `initialize()`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function createViewRoutingPlugin<C = unknown>(options?: ViewRoutingPluginOptions<C>): PluginModule;
|
|
17
|
+
//# sourceMappingURL=view-routing-plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"view-routing-plugin.d.ts","sourceRoot":"","sources":["../../../src/impl/view-routing/view-routing-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAE3D,OAAO,EAAE,aAAa,EAAE,KAAK,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACzE,OAAO,EAAE,oBAAoB,EAAE,MAAM,0BAA0B,CAAC;AAChE,OAAO,KAAK,EAAE,iBAAiB,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAEjE,MAAM,WAAW,wBAAwB,CAAC,CAAC,GAAG,OAAO;IACnD,MAAM,CAAC,EAAE,aAAa,CAAC,CAAC,CAAC,CAAC;IAC1B,SAAS,CAAC,EAAE,oBAAoB,CAAC,CAAC,CAAC,CAAC;IACpC,gBAAgB,CAAC,EAAE,iBAAiB,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,CAAC;IAC3D,mBAAmB,CAAC,EAAE,iBAAiB,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC;CAC1D;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,CAAC,GAAG,OAAO,EAAE,OAAO,GAAE,wBAAwB,CAAC,CAAC,CAAM,GAAG,YAAY,CAY5G"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ROUTE_REGISTRY_TOKEN, VIEW_RESOLVER_REGISTRY_TOKEN } from '../../api/index.js';
|
|
2
|
+
import { RouteRegistry } from './route-registry.js';
|
|
3
|
+
import { ViewResolverRegistry } from './view-resolver-registry.js';
|
|
4
|
+
/**
|
|
5
|
+
* Provides {@link ROUTE_REGISTRY_TOKEN} and {@link VIEW_RESOLVER_REGISTRY_TOKEN}.
|
|
6
|
+
* Plugins that contribute routes or resolvers list this plugin in `dependsOn`
|
|
7
|
+
* and register from their own `initialize()`.
|
|
8
|
+
*/
|
|
9
|
+
export function createViewRoutingPlugin(options = {}) {
|
|
10
|
+
const routes = options.routes ?? new RouteRegistry(options.onDuplicateRoute);
|
|
11
|
+
const resolvers = options.resolvers ?? new ViewResolverRegistry(options.onDuplicateResolver);
|
|
12
|
+
return {
|
|
13
|
+
initialize(ctx) {
|
|
14
|
+
ctx.provide({ provide: ROUTE_REGISTRY_TOKEN, useValue: routes });
|
|
15
|
+
ctx.provide({ provide: VIEW_RESOLVER_REGISTRY_TOKEN, useValue: resolvers });
|
|
16
|
+
},
|
|
17
|
+
activate() {
|
|
18
|
+
/* registries are live from initialize */
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/templates/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,cAAc,gBAAgB,CAAC"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { type ComponentType, type ReactNode } from 'react';
|
|
2
|
+
import { type DirectoryNode, type FileSystemNode } from '@hamak/shared-utils';
|
|
3
|
+
import type { IRouteRegistry, IViewResolverRegistry, RouteContribution, StoreFsNodeRef } from '../api/index.js';
|
|
4
|
+
/** Props of a component a view resolver returns. */
|
|
5
|
+
export interface ResolvedViewProps<T = unknown> {
|
|
6
|
+
node: StoreFsNodeRef<T>;
|
|
7
|
+
}
|
|
8
|
+
export type ViewComponent = ComponentType<ResolvedViewProps<any>>;
|
|
9
|
+
export type RouteComponent = ComponentType<any>;
|
|
10
|
+
export interface ViewRoutingContextValue {
|
|
11
|
+
routes?: IRouteRegistry<RouteComponent>;
|
|
12
|
+
resolvers?: IViewResolverRegistry<ViewComponent>;
|
|
13
|
+
}
|
|
14
|
+
export declare const ViewRoutingContext: import("react").Context<ViewRoutingContextValue>;
|
|
15
|
+
export interface ViewRoutingProviderProps extends ViewRoutingContextValue {
|
|
16
|
+
children?: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
/** Makes the registries available to {@link ContentView} and the hooks below. */
|
|
19
|
+
export declare function ViewRoutingProvider({ routes, resolvers, children }: ViewRoutingProviderProps): import("react").FunctionComponentElement<import("react").ProviderProps<ViewRoutingContextValue>>;
|
|
20
|
+
/** The contributed routes, re-read whenever the registry changes. */
|
|
21
|
+
export declare function useRouteContributions(registry?: IRouteRegistry<RouteComponent>): readonly RouteContribution<RouteComponent>[];
|
|
22
|
+
/** The part of a store-fs facade {@link ContentView} reads through. */
|
|
23
|
+
export interface StoreFsNodeSelectorFactory {
|
|
24
|
+
createSelector(path: string | string[]): (state: any) => FileSystemNode | undefined;
|
|
25
|
+
}
|
|
26
|
+
/** What {@link ContentView} renders when no resolver claims a file: its content, as text. */
|
|
27
|
+
export declare function DefaultContentView({ node }: ResolvedViewProps): import("react").ReactElement<{
|
|
28
|
+
'data-content-type': string;
|
|
29
|
+
}, string | import("react").JSXElementConstructor<any>>;
|
|
30
|
+
export interface ContentViewProps {
|
|
31
|
+
/** The store-fs path of the node to display. */
|
|
32
|
+
path: string | string[];
|
|
33
|
+
fs: StoreFsNodeSelectorFactory;
|
|
34
|
+
/** Defaults to the registry of the enclosing {@link ViewRoutingProvider}. */
|
|
35
|
+
resolvers?: IViewResolverRegistry<ViewComponent>;
|
|
36
|
+
/** Rendered for a file no resolver claims. Defaults to {@link DefaultContentView}. */
|
|
37
|
+
fallback?: ViewComponent;
|
|
38
|
+
/** Rendered for a directory node. Defaults to nothing. */
|
|
39
|
+
directory?: ComponentType<{
|
|
40
|
+
path: string[];
|
|
41
|
+
node: DirectoryNode;
|
|
42
|
+
}>;
|
|
43
|
+
/** Rendered while the path names no node. Defaults to nothing. */
|
|
44
|
+
missing?: ReactNode;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The generic content route: observes one store-fs node and renders the
|
|
48
|
+
* component the view resolvers return for it. A path that runs past a file
|
|
49
|
+
* (`a.json/nope`, `a.json/a.json`) names no node and renders `missing`.
|
|
50
|
+
*
|
|
51
|
+
* It reads the store and nothing else. A change to the node re-renders the view
|
|
52
|
+
* with the new content, and loading the node is the host's business: this
|
|
53
|
+
* component dispatches nothing. A resolver registered or removed later takes
|
|
54
|
+
* effect on the next render; with every resolver passing, `fallback` renders.
|
|
55
|
+
*
|
|
56
|
+
* Resolvers should return a stable component (not one created per call), or
|
|
57
|
+
* the view remounts on every change.
|
|
58
|
+
*/
|
|
59
|
+
export declare function ContentView({ path, fs, resolvers, fallback, directory, missing }: ContentViewProps): import("react").FunctionComponentElement<{
|
|
60
|
+
children?: ReactNode | undefined;
|
|
61
|
+
}> | import("react").ReactElement<{
|
|
62
|
+
path: string[];
|
|
63
|
+
node: DirectoryNode;
|
|
64
|
+
}, string | import("react").JSXElementConstructor<any>> | import("react").ReactElement<ResolvedViewProps<any>, string | import("react").JSXElementConstructor<any>> | null;
|
|
65
|
+
//# sourceMappingURL=view-routing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"view-routing.d.ts","sourceRoot":"","sources":["../../src/templates/view-routing.ts"],"names":[],"mappings":"AAAA,OAAO,EAQL,KAAK,aAAa,EAClB,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAEf,OAAO,EAAa,KAAK,aAAa,EAAE,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AACzF,OAAO,KAAK,EACV,cAAc,EACd,qBAAqB,EACrB,iBAAiB,EACjB,cAAc,EACf,MAAM,QAAQ,CAAC;AAGhB,oDAAoD;AACpD,MAAM,WAAW,iBAAiB,CAAC,CAAC,GAAG,OAAO;IAC5C,IAAI,EAAE,cAAc,CAAC,CAAC,CAAC,CAAC;CACzB;AAED,MAAM,MAAM,aAAa,GAAG,aAAa,CAAC,iBAAiB,CAAC,GAAG,CAAC,CAAC,CAAC;AAClE,MAAM,MAAM,cAAc,GAAG,aAAa,CAAC,GAAG,CAAC,CAAC;AAEhD,MAAM,WAAW,uBAAuB;IACtC,MAAM,CAAC,EAAE,cAAc,CAAC,cAAc,CAAC,CAAC;IACxC,SAAS,CAAC,EAAE,qBAAqB,CAAC,aAAa,CAAC,CAAC;CAClD;AAED,eAAO,MAAM,kBAAkB,kDAA6C,CAAC;AAE7E,MAAM,WAAW,wBAAyB,SAAQ,uBAAuB;IACvE,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB;AAED,iFAAiF;AACjF,wBAAgB,mBAAmB,CAAC,EAAE,MAAM,EAAE,SAAS,EAAE,QAAQ,EAAE,EAAE,wBAAwB,oGAG5F;AAcD,qEAAqE;AACrE,wBAAgB,qBAAqB,CACnC,QAAQ,CAAC,EAAE,cAAc,CAAC,cAAc,CAAC,GACxC,SAAS,iBAAiB,CAAC,cAAc,CAAC,EAAE,CAM9C;AAED,uEAAuE;AACvE,MAAM,WAAW,0BAA0B;IACzC,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,CAAC,KAAK,EAAE,GAAG,KAAK,cAAc,GAAG,SAAS,CAAC;CACrF;AAsBD,6FAA6F;AAC7F,wBAAgB,kBAAkB,CAAC,EAAE,IAAI,EAAE,EAAE,iBAAiB;;wDAG7D;AAED,MAAM,WAAW,gBAAgB;IAC/B,gDAAgD;IAChD,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACxB,EAAE,EAAE,0BAA0B,CAAC;IAC/B,6EAA6E;IAC7E,SAAS,CAAC,EAAE,qBAAqB,CAAC,aAAa,CAAC,CAAC;IACjD,sFAAsF;IACtF,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB,0DAA0D;IAC1D,SAAS,CAAC,EAAE,aAAa,CAAC;QAAE,IAAI,EAAE,MAAM,EAAE,CAAC;QAAC,IAAI,EAAE,aAAa,CAAA;KAAE,CAAC,CAAC;IACnE,kEAAkE;IAClE,OAAO,CAAC,EAAE,SAAS,CAAC;CACrB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,WAAW,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,SAAS,EAAE,QAA6B,EAAE,SAAS,EAAE,OAAc,EAAE,EAAE,gBAAgB;;;UAlB3F,MAAM,EAAE;UAAQ,aAAa;2KAwChE"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { createContext, createElement, Fragment, useContext, useEffect, useMemo, useState, } from 'react';
|
|
2
|
+
import { useSelector } from 'react-redux';
|
|
3
|
+
import { pathSteps } from '@hamak/shared-utils';
|
|
4
|
+
import { storeFsNodeRef } from '../impl/view-routing/store-fs-node-ref.js';
|
|
5
|
+
export const ViewRoutingContext = createContext({});
|
|
6
|
+
/** Makes the registries available to {@link ContentView} and the hooks below. */
|
|
7
|
+
export function ViewRoutingProvider({ routes, resolvers, children }) {
|
|
8
|
+
const value = useMemo(() => ({ routes, resolvers }), [routes, resolvers]);
|
|
9
|
+
return createElement(ViewRoutingContext.Provider, { value }, children);
|
|
10
|
+
}
|
|
11
|
+
/** A counter that moves on every registry change, so readers re-render. */
|
|
12
|
+
function useRegistryVersion(registry) {
|
|
13
|
+
const [version, setVersion] = useState(0);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (registry === undefined)
|
|
16
|
+
return undefined;
|
|
17
|
+
// A change between render and subscription would otherwise be missed.
|
|
18
|
+
setVersion((v) => v + 1);
|
|
19
|
+
return registry.subscribe(() => setVersion((v) => v + 1));
|
|
20
|
+
}, [registry]);
|
|
21
|
+
return version;
|
|
22
|
+
}
|
|
23
|
+
/** The contributed routes, re-read whenever the registry changes. */
|
|
24
|
+
export function useRouteContributions(registry) {
|
|
25
|
+
const contextRoutes = useContext(ViewRoutingContext).routes;
|
|
26
|
+
const routes = registry ?? contextRoutes;
|
|
27
|
+
const version = useRegistryVersion(routes);
|
|
28
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
29
|
+
return useMemo(() => routes?.list() ?? [], [routes, version]);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Selects the node at exactly `steps`. A store-fs selector stops at a file and
|
|
33
|
+
* returns it for any deeper step, so `a.json/nope` and even `a.json/a.json`
|
|
34
|
+
* would select `a.json`. The node only counts when the parent path selects a
|
|
35
|
+
* directory holding it under the last step; past a file, the parent path
|
|
36
|
+
* selects that file instead. The result is the node itself (or `undefined`),
|
|
37
|
+
* so a change to a sibling does not re-render.
|
|
38
|
+
*/
|
|
39
|
+
function strictNodeSelector(fs, steps) {
|
|
40
|
+
const nodeAt = fs.createSelector(steps);
|
|
41
|
+
if (steps.length === 0)
|
|
42
|
+
return nodeAt;
|
|
43
|
+
const parentAt = fs.createSelector(steps.slice(0, -1));
|
|
44
|
+
const name = steps[steps.length - 1];
|
|
45
|
+
return (state) => {
|
|
46
|
+
const node = nodeAt(state);
|
|
47
|
+
const parent = parentAt(state);
|
|
48
|
+
return node !== undefined && parent?.type === 'directory' && parent.children[name] === node ? node : undefined;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/** What {@link ContentView} renders when no resolver claims a file: its content, as text. */
|
|
52
|
+
export function DefaultContentView({ node }) {
|
|
53
|
+
const text = typeof node.content === 'string' ? node.content : JSON.stringify(node.content, null, 2);
|
|
54
|
+
return createElement('pre', { 'data-content-type': node.contentType }, text ?? '');
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The generic content route: observes one store-fs node and renders the
|
|
58
|
+
* component the view resolvers return for it. A path that runs past a file
|
|
59
|
+
* (`a.json/nope`, `a.json/a.json`) names no node and renders `missing`.
|
|
60
|
+
*
|
|
61
|
+
* It reads the store and nothing else. A change to the node re-renders the view
|
|
62
|
+
* with the new content, and loading the node is the host's business: this
|
|
63
|
+
* component dispatches nothing. A resolver registered or removed later takes
|
|
64
|
+
* effect on the next render; with every resolver passing, `fallback` renders.
|
|
65
|
+
*
|
|
66
|
+
* Resolvers should return a stable component (not one created per call), or
|
|
67
|
+
* the view remounts on every change.
|
|
68
|
+
*/
|
|
69
|
+
export function ContentView({ path, fs, resolvers, fallback = DefaultContentView, directory, missing = null }) {
|
|
70
|
+
const contextResolvers = useContext(ViewRoutingContext).resolvers;
|
|
71
|
+
const registry = resolvers ?? contextResolvers;
|
|
72
|
+
const version = useRegistryVersion(registry);
|
|
73
|
+
const pathKey = JSON.stringify(path);
|
|
74
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
75
|
+
const steps = useMemo(() => pathSteps(path), [pathKey]);
|
|
76
|
+
const selector = useMemo(() => strictNodeSelector(fs, steps), [fs, steps]);
|
|
77
|
+
const node = useSelector(selector);
|
|
78
|
+
const ref = useMemo(() => storeFsNodeRef(path, node), [node, pathKey]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
79
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
80
|
+
const resolved = useMemo(() => (ref === undefined ? undefined : registry?.resolve(ref)), [registry, version, ref]);
|
|
81
|
+
if (node === undefined)
|
|
82
|
+
return createElement(Fragment, null, missing);
|
|
83
|
+
if (node.type === 'directory') {
|
|
84
|
+
return directory === undefined
|
|
85
|
+
? null
|
|
86
|
+
: createElement(directory, { path: steps, node });
|
|
87
|
+
}
|
|
88
|
+
return createElement(resolved?.component ?? fallback, { node: ref });
|
|
89
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hamak/ui-shell",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.0",
|
|
4
4
|
"description": "UI Shell Framework with theming, routing, and layout management",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -20,6 +20,11 @@
|
|
|
20
20
|
"types": "./dist/spi/index.d.ts",
|
|
21
21
|
"import": "./dist/spi/index.js",
|
|
22
22
|
"default": "./dist/spi/index.js"
|
|
23
|
+
},
|
|
24
|
+
"./react": {
|
|
25
|
+
"types": "./dist/templates/index.d.ts",
|
|
26
|
+
"import": "./dist/templates/index.js",
|
|
27
|
+
"default": "./dist/templates/index.js"
|
|
23
28
|
}
|
|
24
29
|
},
|
|
25
30
|
"typesVersions": {
|
|
@@ -29,6 +34,9 @@
|
|
|
29
34
|
],
|
|
30
35
|
"spi": [
|
|
31
36
|
"./dist/spi/index.d.ts"
|
|
37
|
+
],
|
|
38
|
+
"react": [
|
|
39
|
+
"./dist/templates/index.d.ts"
|
|
32
40
|
]
|
|
33
41
|
}
|
|
34
42
|
},
|
|
@@ -45,7 +53,8 @@
|
|
|
45
53
|
},
|
|
46
54
|
"scripts": {
|
|
47
55
|
"build": "tsc -p tsconfig.lib.json && node ../../scripts/fix-esm-extensions.mjs ./dist",
|
|
48
|
-
"clean": "rm -rf dist"
|
|
56
|
+
"clean": "rm -rf dist",
|
|
57
|
+
"test": "vitest run"
|
|
49
58
|
},
|
|
50
59
|
"keywords": [
|
|
51
60
|
"ui-shell",
|
|
@@ -59,9 +68,31 @@
|
|
|
59
68
|
"license": "MIT",
|
|
60
69
|
"dependencies": {
|
|
61
70
|
"@hamak/microkernel-api": "*",
|
|
62
|
-
"@hamak/microkernel-spi": "*"
|
|
71
|
+
"@hamak/microkernel-spi": "*",
|
|
72
|
+
"@hamak/shared-utils": "*"
|
|
63
73
|
},
|
|
64
74
|
"devDependencies": {
|
|
65
|
-
"typescript": "^5.9.3"
|
|
75
|
+
"typescript": "^5.9.3",
|
|
76
|
+
"@types/react": "^18.2.0",
|
|
77
|
+
"react": "^18.2.0",
|
|
78
|
+
"react-redux": "^9.1.0",
|
|
79
|
+
"vitest": "^2.0.0",
|
|
80
|
+
"@hamak/microkernel-impl": "*",
|
|
81
|
+
"@hamak/ui-store": "*",
|
|
82
|
+
"@reduxjs/toolkit": "^2.0.0",
|
|
83
|
+
"@testing-library/react": "^14.0.0",
|
|
84
|
+
"jsdom": "^27.0.0"
|
|
85
|
+
},
|
|
86
|
+
"peerDependencies": {
|
|
87
|
+
"react": "^18.2.0",
|
|
88
|
+
"react-redux": "^9.1.0"
|
|
89
|
+
},
|
|
90
|
+
"peerDependenciesMeta": {
|
|
91
|
+
"react": {
|
|
92
|
+
"optional": true
|
|
93
|
+
},
|
|
94
|
+
"react-redux": {
|
|
95
|
+
"optional": true
|
|
96
|
+
}
|
|
66
97
|
}
|
|
67
98
|
}
|