@swimmesberger/elarion-contributions 0.0.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 ADDED
@@ -0,0 +1,111 @@
1
+ # @swimmesberger/elarion-contributions
2
+
3
+ The [Elarion](https://github.com/swimmesberger/Elarion) **frontend contribution model**
4
+ ([ADR-0032](https://github.com/swimmesberger/Elarion/blob/main/docs/decisions/0032-frontend-contribution-model.md)):
5
+ typed extension-point tokens, declarative module manifests, and capability-gated, deterministic resolution.
6
+ It extends the backend's review-isolation rule — *a module only touches its own code* — to the frontend:
7
+ adding a sidebar item or a cross-module action edits one file in the owning module, never the shell.
8
+
9
+ - **Framework-free core** (`@swimmesberger/elarion-contributions`) — no dependencies, no React.
10
+ - **React bindings** (`@swimmesberger/elarion-contributions/react`) — `ContributionProvider`,
11
+ `useContributions`, `<ExtensionSlot>`. React is an optional peer dependency; other view frameworks
12
+ can port the bindings in a page of code.
13
+ - **TanStack Router guard** (`@swimmesberger/elarion-contributions/tanstack-router`) —
14
+ `redirectUnless(when, to)` + the vocabulary-bound `createRouteGuards<V>()`: a `beforeLoad` guard
15
+ evaluating the same `when` clause as a contribution, so a route and its nav entry can never gate
16
+ differently. `@tanstack/react-router` is an optional peer; everything else routing-related stays
17
+ the router's own API.
18
+
19
+ ```bash
20
+ npm install @swimmesberger/elarion-contributions
21
+ ```
22
+
23
+ ## What you import vs. what you own
24
+
25
+ This package ships the *machinery* with fixed semantics; your application owns the *points* and the shell
26
+ (Elarion deliberately ships no UI kit and no router integration):
27
+
28
+ | You import (fixed semantics) | You own (copy from the sample) |
29
+ | --- | --- |
30
+ | `defineExtensionPoint`, `defineModule`, `contribute` | Your extension points (`sidebarItems`, …) and their payload types |
31
+ | `evaluateWhen` + the `when` AND semantics | The kit instantiation binding your generated vocabulary |
32
+ | `createContributionRegistry` (filter + deterministic order) | The app shell that renders each slot |
33
+ | `ContributionProvider` / `useContributions` / `<ExtensionSlot>` | Route composition and module discovery (e.g. `import.meta.glob`) |
34
+ | `redirectUnless` route guard (`/tanstack-router`) | Everything else routing — the router's own API |
35
+
36
+ ## Quick start
37
+
38
+ Bind the kernel once to your generated capability vocabulary (from the Elarion client generator's
39
+ `session-client.ts`), so `when` clauses are compile-checked against the catalog the backend enforces:
40
+
41
+ ```ts
42
+ // platform/contributions.ts — the app's one kit
43
+ import { createContributionKit, type ModuleManifest } from "@swimmesberger/elarion-contributions"
44
+ import type { FlagName, ModuleName, PermissionName, RoleName } from "./generated/session-client"
45
+
46
+ export interface AppVocabulary {
47
+ module: ModuleName
48
+ permission: PermissionName
49
+ flag: FlagName
50
+ role: RoleName
51
+ }
52
+ export type AppManifest = ModuleManifest<AppVocabulary>
53
+ export const { defineModule, defineExtensionPoint, contribute } = createContributionKit<AppVocabulary>()
54
+ ```
55
+
56
+ Declare a point where the *owner* of the slot lives (the shell for a sidebar, a module for its own
57
+ extension surface), and export the token from the owner's public entry. The payload shape is yours —
58
+ whatever your shell needs to render:
59
+
60
+ ```ts
61
+ // platform/points.ts
62
+ export interface SidebarItem { readonly label: string; readonly icon: LucideIcon; readonly to: string }
63
+ export const sidebarItems = defineExtensionPoint<SidebarItem>("platform.sidebar")
64
+ ```
65
+
66
+ Modules contribute through their manifest — plain data plus lazy component references, never import-time
67
+ registration:
68
+
69
+ ```ts
70
+ // modules/invoicing/module.tsx
71
+ export const invoicingManifest = defineModule({
72
+ name: Modules.Invoicing,
73
+ when: { module: Modules.Invoicing }, // module-level gate, ANDed into every contribution
74
+ contributes: [
75
+ contribute(sidebarItems, [{
76
+ id: "invoices", label: "Invoices", to: "/invoices", order: 20,
77
+ when: { permission: Permissions.invoices.read },
78
+ }]),
79
+ ],
80
+ })
81
+ ```
82
+
83
+ The composition root resolves once per capability snapshot and hands the registry to React:
84
+
85
+ ```tsx
86
+ import { createContributionRegistry } from "@swimmesberger/elarion-contributions"
87
+ import { ContributionProvider, useContributions } from "@swimmesberger/elarion-contributions/react"
88
+
89
+ const registry = createContributionRegistry(appModules.map((m) => m.manifest), capabilities)
90
+ // <ContributionProvider registry={registry}> … </ContributionProvider>
91
+
92
+ // anywhere in the shell:
93
+ const items = useContributions(sidebarItems)
94
+ ```
95
+
96
+ ## Semantics (the part that must not drift)
97
+
98
+ - **`when` is AND** across its fields (`module`, `permission`, `flag`, `role`), mirroring
99
+ `[RequirePermission]`/`[RequireRole]`/`[FeatureGate]`; a manifest-level `when` is ANDed into every
100
+ contribution. Absent capabilities fail closed.
101
+ - **Resolution is pure and deterministic**: contributions sort by `order` (default 0), then `id`, then
102
+ contributing module name, using code-unit comparison — the same input renders the same UI on server
103
+ and client. Refreshing the snapshot means building a new registry.
104
+ - **UX projection, never security.** A hidden contribution is not a secured operation — the backend
105
+ handler's own gates are the enforcement on every call.
106
+
107
+ ## Reference usage
108
+
109
+ The [Billing sample](https://github.com/swimmesberger/Elarion/tree/main/samples/Billing/web) is the
110
+ living reference: module folders, a shell-owned sidebar point, a cross-module row-action point with a
111
+ lazily-chunked contributed dialog, and TanStack Router route subtrees composed per module.
@@ -0,0 +1,108 @@
1
+ /**
2
+ * The capability vocabulary a contribution kit is typed against — supplied by the application from the
3
+ * generated literal unions in `session-client.ts` (ModuleName/PermissionName/FlagName/RoleName).
4
+ */
5
+ export interface Vocabulary {
6
+ module: string;
7
+ permission: string;
8
+ flag: string;
9
+ role: string;
10
+ }
11
+ /** Keeps literal-union autocomplete while accepting out-of-vocabulary names (the generated accessors' shape). */
12
+ type Hint<T extends string> = T | (string & {});
13
+ /**
14
+ * The declarative visibility condition of a contribution — the frontend mirror of
15
+ * `[RequirePermission]`/`[RequireRole]`/`[FeatureGate]`: every present field must hold (AND).
16
+ */
17
+ export interface WhenClause<V extends Vocabulary = Vocabulary> {
18
+ readonly module?: Hint<V["module"]>;
19
+ readonly permission?: Hint<V["permission"]>;
20
+ readonly flag?: Hint<V["flag"]>;
21
+ readonly role?: Hint<V["role"]>;
22
+ }
23
+ /**
24
+ * The snapshot surface resolution reads — structurally satisfied by the `SessionCapabilities` class the
25
+ * Elarion client generator emits, so this package never depends on generated code.
26
+ */
27
+ export interface CapabilityReader {
28
+ isModuleEnabled(name: string): boolean;
29
+ hasPermission(permission: string): boolean;
30
+ hasRole(role: string): boolean;
31
+ isFlagEnabled(name: string): boolean;
32
+ }
33
+ export declare function evaluateWhen(when: WhenClause | undefined, capabilities: CapabilityReader): boolean;
34
+ declare const CONTRIBUTION: unique symbol;
35
+ declare const SLOT_CONTEXT: unique symbol;
36
+ /**
37
+ * A typed extension-point token. Exporting one from a module's public entry is the frontend
38
+ * [ModuleContract]: contributors import the token — an explicit, compile-checked, correctly-directed
39
+ * dependency — without pulling the owning module's components into their chunk, because the token is data.
40
+ *
41
+ * `TItem` is the payload the point accepts; `TContext` documents what a slot site supplies to the payload's
42
+ * callbacks or components (`void` for context-free slots like a sidebar).
43
+ */
44
+ export interface ExtensionPoint<TItem, TContext = void> {
45
+ /** Globally unique, stable id — convention `{owner}.{point}`, e.g. `"clients.rowActions"`. */
46
+ readonly id: string;
47
+ /** Phantom type markers only — nothing is ever stored under these keys. */
48
+ readonly [CONTRIBUTION]?: TItem;
49
+ readonly [SLOT_CONTEXT]?: TContext;
50
+ }
51
+ export declare function defineExtensionPoint<TItem, TContext = void>(id: string): ExtensionPoint<TItem, TContext>;
52
+ /** A single contribution: the point's payload plus identity, ordering, and visibility. */
53
+ export type Contribution<TItem, V extends Vocabulary = Vocabulary> = TItem & {
54
+ /** Unique within the point across all modules; part of the deterministic sort key. */
55
+ readonly id: string;
56
+ /** Ascending sort rank (default 0); ties break by id, then by contributing module name. */
57
+ readonly order?: number;
58
+ /** Visibility condition, ANDed with the contributing module's manifest-level `when`. */
59
+ readonly when?: WhenClause<V>;
60
+ };
61
+ /** One manifest entry: a point token and the items contributed to it. Built with {@link contribute}. */
62
+ export interface ContributionBatch<V extends Vocabulary = Vocabulary> {
63
+ readonly point: ExtensionPoint<unknown, unknown>;
64
+ readonly items: ReadonlyArray<Contribution<unknown, V>>;
65
+ }
66
+ /** Types a batch against its point: the items must match the point's declared payload. */
67
+ export declare function contribute<TItem, TContext, V extends Vocabulary = Vocabulary>(point: ExtensionPoint<TItem, TContext>, items: ReadonlyArray<Contribution<TItem, V>>): ContributionBatch<V>;
68
+ /**
69
+ * A frontend module's manifest: plain data plus lazy references — the whole surface the composition root
70
+ * sees. Manifests are inspectable without executing module code and testable as plain arrays (the reason
71
+ * VS Code moved contributions into static `package.json` data).
72
+ */
73
+ export interface ModuleManifest<V extends Vocabulary = Vocabulary> {
74
+ /** The module's name — its backend [AppModule] name when it mirrors one, any unique name when UI-only. */
75
+ readonly name: Hint<V["module"]>;
76
+ /**
77
+ * Module-level visibility, ANDed into every contribution. A backend-paired module gates itself here once
78
+ * — `when: { module: Modules.X }` — so disabling the backend module removes the whole frontend module;
79
+ * a UI-only module omits it.
80
+ */
81
+ readonly when?: WhenClause<V>;
82
+ readonly contributes: ReadonlyArray<ContributionBatch<V>>;
83
+ }
84
+ export declare function defineModule<V extends Vocabulary = Vocabulary>(manifest: ModuleManifest<V>): ModuleManifest<V>;
85
+ /** The resolved, filtered, deterministically ordered view of every manifest against one snapshot. */
86
+ export interface ContributionRegistry {
87
+ get<TItem, TContext>(point: ExtensionPoint<TItem, TContext>): ReadonlyArray<Contribution<TItem>>;
88
+ }
89
+ /**
90
+ * Resolves manifests against a capability snapshot. Resolution is eager and pure — the same
91
+ * (manifests, capabilities) input yields the same registry, so a server render and the client hydration
92
+ * produce identical trees. Refreshing the snapshot (login, context change) means building a new registry,
93
+ * the same contract as the generated session provider.
94
+ */
95
+ export declare function createContributionRegistry(manifests: ReadonlyArray<ModuleManifest>, capabilities: CapabilityReader): ContributionRegistry;
96
+ /** The vocabulary-bound facade of the kernel — created once by the application, imported by every module. */
97
+ export interface ContributionKit<V extends Vocabulary> {
98
+ defineModule(manifest: ModuleManifest<V>): ModuleManifest<V>;
99
+ defineExtensionPoint<TItem, TContext = void>(id: string): ExtensionPoint<TItem, TContext>;
100
+ contribute<TItem, TContext>(point: ExtensionPoint<TItem, TContext>, items: ReadonlyArray<Contribution<TItem, V>>): ContributionBatch<V>;
101
+ }
102
+ /**
103
+ * Binds the kernel to an application's capability vocabulary (the generated literal unions), so every
104
+ * `when` clause is compile-checked against the same catalog the backend enforces — a typo'd permission is
105
+ * a type error, not a silently hidden item. Purely a typing layer with zero runtime cost.
106
+ */
107
+ export declare function createContributionKit<V extends Vocabulary>(): ContributionKit<V>;
108
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,89 @@
1
+ // The contribution kernel of the Elarion frontend module system (ADR-0032): typed extension-point tokens,
2
+ // declarative module manifests, and a snapshot-pure resolution step. Framework-free — the React bindings
3
+ // live in the `/react` sub-export, and porting them to another view framework is a page of code.
4
+ //
5
+ // Semantics in one paragraph: a module ships a manifest (plain data plus lazy component references — never
6
+ // import-time `register()` calls, which are import-order-dependent, side-effectful, and invisible to
7
+ // tree-shaking); an extension point is a typed token whose export from a module's public entry is the
8
+ // frontend [ModuleContract]; resolution filters every contribution by its `when` clause (ANDed with the
9
+ // owning manifest's module-level `when`) against the capability snapshot and sorts deterministically, so
10
+ // the same input always renders the same UI on server and client. `when` is a UX projection, never
11
+ // security — the backend's [RequirePermission]/[FeatureGate] remain the enforcement on every call.
12
+ export function evaluateWhen(when, capabilities) {
13
+ if (when === undefined)
14
+ return true;
15
+ if (when.module !== undefined && !capabilities.isModuleEnabled(when.module))
16
+ return false;
17
+ if (when.permission !== undefined && !capabilities.hasPermission(when.permission))
18
+ return false;
19
+ if (when.role !== undefined && !capabilities.hasRole(when.role))
20
+ return false;
21
+ if (when.flag !== undefined && !capabilities.isFlagEnabled(when.flag))
22
+ return false;
23
+ return true;
24
+ }
25
+ export function defineExtensionPoint(id) {
26
+ return { id };
27
+ }
28
+ /** Types a batch against its point: the items must match the point's declared payload. */
29
+ export function contribute(point, items) {
30
+ return { point, items };
31
+ }
32
+ export function defineModule(manifest) {
33
+ return manifest;
34
+ }
35
+ // Code-unit string comparison — localeCompare would make the resolved order environment-dependent.
36
+ function compareStrings(a, b) {
37
+ if (a < b)
38
+ return -1;
39
+ if (a > b)
40
+ return 1;
41
+ return 0;
42
+ }
43
+ const EMPTY = [];
44
+ /**
45
+ * Resolves manifests against a capability snapshot. Resolution is eager and pure — the same
46
+ * (manifests, capabilities) input yields the same registry, so a server render and the client hydration
47
+ * produce identical trees. Refreshing the snapshot (login, context change) means building a new registry,
48
+ * the same contract as the generated session provider.
49
+ */
50
+ export function createContributionRegistry(manifests, capabilities) {
51
+ const byPoint = new Map();
52
+ for (const manifest of manifests) {
53
+ if (!evaluateWhen(manifest.when, capabilities))
54
+ continue;
55
+ for (const batch of manifest.contributes) {
56
+ for (const item of batch.items) {
57
+ if (!evaluateWhen(item.when, capabilities))
58
+ continue;
59
+ const entries = byPoint.get(batch.point.id);
60
+ if (entries === undefined) {
61
+ byPoint.set(batch.point.id, [{ item, module: manifest.name }]);
62
+ }
63
+ else {
64
+ entries.push({ item, module: manifest.name });
65
+ }
66
+ }
67
+ }
68
+ }
69
+ const resolved = new Map();
70
+ for (const [pointId, entries] of byPoint) {
71
+ entries.sort((a, b) => (a.item.order ?? 0) - (b.item.order ?? 0) ||
72
+ compareStrings(a.item.id, b.item.id) ||
73
+ compareStrings(a.module, b.module));
74
+ resolved.set(pointId, entries.map((entry) => entry.item));
75
+ }
76
+ return {
77
+ get(point) {
78
+ return (resolved.get(point.id) ?? EMPTY);
79
+ },
80
+ };
81
+ }
82
+ /**
83
+ * Binds the kernel to an application's capability vocabulary (the generated literal unions), so every
84
+ * `when` clause is compile-checked against the same catalog the backend enforces — a typo'd permission is
85
+ * a type error, not a silently hidden item. Purely a typing layer with zero runtime cost.
86
+ */
87
+ export function createContributionKit() {
88
+ return { defineModule, defineExtensionPoint, contribute };
89
+ }
@@ -0,0 +1,13 @@
1
+ import { type ReactNode } from "react";
2
+ import type { Contribution, ContributionRegistry, ExtensionPoint } from "./index.js";
3
+ export declare function ContributionProvider({ registry, children, }: {
4
+ registry: ContributionRegistry;
5
+ children: ReactNode;
6
+ }): import("react").JSX.Element;
7
+ /** The resolved contributions for a point — already filtered by `when` and deterministically ordered. */
8
+ export declare function useContributions<TItem, TContext>(point: ExtensionPoint<TItem, TContext>): ReadonlyArray<Contribution<TItem>>;
9
+ /** Renders a point's contributions through a render prop — sugar over {@link useContributions} for inline slots. */
10
+ export declare function ExtensionSlot<TItem, TContext>({ point, render, }: {
11
+ point: ExtensionPoint<TItem, TContext>;
12
+ render: (item: Contribution<TItem>) => ReactNode;
13
+ }): import("react").JSX.Element;
package/dist/react.js ADDED
@@ -0,0 +1,22 @@
1
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
2
+ // React bindings over the contribution kernel — the thin `/react` adapter of ADR-0032 (the same split as
3
+ // Elarion.Abstractions ↔ Elarion.AspNetCore): one provider, one hook, one slot component. Everything
4
+ // interesting happens in the framework-free core; this file only surfaces the resolved registry through
5
+ // React context, so porting it to another view framework is a page of code, not a redesign.
6
+ import { createContext, Fragment, useContext } from "react";
7
+ const RegistryContext = createContext(null);
8
+ export function ContributionProvider({ registry, children, }) {
9
+ return _jsx(RegistryContext.Provider, { value: registry, children: children });
10
+ }
11
+ /** The resolved contributions for a point — already filtered by `when` and deterministically ordered. */
12
+ export function useContributions(point) {
13
+ const registry = useContext(RegistryContext);
14
+ if (registry === null)
15
+ throw new Error("useContributions requires a <ContributionProvider> above it.");
16
+ return registry.get(point);
17
+ }
18
+ /** Renders a point's contributions through a render prop — sugar over {@link useContributions} for inline slots. */
19
+ export function ExtensionSlot({ point, render, }) {
20
+ const items = useContributions(point);
21
+ return (_jsx(_Fragment, { children: items.map((item) => (_jsx(Fragment, { children: render(item) }, item.id))) }));
22
+ }
@@ -0,0 +1,28 @@
1
+ import { type CapabilityReader, type Vocabulary, type WhenClause } from "./index.js";
2
+ /** The router-context shape the guard reads — the app's router context carries the capability snapshot. */
3
+ export interface GuardContext {
4
+ readonly context: {
5
+ readonly caps: CapabilityReader;
6
+ };
7
+ }
8
+ /**
9
+ * A `beforeLoad` guard: redirects to `to` unless the `when` clause holds against the capability snapshot
10
+ * in the router context — the route-level mirror of a contribution's `when`, so a deep link into a hidden
11
+ * module bounces instead of rendering. UX only: the handlers behind the route still enforce their own
12
+ * `[RequirePermission]`/`[FeatureGate]` on every call.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * createRoute({ path: "/invoices", beforeLoad: redirectUnless({ module: "Invoicing" }, "/"), … })
17
+ * ```
18
+ */
19
+ export declare function redirectUnless<V extends Vocabulary = Vocabulary>(when: WhenClause<V>, to: string): (opts: GuardContext) => void;
20
+ /** The vocabulary-bound guard factory — the router-side sibling of `createContributionKit`. */
21
+ export interface RouteGuards<V extends Vocabulary> {
22
+ redirectUnless(when: WhenClause<V>, to: string): (opts: GuardContext) => void;
23
+ }
24
+ /**
25
+ * Binds the route guards to an application's capability vocabulary, so a route's `when` clause is
26
+ * compile-checked against the same generated literal unions as a manifest's. Purely a typing layer.
27
+ */
28
+ export declare function createRouteGuards<V extends Vocabulary>(): RouteGuards<V>;
@@ -0,0 +1,32 @@
1
+ // TanStack Router adapter — deliberately a single semantic helper, not routing machinery (an ADR-0032
2
+ // non-goal): routes stay module-owned TanStack subtrees composed with the router's own API. What the
3
+ // framework owns is the `when` clause, and a route guard must evaluate it with exactly the slot-filter
4
+ // semantics (AND, fail-closed) — this sub-export exists so that never drifts. The guard takes the same
5
+ // `when` object a sidebar item declares, so "this page and its nav entry gate identically" is one literal.
6
+ import { redirect } from "@tanstack/react-router";
7
+ import { evaluateWhen } from "./index.js";
8
+ /**
9
+ * A `beforeLoad` guard: redirects to `to` unless the `when` clause holds against the capability snapshot
10
+ * in the router context — the route-level mirror of a contribution's `when`, so a deep link into a hidden
11
+ * module bounces instead of rendering. UX only: the handlers behind the route still enforce their own
12
+ * `[RequirePermission]`/`[FeatureGate]` on every call.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * createRoute({ path: "/invoices", beforeLoad: redirectUnless({ module: "Invoicing" }, "/"), … })
17
+ * ```
18
+ */
19
+ export function redirectUnless(when, to) {
20
+ return ({ context }) => {
21
+ if (!evaluateWhen(when, context.caps)) {
22
+ throw redirect({ to });
23
+ }
24
+ };
25
+ }
26
+ /**
27
+ * Binds the route guards to an application's capability vocabulary, so a route's `when` clause is
28
+ * compile-checked against the same generated literal unions as a manifest's. Purely a typing layer.
29
+ */
30
+ export function createRouteGuards() {
31
+ return { redirectUnless };
32
+ }
package/package.json ADDED
@@ -0,0 +1,66 @@
1
+ {
2
+ "name": "@swimmesberger/elarion-contributions",
3
+ "version": "0.0.0",
4
+ "description": "The Elarion frontend contribution model: typed extension points, declarative module manifests, and capability-gated resolution — framework-free core with React bindings under /react.",
5
+ "license": "Apache-2.0",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ },
12
+ "./react": {
13
+ "types": "./dist/react.d.ts",
14
+ "import": "./dist/react.js"
15
+ },
16
+ "./tanstack-router": {
17
+ "types": "./dist/tanstack-router.d.ts",
18
+ "import": "./dist/tanstack-router.js"
19
+ }
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "README.md"
24
+ ],
25
+ "scripts": {
26
+ "build": "tsc -p tsconfig.json",
27
+ "typecheck": "tsc --noEmit -p tsconfig.json",
28
+ "test": "vitest run",
29
+ "prepack": "npm run build",
30
+ "prepare": "npm run build"
31
+ },
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/swimmesberger/Elarion.git",
35
+ "directory": "src/elarion-contributions"
36
+ },
37
+ "bugs": {
38
+ "url": "https://github.com/swimmesberger/Elarion/issues"
39
+ },
40
+ "homepage": "https://github.com/swimmesberger/Elarion#readme",
41
+ "publishConfig": {
42
+ "access": "public",
43
+ "registry": "https://registry.npmjs.org/"
44
+ },
45
+ "engines": {
46
+ "node": ">=20.11"
47
+ },
48
+ "peerDependencies": {
49
+ "@tanstack/react-router": ">=1",
50
+ "react": ">=18"
51
+ },
52
+ "peerDependenciesMeta": {
53
+ "react": {
54
+ "optional": true
55
+ },
56
+ "@tanstack/react-router": {
57
+ "optional": true
58
+ }
59
+ },
60
+ "devDependencies": {
61
+ "@tanstack/react-router": "^1.170.17",
62
+ "@types/react": "^19.2.17",
63
+ "typescript": "^5.9.3",
64
+ "vitest": "^4.0.18"
65
+ }
66
+ }