@m6d/cerebro 0.1.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 +251 -0
- package/dist/access/navigation.d.ts +21 -0
- package/dist/access/route-access.d.ts +45 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +381 -0
- package/dist/internal/access-keys.d.ts +12 -0
- package/dist/internal/records.d.ts +2 -0
- package/dist/internal/store.d.ts +8 -0
- package/dist/licensing/feature-http-loader.d.ts +37 -0
- package/dist/licensing/feature-store.d.ts +73 -0
- package/dist/licensing/feature-utils.d.ts +39 -0
- package/package.json +57 -0
package/README.md
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# Cerebro JavaScript SDK
|
|
2
|
+
|
|
3
|
+
Reusable frontend licensing primitives for JavaScript/TypeScript clients.
|
|
4
|
+
|
|
5
|
+
## What This SDK Covers
|
|
6
|
+
|
|
7
|
+
The SDK focuses on generic client-side licensing patterns.
|
|
8
|
+
|
|
9
|
+
It provides framework-agnostic building blocks for:
|
|
10
|
+
|
|
11
|
+
- Loading licensed features and checking feature availability
|
|
12
|
+
- Evaluating route access constraints (features only)
|
|
13
|
+
- Filtering navigation trees by access requirements
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
bun add @m6d/cerebro
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
For local development in this repository:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
bun install
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Development Scripts
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
bun lint
|
|
31
|
+
bun check
|
|
32
|
+
bun test
|
|
33
|
+
bun run build
|
|
34
|
+
bun run sample:feature-store
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## CI/CD and Publishing
|
|
38
|
+
|
|
39
|
+
- PRs targeting `main` run quality checks via `.github/workflows/pr-checks.yml`
|
|
40
|
+
- Pushes to `main` run semantic-release via `.github/workflows/release.yml`
|
|
41
|
+
- Releases use npm trusted publishing (OIDC), not long-lived npm tokens
|
|
42
|
+
|
|
43
|
+
To enable trusted publishing for `@m6d/cerebro`:
|
|
44
|
+
|
|
45
|
+
1. Open the package settings on npmjs.com for `@m6d/cerebro`
|
|
46
|
+
2. Add a Trusted Publisher for GitHub Actions
|
|
47
|
+
3. Set Organization/User, Repository, and workflow filename to `release.yml`
|
|
48
|
+
4. Ensure publishes run on GitHub-hosted runners (this workflow uses `ubuntu-latest`)
|
|
49
|
+
|
|
50
|
+
## Documentation
|
|
51
|
+
|
|
52
|
+
- `docs/README.md`
|
|
53
|
+
- `docs/licensing/README.md`
|
|
54
|
+
- `docs/routing/README.md`
|
|
55
|
+
|
|
56
|
+
## Samples
|
|
57
|
+
|
|
58
|
+
- `samples/README.md`
|
|
59
|
+
- `samples/vanilla-feature-store-sample/README.md`
|
|
60
|
+
|
|
61
|
+
## Generic Example
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import {
|
|
65
|
+
createFeatureStore,
|
|
66
|
+
createLicenseFeatureLoader,
|
|
67
|
+
evaluateRouteAccess,
|
|
68
|
+
} from "@m6d/cerebro";
|
|
69
|
+
|
|
70
|
+
const featureStore = createFeatureStore({
|
|
71
|
+
loadFeatures: createLicenseFeatureLoader({
|
|
72
|
+
url: "https://example.com/licenses/features",
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
await featureStore.load();
|
|
77
|
+
|
|
78
|
+
const decision = evaluateRouteAccess(
|
|
79
|
+
{
|
|
80
|
+
featureIds: ["kpis", "plans"],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
features: featureStore.getState().features,
|
|
84
|
+
},
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
if (!decision.allowed) {
|
|
88
|
+
console.log("Blocked", decision.reason, decision);
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## Use Cases Coverage
|
|
93
|
+
|
|
94
|
+
These are common licensing use cases and how they map to this SDK:
|
|
95
|
+
|
|
96
|
+
- Route feature guard (`featureIds`) -> `evaluateRouteAccess(...)` or `canAccessRoute(...)`
|
|
97
|
+
- Filtering navigation/app lists by licensed modules -> `filterAccessibleItems(...)`
|
|
98
|
+
- Inline checks inside components/pages -> `featureStore.hasFeature(...)`, `featureStore.hasAnyFeature(...)`, `hasFeature(...)`
|
|
99
|
+
- Bootstrapping and refreshing licensed features -> `createLicenseFeatureLoader(...)` + `featureStore.load()`
|
|
100
|
+
|
|
101
|
+
This SDK intentionally does not include permission logic, notifications, router bindings, or framework-specific state wrappers.
|
|
102
|
+
|
|
103
|
+
## Angular Example
|
|
104
|
+
|
|
105
|
+
```ts
|
|
106
|
+
// app/core/licensing.service.ts
|
|
107
|
+
import { Injectable } from "@angular/core";
|
|
108
|
+
import {
|
|
109
|
+
createFeatureStore,
|
|
110
|
+
createLicenseFeatureLoader,
|
|
111
|
+
evaluateRouteAccess,
|
|
112
|
+
} from "@m6d/cerebro";
|
|
113
|
+
|
|
114
|
+
@Injectable({ providedIn: "root" })
|
|
115
|
+
export class LicensingService {
|
|
116
|
+
private featureStore = createFeatureStore({
|
|
117
|
+
loadFeatures: createLicenseFeatureLoader({
|
|
118
|
+
url: "/licenses/features",
|
|
119
|
+
credentials: "include",
|
|
120
|
+
}),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
async init() {
|
|
124
|
+
await this.featureStore.load();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
hasFeature(featureId: string) {
|
|
128
|
+
return this.featureStore.hasFeature(featureId);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
canAccess(featureIds: readonly string[]) {
|
|
132
|
+
return evaluateRouteAccess(
|
|
133
|
+
{ featureIds },
|
|
134
|
+
{ features: this.featureStore.getState().features },
|
|
135
|
+
).allowed;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
```ts
|
|
141
|
+
// app/core/feature.guard.ts
|
|
142
|
+
import { inject } from "@angular/core";
|
|
143
|
+
import { CanActivateFn } from "@angular/router";
|
|
144
|
+
import { LicensingService } from "./licensing.service";
|
|
145
|
+
|
|
146
|
+
export const featureGuard: CanActivateFn = (route) => {
|
|
147
|
+
const licensing = inject(LicensingService);
|
|
148
|
+
const featureIds = (route.data?.["featureIds"] as string[] | undefined) ?? [];
|
|
149
|
+
return licensing.canAccess(featureIds);
|
|
150
|
+
};
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
```ts
|
|
154
|
+
// app/app.config.ts (or APP_INITIALIZER module setup)
|
|
155
|
+
import { APP_INITIALIZER } from "@angular/core";
|
|
156
|
+
import { LicensingService } from "./core/licensing.service";
|
|
157
|
+
|
|
158
|
+
export const licensingInitializer = {
|
|
159
|
+
provide: APP_INITIALIZER,
|
|
160
|
+
multi: true,
|
|
161
|
+
deps: [LicensingService],
|
|
162
|
+
useFactory: (licensing: LicensingService) => () => licensing.init(),
|
|
163
|
+
};
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## React Example
|
|
167
|
+
|
|
168
|
+
```tsx
|
|
169
|
+
import {
|
|
170
|
+
createFeatureStore,
|
|
171
|
+
createLicenseFeatureLoader,
|
|
172
|
+
evaluateRouteAccess,
|
|
173
|
+
} from "@m6d/cerebro";
|
|
174
|
+
import {
|
|
175
|
+
createContext,
|
|
176
|
+
useContext,
|
|
177
|
+
useEffect,
|
|
178
|
+
useMemo,
|
|
179
|
+
useSyncExternalStore,
|
|
180
|
+
} from "react";
|
|
181
|
+
|
|
182
|
+
const featureStore = createFeatureStore({
|
|
183
|
+
loadFeatures: createLicenseFeatureLoader({
|
|
184
|
+
url: "/licenses/features",
|
|
185
|
+
credentials: "include",
|
|
186
|
+
}),
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
type LicensingContextValue = {
|
|
190
|
+
features: string[];
|
|
191
|
+
isLoading: boolean;
|
|
192
|
+
hasFeature: (featureId: string) => boolean;
|
|
193
|
+
canAccess: (featureIds: readonly string[]) => boolean;
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
const LicensingContext = createContext<LicensingContextValue | null>(null);
|
|
197
|
+
|
|
198
|
+
export function LicensingProvider({ children }: { children: React.ReactNode }) {
|
|
199
|
+
const state = useSyncExternalStore(
|
|
200
|
+
featureStore.subscribe,
|
|
201
|
+
featureStore.getState,
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
useEffect(() => {
|
|
205
|
+
featureStore.load().catch(() => undefined);
|
|
206
|
+
}, []);
|
|
207
|
+
|
|
208
|
+
const value = useMemo<LicensingContextValue>(() => {
|
|
209
|
+
return {
|
|
210
|
+
features: state.features,
|
|
211
|
+
isLoading: state.isLoading,
|
|
212
|
+
hasFeature: (featureId) => featureStore.hasFeature(featureId),
|
|
213
|
+
canAccess: (featureIds) => {
|
|
214
|
+
return evaluateRouteAccess({ featureIds }, { features: state.features })
|
|
215
|
+
.allowed;
|
|
216
|
+
},
|
|
217
|
+
};
|
|
218
|
+
}, [state.features, state.isLoading]);
|
|
219
|
+
|
|
220
|
+
return <LicensingContext value={value}>{children}</LicensingContext>;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function useLicensing() {
|
|
224
|
+
const value = useContext(LicensingContext);
|
|
225
|
+
if (!value) {
|
|
226
|
+
throw new Error("useLicensing must be used inside <LicensingProvider>");
|
|
227
|
+
}
|
|
228
|
+
return value;
|
|
229
|
+
}
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
```tsx
|
|
233
|
+
// any component
|
|
234
|
+
import { useLicensing } from "./licensing-context";
|
|
235
|
+
|
|
236
|
+
function KpiPage() {
|
|
237
|
+
const licensing = useLicensing();
|
|
238
|
+
|
|
239
|
+
if (!licensing.canAccess(["kpis"])) {
|
|
240
|
+
return <div>Feature not available in your license.</div>;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return <div>KPI content</div>;
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Notes
|
|
248
|
+
|
|
249
|
+
- Response parsers support both direct payloads and `Result<T>`-style payloads (`{ extra: ... }`)
|
|
250
|
+
- Feature checks default to case-insensitive matching
|
|
251
|
+
- Route `featureIds` default to `any` mode
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type RouteAccessContext, type RouteAccessRequirement } from "./route-access";
|
|
2
|
+
/**
|
|
3
|
+
* Navigation item shape that supports feature-gated filtering.
|
|
4
|
+
*/
|
|
5
|
+
export type AccessControlledItem = RouteAccessRequirement & {
|
|
6
|
+
/** Child items that should be recursively filtered. */
|
|
7
|
+
children?: readonly AccessControlledItem[];
|
|
8
|
+
/** Additional consumer-defined fields. */
|
|
9
|
+
[key: string]: unknown;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Options used while filtering navigation items.
|
|
13
|
+
*/
|
|
14
|
+
export type FilterAccessibleItemsOptions = RouteAccessContext & {
|
|
15
|
+
/** Keep a parent item visible when any child remains visible. Defaults to `true`. */
|
|
16
|
+
keepParentWhenAnyChildAccessible?: boolean;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Filters a navigation tree and keeps only accessible items.
|
|
20
|
+
*/
|
|
21
|
+
export declare const filterAccessibleItems: (items: readonly AccessControlledItem[], options?: FilterAccessibleItemsOptions) => AccessControlledItem[];
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { type AccessRequirementMode } from "../internal/access-keys";
|
|
2
|
+
import { type FeatureCheckOptions } from "../licensing/feature-utils";
|
|
3
|
+
/**
|
|
4
|
+
* Feature constraints attached to a route-like object.
|
|
5
|
+
*/
|
|
6
|
+
export type RouteAccessRequirement = {
|
|
7
|
+
/** Feature codes required for this route. */
|
|
8
|
+
featureIds?: readonly string[];
|
|
9
|
+
/** Matching strategy for `featureIds`. Defaults to `any`. */
|
|
10
|
+
featureMode?: AccessRequirementMode;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Runtime values used when evaluating route access.
|
|
14
|
+
*/
|
|
15
|
+
export type RouteAccessContext = FeatureCheckOptions & {
|
|
16
|
+
/** Active feature list for the current license/session. */
|
|
17
|
+
features?: readonly string[];
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Result returned by route access evaluators.
|
|
21
|
+
*/
|
|
22
|
+
export type RouteAccessDecision = {
|
|
23
|
+
/** Whether access is granted. */
|
|
24
|
+
allowed: boolean;
|
|
25
|
+
/** Primary reason that explains the decision. */
|
|
26
|
+
reason: "allowed" | "missing_feature";
|
|
27
|
+
/** Missing required features when denied. */
|
|
28
|
+
missingFeatures: string[];
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Evaluates feature access for a single route requirement.
|
|
32
|
+
*/
|
|
33
|
+
export declare const evaluateRouteAccess: (requirement: RouteAccessRequirement, context?: RouteAccessContext) => {
|
|
34
|
+
allowed: false;
|
|
35
|
+
reason: "missing_feature";
|
|
36
|
+
missingFeatures: string[];
|
|
37
|
+
} | {
|
|
38
|
+
allowed: true;
|
|
39
|
+
reason: "allowed";
|
|
40
|
+
missingFeatures: never[];
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Convenience wrapper that only returns a boolean decision.
|
|
44
|
+
*/
|
|
45
|
+
export declare const canAccessRoute: (requirement: RouteAccessRequirement, context?: RouteAccessContext) => boolean;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { filterAccessibleItems, type AccessControlledItem, type FilterAccessibleItemsOptions, } from "./access/navigation";
|
|
2
|
+
export { canAccessRoute, evaluateRouteAccess, type RouteAccessContext, type RouteAccessDecision, type RouteAccessRequirement, } from "./access/route-access";
|
|
3
|
+
export { createLicenseFeatureLoader, defaultLicenseFeatureResponseParser, type CreateLicenseFeatureLoaderOptions, type LicenseFeatureResponseParser, } from "./licensing/feature-http-loader";
|
|
4
|
+
export { createFeatureStore, type CreateFeatureStoreOptions, type FeatureStoreState, } from "./licensing/feature-store";
|
|
5
|
+
export { getMissingFeatures, hasAllFeatures, hasAnyFeature, hasFeature, hasFeatures, normalizeFeatureList, type FeatureCheckOptions, type FeatureRequirementOptions, } from "./licensing/feature-utils";
|
|
6
|
+
export { type AccessRequirementMode } from "./internal/access-keys";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
// src/internal/access-keys.ts
|
|
2
|
+
var normalizeAccessKey = (rawValue, options = {}) => {
|
|
3
|
+
const trimmedValue = rawValue.trim();
|
|
4
|
+
if (trimmedValue.length === 0) {
|
|
5
|
+
return "";
|
|
6
|
+
}
|
|
7
|
+
if (options.caseSensitive) {
|
|
8
|
+
return trimmedValue;
|
|
9
|
+
}
|
|
10
|
+
return trimmedValue.toLowerCase();
|
|
11
|
+
};
|
|
12
|
+
var normalizeAccessKeys = (rawValues, options = {}) => {
|
|
13
|
+
const dedupedValues = new Set;
|
|
14
|
+
for (const rawValue of rawValues) {
|
|
15
|
+
const normalizedValue = normalizeAccessKey(rawValue, options);
|
|
16
|
+
if (normalizedValue.length === 0) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
dedupedValues.add(normalizedValue);
|
|
20
|
+
}
|
|
21
|
+
return Array.from(dedupedValues);
|
|
22
|
+
};
|
|
23
|
+
var createAccessLookup = (rawValues, options = {}) => {
|
|
24
|
+
return new Set(normalizeAccessKeys(rawValues, options));
|
|
25
|
+
};
|
|
26
|
+
var hasAccessKey = (rawValues, targetValue, options = {}) => {
|
|
27
|
+
const normalizedTargetValue = normalizeAccessKey(targetValue, options);
|
|
28
|
+
if (normalizedTargetValue.length === 0) {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
const lookup = createAccessLookup(rawValues, options);
|
|
32
|
+
return lookup.has(normalizedTargetValue);
|
|
33
|
+
};
|
|
34
|
+
var getMissingAccessKeys = (rawValues, requiredValues, options = {}) => {
|
|
35
|
+
const lookup = createAccessLookup(rawValues, options);
|
|
36
|
+
const normalizedRequiredValues = normalizeAccessKeys(requiredValues, options);
|
|
37
|
+
return normalizedRequiredValues.filter((requiredValue) => {
|
|
38
|
+
return !lookup.has(requiredValue);
|
|
39
|
+
});
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// src/licensing/feature-utils.ts
|
|
43
|
+
var normalizeFeatureList = (features, options = {}) => {
|
|
44
|
+
return normalizeAccessKeys(features, options);
|
|
45
|
+
};
|
|
46
|
+
var hasFeature = (features, requiredFeature, options = {}) => {
|
|
47
|
+
return hasAccessKey(features, requiredFeature, options);
|
|
48
|
+
};
|
|
49
|
+
var hasAnyFeature = (features, requiredFeatures, options = {}) => {
|
|
50
|
+
const normalizedRequiredFeatures = normalizeAccessKeys(requiredFeatures, options);
|
|
51
|
+
if (normalizedRequiredFeatures.length === 0) {
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
const lookup = createAccessLookup(features, options);
|
|
55
|
+
return normalizedRequiredFeatures.some((requiredFeature) => {
|
|
56
|
+
return lookup.has(requiredFeature);
|
|
57
|
+
});
|
|
58
|
+
};
|
|
59
|
+
var hasAllFeatures = (features, requiredFeatures, options = {}) => {
|
|
60
|
+
const normalizedRequiredFeatures = normalizeAccessKeys(requiredFeatures, options);
|
|
61
|
+
if (normalizedRequiredFeatures.length === 0) {
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
const lookup = createAccessLookup(features, options);
|
|
65
|
+
return normalizedRequiredFeatures.every((requiredFeature) => {
|
|
66
|
+
return lookup.has(requiredFeature);
|
|
67
|
+
});
|
|
68
|
+
};
|
|
69
|
+
var hasFeatures = (features, requiredFeatures, options = {}) => {
|
|
70
|
+
if ((options.mode ?? "all") === "any") {
|
|
71
|
+
return hasAnyFeature(features, requiredFeatures, options);
|
|
72
|
+
}
|
|
73
|
+
return hasAllFeatures(features, requiredFeatures, options);
|
|
74
|
+
};
|
|
75
|
+
var getMissingFeatures = (features, requiredFeatures, options = {}) => {
|
|
76
|
+
return getMissingAccessKeys(features, requiredFeatures, options);
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
// src/access/route-access.ts
|
|
80
|
+
var evaluateRouteAccess = (requirement, context = {}) => {
|
|
81
|
+
const features = context.features ?? [];
|
|
82
|
+
const requiredFeatureIds = requirement.featureIds ?? [];
|
|
83
|
+
if (requiredFeatureIds.length > 0) {
|
|
84
|
+
const isFeatureAccessAllowed = hasFeatures(features, requiredFeatureIds, {
|
|
85
|
+
caseSensitive: context.caseSensitive,
|
|
86
|
+
mode: requirement.featureMode ?? "any"
|
|
87
|
+
});
|
|
88
|
+
if (!isFeatureAccessAllowed) {
|
|
89
|
+
return {
|
|
90
|
+
allowed: false,
|
|
91
|
+
reason: "missing_feature",
|
|
92
|
+
missingFeatures: getMissingFeatures(features, requiredFeatureIds, {
|
|
93
|
+
caseSensitive: context.caseSensitive
|
|
94
|
+
})
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
allowed: true,
|
|
100
|
+
reason: "allowed",
|
|
101
|
+
missingFeatures: []
|
|
102
|
+
};
|
|
103
|
+
};
|
|
104
|
+
var canAccessRoute = (requirement, context = {}) => {
|
|
105
|
+
return evaluateRouteAccess(requirement, context).allowed;
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
// src/access/navigation.ts
|
|
109
|
+
var filterAccessibleItems = (items, options = {}) => {
|
|
110
|
+
const keepParentWhenAnyChildAccessible = options.keepParentWhenAnyChildAccessible ?? true;
|
|
111
|
+
const context = {
|
|
112
|
+
features: options.features,
|
|
113
|
+
caseSensitive: options.caseSensitive
|
|
114
|
+
};
|
|
115
|
+
const filteredItems = [];
|
|
116
|
+
for (const item of items) {
|
|
117
|
+
const decision = evaluateRouteAccess(item, context);
|
|
118
|
+
const filteredChildren = item.children ? filterAccessibleItems(item.children, options) : undefined;
|
|
119
|
+
const hasVisibleChildren = (filteredChildren?.length ?? 0) > 0;
|
|
120
|
+
const shouldIncludeItem = decision.allowed || keepParentWhenAnyChildAccessible && hasVisibleChildren;
|
|
121
|
+
if (!shouldIncludeItem) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
const nextItem = {
|
|
125
|
+
...item,
|
|
126
|
+
...filteredChildren ? { children: filteredChildren } : {}
|
|
127
|
+
};
|
|
128
|
+
filteredItems.push(nextItem);
|
|
129
|
+
}
|
|
130
|
+
return filteredItems;
|
|
131
|
+
};
|
|
132
|
+
// src/internal/records.ts
|
|
133
|
+
var isRecord = (value) => {
|
|
134
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
135
|
+
};
|
|
136
|
+
var toStringArray = (value) => {
|
|
137
|
+
if (!Array.isArray(value)) {
|
|
138
|
+
return [];
|
|
139
|
+
}
|
|
140
|
+
const values = [];
|
|
141
|
+
for (const entry of value) {
|
|
142
|
+
if (typeof entry === "string") {
|
|
143
|
+
values.push(entry);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return values;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// src/licensing/feature-http-loader.ts
|
|
150
|
+
var resolveHeaders = async (headerProvider) => {
|
|
151
|
+
if (!headerProvider) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (typeof headerProvider === "function") {
|
|
155
|
+
return await headerProvider();
|
|
156
|
+
}
|
|
157
|
+
return headerProvider;
|
|
158
|
+
};
|
|
159
|
+
var tryReadStringArray = (payload) => {
|
|
160
|
+
const values = toStringArray(payload);
|
|
161
|
+
if (values.length > 0) {
|
|
162
|
+
return values;
|
|
163
|
+
}
|
|
164
|
+
if (Array.isArray(payload)) {
|
|
165
|
+
return [];
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
};
|
|
169
|
+
var defaultLicenseFeatureResponseParser = (payload) => {
|
|
170
|
+
const directArray = tryReadStringArray(payload);
|
|
171
|
+
if (directArray) {
|
|
172
|
+
return directArray;
|
|
173
|
+
}
|
|
174
|
+
if (!isRecord(payload)) {
|
|
175
|
+
throw new Error("Unable to parse feature response payload");
|
|
176
|
+
}
|
|
177
|
+
const payloadRecord = payload;
|
|
178
|
+
const directFeatures = tryReadStringArray(payloadRecord.features);
|
|
179
|
+
if (directFeatures) {
|
|
180
|
+
return directFeatures;
|
|
181
|
+
}
|
|
182
|
+
const directExtra = tryReadStringArray(payloadRecord.extra);
|
|
183
|
+
if (directExtra) {
|
|
184
|
+
return directExtra;
|
|
185
|
+
}
|
|
186
|
+
if (isRecord(payloadRecord.data)) {
|
|
187
|
+
const dataRecord = payloadRecord.data;
|
|
188
|
+
const nestedFeatures = tryReadStringArray(dataRecord.features);
|
|
189
|
+
if (nestedFeatures) {
|
|
190
|
+
return nestedFeatures;
|
|
191
|
+
}
|
|
192
|
+
const nestedExtra = tryReadStringArray(dataRecord.extra);
|
|
193
|
+
if (nestedExtra) {
|
|
194
|
+
return nestedExtra;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
throw new Error("Unable to parse feature response payload");
|
|
198
|
+
};
|
|
199
|
+
var createLicenseFeatureLoader = (options) => {
|
|
200
|
+
const parser = options.parser ?? defaultLicenseFeatureResponseParser;
|
|
201
|
+
const fetcher = options.fetch ?? fetch;
|
|
202
|
+
const loadFeatures = async () => {
|
|
203
|
+
const response = await fetcher(options.url, {
|
|
204
|
+
method: "GET",
|
|
205
|
+
credentials: options.credentials,
|
|
206
|
+
headers: await resolveHeaders(options.headers)
|
|
207
|
+
});
|
|
208
|
+
if (!response.ok) {
|
|
209
|
+
throw new Error(`Failed to load license features (${response.status} ${response.statusText})`);
|
|
210
|
+
}
|
|
211
|
+
const payload = await response.json();
|
|
212
|
+
return parser(payload);
|
|
213
|
+
};
|
|
214
|
+
return loadFeatures;
|
|
215
|
+
};
|
|
216
|
+
// src/internal/store.ts
|
|
217
|
+
var createStore = (initialState) => {
|
|
218
|
+
let currentState = initialState;
|
|
219
|
+
const listeners = new Set;
|
|
220
|
+
const getState = () => {
|
|
221
|
+
return currentState;
|
|
222
|
+
};
|
|
223
|
+
const setState = (updater) => {
|
|
224
|
+
currentState = typeof updater === "function" ? updater(currentState) : updater;
|
|
225
|
+
for (const listener of listeners) {
|
|
226
|
+
listener(currentState);
|
|
227
|
+
}
|
|
228
|
+
return currentState;
|
|
229
|
+
};
|
|
230
|
+
const subscribe = (listener) => {
|
|
231
|
+
listeners.add(listener);
|
|
232
|
+
return () => {
|
|
233
|
+
listeners.delete(listener);
|
|
234
|
+
};
|
|
235
|
+
};
|
|
236
|
+
return {
|
|
237
|
+
getState,
|
|
238
|
+
setState,
|
|
239
|
+
subscribe
|
|
240
|
+
};
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
// src/licensing/feature-store.ts
|
|
244
|
+
var createFeatureStore = (options = {}) => {
|
|
245
|
+
const initialFeatures = normalizeFeatureList(options.initialFeatures ?? [], options);
|
|
246
|
+
const store = createStore({
|
|
247
|
+
features: initialFeatures,
|
|
248
|
+
loaded: initialFeatures.length > 0,
|
|
249
|
+
isLoading: false,
|
|
250
|
+
lastLoadedAt: initialFeatures.length > 0 ? Date.now() : null,
|
|
251
|
+
error: null
|
|
252
|
+
});
|
|
253
|
+
let inFlightLoad = null;
|
|
254
|
+
const getState = () => {
|
|
255
|
+
return store.getState();
|
|
256
|
+
};
|
|
257
|
+
const subscribe = store.subscribe;
|
|
258
|
+
const setFeatures = (features) => {
|
|
259
|
+
const normalizedFeatures = normalizeFeatureList(features, options);
|
|
260
|
+
store.setState((state) => {
|
|
261
|
+
return {
|
|
262
|
+
...state,
|
|
263
|
+
features: normalizedFeatures,
|
|
264
|
+
loaded: true,
|
|
265
|
+
isLoading: false,
|
|
266
|
+
lastLoadedAt: Date.now(),
|
|
267
|
+
error: null
|
|
268
|
+
};
|
|
269
|
+
});
|
|
270
|
+
return normalizedFeatures;
|
|
271
|
+
};
|
|
272
|
+
const clear = () => {
|
|
273
|
+
store.setState((state) => {
|
|
274
|
+
return {
|
|
275
|
+
...state,
|
|
276
|
+
features: [],
|
|
277
|
+
loaded: false,
|
|
278
|
+
isLoading: false,
|
|
279
|
+
lastLoadedAt: null,
|
|
280
|
+
error: null
|
|
281
|
+
};
|
|
282
|
+
});
|
|
283
|
+
};
|
|
284
|
+
const load = async () => {
|
|
285
|
+
if (!options.loadFeatures) {
|
|
286
|
+
const state = getState();
|
|
287
|
+
if (!state.loaded) {
|
|
288
|
+
store.setState((currentState) => {
|
|
289
|
+
return {
|
|
290
|
+
...currentState,
|
|
291
|
+
loaded: true,
|
|
292
|
+
lastLoadedAt: currentState.lastLoadedAt ?? Date.now()
|
|
293
|
+
};
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
return getState().features;
|
|
297
|
+
}
|
|
298
|
+
if (inFlightLoad) {
|
|
299
|
+
return inFlightLoad;
|
|
300
|
+
}
|
|
301
|
+
store.setState((state) => {
|
|
302
|
+
return {
|
|
303
|
+
...state,
|
|
304
|
+
isLoading: true,
|
|
305
|
+
error: null
|
|
306
|
+
};
|
|
307
|
+
});
|
|
308
|
+
inFlightLoad = options.loadFeatures().then((features) => {
|
|
309
|
+
return setFeatures(features);
|
|
310
|
+
}).catch((error) => {
|
|
311
|
+
const shouldResetToEmpty = options.resetToEmptyOnError ?? true;
|
|
312
|
+
store.setState((state) => {
|
|
313
|
+
return {
|
|
314
|
+
...state,
|
|
315
|
+
features: shouldResetToEmpty ? [] : state.features,
|
|
316
|
+
loaded: true,
|
|
317
|
+
isLoading: false,
|
|
318
|
+
lastLoadedAt: Date.now(),
|
|
319
|
+
error
|
|
320
|
+
};
|
|
321
|
+
});
|
|
322
|
+
throw error;
|
|
323
|
+
}).finally(() => {
|
|
324
|
+
inFlightLoad = null;
|
|
325
|
+
});
|
|
326
|
+
return inFlightLoad;
|
|
327
|
+
};
|
|
328
|
+
const hasOneFeature = (requiredFeature) => {
|
|
329
|
+
return hasFeature(getState().features, requiredFeature, options);
|
|
330
|
+
};
|
|
331
|
+
const hasAnyRequiredFeature = (requiredFeatures) => {
|
|
332
|
+
return hasAnyFeature(getState().features, requiredFeatures, options);
|
|
333
|
+
};
|
|
334
|
+
const hasAllRequiredFeatures = (requiredFeatures) => {
|
|
335
|
+
return hasAllFeatures(getState().features, requiredFeatures, options);
|
|
336
|
+
};
|
|
337
|
+
const hasRequiredFeatures = (requiredFeatures, requirementOptions = {}) => {
|
|
338
|
+
return hasFeatures(getState().features, requiredFeatures, {
|
|
339
|
+
...options,
|
|
340
|
+
...requirementOptions
|
|
341
|
+
});
|
|
342
|
+
};
|
|
343
|
+
const getMissingRequiredFeatures = (requiredFeatures, requirementOptions = {}) => {
|
|
344
|
+
return getMissingFeatures(getState().features, requiredFeatures, {
|
|
345
|
+
...options,
|
|
346
|
+
...requirementOptions
|
|
347
|
+
});
|
|
348
|
+
};
|
|
349
|
+
if (options.autoLoad) {
|
|
350
|
+
load().catch(() => {
|
|
351
|
+
return;
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
const featureStore = {
|
|
355
|
+
getState,
|
|
356
|
+
subscribe,
|
|
357
|
+
setFeatures,
|
|
358
|
+
clear,
|
|
359
|
+
load,
|
|
360
|
+
hasFeature: hasOneFeature,
|
|
361
|
+
hasAnyFeature: hasAnyRequiredFeature,
|
|
362
|
+
hasAllFeatures: hasAllRequiredFeatures,
|
|
363
|
+
hasFeatures: hasRequiredFeatures,
|
|
364
|
+
getMissingFeatures: getMissingRequiredFeatures
|
|
365
|
+
};
|
|
366
|
+
return featureStore;
|
|
367
|
+
};
|
|
368
|
+
export {
|
|
369
|
+
normalizeFeatureList,
|
|
370
|
+
hasFeatures,
|
|
371
|
+
hasFeature,
|
|
372
|
+
hasAnyFeature,
|
|
373
|
+
hasAllFeatures,
|
|
374
|
+
getMissingFeatures,
|
|
375
|
+
filterAccessibleItems,
|
|
376
|
+
evaluateRouteAccess,
|
|
377
|
+
defaultLicenseFeatureResponseParser,
|
|
378
|
+
createLicenseFeatureLoader,
|
|
379
|
+
createFeatureStore,
|
|
380
|
+
canAccessRoute
|
|
381
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Determines whether a check requires any or all values to match.
|
|
3
|
+
*/
|
|
4
|
+
export type AccessRequirementMode = "any" | "all";
|
|
5
|
+
export type AccessKeyOptions = {
|
|
6
|
+
caseSensitive?: boolean;
|
|
7
|
+
};
|
|
8
|
+
export declare const normalizeAccessKey: (rawValue: string, options?: AccessKeyOptions) => string;
|
|
9
|
+
export declare const normalizeAccessKeys: (rawValues: readonly string[], options?: AccessKeyOptions) => string[];
|
|
10
|
+
export declare const createAccessLookup: (rawValues: readonly string[], options?: AccessKeyOptions) => Set<string>;
|
|
11
|
+
export declare const hasAccessKey: (rawValues: readonly string[], targetValue: string, options?: AccessKeyOptions) => boolean;
|
|
12
|
+
export declare const getMissingAccessKeys: (rawValues: readonly string[], requiredValues: readonly string[], options?: AccessKeyOptions) => string[];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type StoreListener<TState> = (state: TState) => void;
|
|
2
|
+
type StateUpdater<TState> = TState | ((state: TState) => TState);
|
|
3
|
+
export declare const createStore: <TState>(initialState: TState) => {
|
|
4
|
+
getState: () => TState;
|
|
5
|
+
setState: (updater: StateUpdater<TState>) => TState;
|
|
6
|
+
subscribe: (listener: StoreListener<TState>) => () => void;
|
|
7
|
+
};
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
type HeaderProvider = HeadersInit | (() => HeadersInit | Promise<HeadersInit>);
|
|
2
|
+
/**
|
|
3
|
+
* Parses an unknown HTTP payload into a list of feature codes.
|
|
4
|
+
*/
|
|
5
|
+
export type LicenseFeatureResponseParser = (payload: unknown) => readonly string[];
|
|
6
|
+
/**
|
|
7
|
+
* A function that loads the active feature list.
|
|
8
|
+
*/
|
|
9
|
+
export type LicenseFeatureLoader = () => Promise<readonly string[]>;
|
|
10
|
+
/**
|
|
11
|
+
* Configuration used by `createLicenseFeatureLoader`.
|
|
12
|
+
*/
|
|
13
|
+
export type CreateLicenseFeatureLoaderOptions = {
|
|
14
|
+
/** Full endpoint URL that returns the active feature list. */
|
|
15
|
+
url: string;
|
|
16
|
+
/** Optional custom `fetch` implementation. */
|
|
17
|
+
fetch?: typeof fetch;
|
|
18
|
+
/** Optional static/dynamic request headers. */
|
|
19
|
+
headers?: HeaderProvider;
|
|
20
|
+
/** Optional `fetch` credentials mode. */
|
|
21
|
+
credentials?: RequestCredentials;
|
|
22
|
+
/** Optional custom payload parser. */
|
|
23
|
+
parser?: LicenseFeatureResponseParser;
|
|
24
|
+
};
|
|
25
|
+
export declare const defaultLicenseFeatureResponseParser: LicenseFeatureResponseParser;
|
|
26
|
+
/**
|
|
27
|
+
* Creates an HTTP-based feature loader.
|
|
28
|
+
*
|
|
29
|
+
* The default parser supports these payload shapes:
|
|
30
|
+
* - `string[]`
|
|
31
|
+
* - `{ features: string[] }`
|
|
32
|
+
* - `{ extra: string[] }`
|
|
33
|
+
* - `{ data: { features: string[] } }`
|
|
34
|
+
* - `{ data: { extra: string[] } }`
|
|
35
|
+
*/
|
|
36
|
+
export declare const createLicenseFeatureLoader: (options: CreateLicenseFeatureLoaderOptions) => LicenseFeatureLoader;
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { type FeatureCheckOptions, type FeatureRequirementOptions } from "./feature-utils";
|
|
2
|
+
/**
|
|
3
|
+
* Async feature loader used by the store.
|
|
4
|
+
*/
|
|
5
|
+
export type FeatureLoader = () => Promise<readonly string[]>;
|
|
6
|
+
/**
|
|
7
|
+
* Runtime state exposed by the feature store.
|
|
8
|
+
*/
|
|
9
|
+
export type FeatureStoreState = {
|
|
10
|
+
/** Current normalized feature list. */
|
|
11
|
+
features: string[];
|
|
12
|
+
/** Indicates if the store has completed at least one initialization/load step. */
|
|
13
|
+
loaded: boolean;
|
|
14
|
+
/** Indicates if a load operation is currently in progress. */
|
|
15
|
+
isLoading: boolean;
|
|
16
|
+
/** Unix timestamp (ms) of the last successful/failed load attempt. */
|
|
17
|
+
lastLoadedAt: number | null;
|
|
18
|
+
/** Last load error value, if any. */
|
|
19
|
+
error: unknown | null;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Options used to create a feature store.
|
|
23
|
+
*/
|
|
24
|
+
export type CreateFeatureStoreOptions = FeatureCheckOptions & {
|
|
25
|
+
/** Optional async loader used by `load()`. */
|
|
26
|
+
loadFeatures?: FeatureLoader;
|
|
27
|
+
/** Optional initial feature list. */
|
|
28
|
+
initialFeatures?: readonly string[];
|
|
29
|
+
/** Automatically call `load()` on store creation. */
|
|
30
|
+
autoLoad?: boolean;
|
|
31
|
+
/** Reset `features` to `[]` when loading fails. Defaults to `true`. */
|
|
32
|
+
resetToEmptyOnError?: boolean;
|
|
33
|
+
};
|
|
34
|
+
/**
|
|
35
|
+
* Public API returned by `createFeatureStore`.
|
|
36
|
+
*/
|
|
37
|
+
export type FeatureStore = {
|
|
38
|
+
/** Returns current store state. */
|
|
39
|
+
getState: () => FeatureStoreState;
|
|
40
|
+
/** Subscribes to state changes. Returns an unsubscribe function. */
|
|
41
|
+
subscribe: (listener: (state: FeatureStoreState) => void) => () => void;
|
|
42
|
+
/** Replaces current features after normalization. */
|
|
43
|
+
setFeatures: (features: readonly string[]) => string[];
|
|
44
|
+
/** Clears all features and resets load markers. */
|
|
45
|
+
clear: () => void;
|
|
46
|
+
/** Loads features from `loadFeatures` when configured. */
|
|
47
|
+
load: () => Promise<string[]>;
|
|
48
|
+
/** Checks a single required feature. */
|
|
49
|
+
hasFeature: (requiredFeature: string) => boolean;
|
|
50
|
+
/** Checks that at least one required feature exists. */
|
|
51
|
+
hasAnyFeature: (requiredFeatures: readonly string[]) => boolean;
|
|
52
|
+
/** Checks that all required features exist. */
|
|
53
|
+
hasAllFeatures: (requiredFeatures: readonly string[]) => boolean;
|
|
54
|
+
/** Checks required features with configurable mode. */
|
|
55
|
+
hasFeatures: (requiredFeatures: readonly string[], requirementOptions?: FeatureRequirementOptions) => boolean;
|
|
56
|
+
/** Returns required feature codes that are currently missing. */
|
|
57
|
+
getMissingFeatures: (requiredFeatures: readonly string[], requirementOptions?: FeatureCheckOptions) => string[];
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Creates a lightweight in-memory feature store.
|
|
61
|
+
*/
|
|
62
|
+
export declare const createFeatureStore: (options?: CreateFeatureStoreOptions) => {
|
|
63
|
+
getState: () => FeatureStoreState;
|
|
64
|
+
subscribe: (listener: import("../internal/store").StoreListener<FeatureStoreState>) => () => void;
|
|
65
|
+
setFeatures: (features: readonly string[]) => string[];
|
|
66
|
+
clear: () => void;
|
|
67
|
+
load: () => Promise<string[]>;
|
|
68
|
+
hasFeature: (requiredFeature: string) => boolean;
|
|
69
|
+
hasAnyFeature: (requiredFeatures: readonly string[]) => boolean;
|
|
70
|
+
hasAllFeatures: (requiredFeatures: readonly string[]) => boolean;
|
|
71
|
+
hasFeatures: (requiredFeatures: readonly string[], requirementOptions?: FeatureRequirementOptions) => boolean;
|
|
72
|
+
getMissingFeatures: (requiredFeatures: readonly string[], requirementOptions?: FeatureCheckOptions) => string[];
|
|
73
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { type AccessKeyOptions, type AccessRequirementMode } from "../internal/access-keys";
|
|
2
|
+
/**
|
|
3
|
+
* Options shared by all feature-check helpers.
|
|
4
|
+
*
|
|
5
|
+
* @property caseSensitive Set to `true` to keep original casing during checks.
|
|
6
|
+
*/
|
|
7
|
+
export type FeatureCheckOptions = AccessKeyOptions;
|
|
8
|
+
/**
|
|
9
|
+
* Options for checking multiple required features.
|
|
10
|
+
*
|
|
11
|
+
* @property mode `all` requires every feature, `any` requires at least one.
|
|
12
|
+
*/
|
|
13
|
+
export type FeatureRequirementOptions = FeatureCheckOptions & {
|
|
14
|
+
mode?: AccessRequirementMode;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Normalizes and deduplicates feature codes.
|
|
18
|
+
*/
|
|
19
|
+
export declare const normalizeFeatureList: (features: readonly string[], options?: FeatureCheckOptions) => string[];
|
|
20
|
+
/**
|
|
21
|
+
* Returns `true` when a single feature is available.
|
|
22
|
+
*/
|
|
23
|
+
export declare const hasFeature: (features: readonly string[], requiredFeature: string, options?: FeatureCheckOptions) => boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Returns `true` when at least one required feature is available.
|
|
26
|
+
*/
|
|
27
|
+
export declare const hasAnyFeature: (features: readonly string[], requiredFeatures: readonly string[], options?: FeatureCheckOptions) => boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Returns `true` when all required features are available.
|
|
30
|
+
*/
|
|
31
|
+
export declare const hasAllFeatures: (features: readonly string[], requiredFeatures: readonly string[], options?: FeatureCheckOptions) => boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Checks required features using the provided `mode`.
|
|
34
|
+
*/
|
|
35
|
+
export declare const hasFeatures: (features: readonly string[], requiredFeatures: readonly string[], options?: FeatureRequirementOptions) => boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Returns normalized required feature codes that are currently missing.
|
|
38
|
+
*/
|
|
39
|
+
export declare const getMissingFeatures: (features: readonly string[], requiredFeatures: readonly string[], options?: FeatureCheckOptions) => string[];
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@m6d/cerebro",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": {
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"import": "./dist/index.js"
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"main": "./dist/index.js",
|
|
12
|
+
"module": "./dist/index.js",
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "bun run clean && bun build ./src/index.ts --outdir ./dist --format esm --target browser && bunx --bun tsc -p tsconfig.build.json",
|
|
19
|
+
"check": "bunx --bun tsc --noEmit",
|
|
20
|
+
"clean": "rm -rf dist",
|
|
21
|
+
"format": "prettier --write .",
|
|
22
|
+
"format:check": "prettier --check .",
|
|
23
|
+
"lint": "eslint",
|
|
24
|
+
"prepare": "husky",
|
|
25
|
+
"release": "semantic-release",
|
|
26
|
+
"sample:feature-store": "bun run ./samples/vanilla-feature-store-sample/index.ts",
|
|
27
|
+
"spellcheck": "cspell \"**/*.{ts,tsx,js,jsx,json,md}\"",
|
|
28
|
+
"test": "bun test"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@commitlint/cli": "^20.4.1",
|
|
32
|
+
"@commitlint/config-conventional": "^20.4.1",
|
|
33
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
34
|
+
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
35
|
+
"@semantic-release/exec": "^7.1.0",
|
|
36
|
+
"@semantic-release/git": "^10.0.1",
|
|
37
|
+
"@semantic-release/github": "^11.0.6",
|
|
38
|
+
"@semantic-release/npm": "^12.0.2",
|
|
39
|
+
"@semantic-release/release-notes-generator": "^14.1.0",
|
|
40
|
+
"@types/bun": "latest",
|
|
41
|
+
"cspell": "^9.6.4",
|
|
42
|
+
"eslint": "^9",
|
|
43
|
+
"eslint-config-prettier": "^10.1.8",
|
|
44
|
+
"eslint-plugin-prettier": "^5.5.5",
|
|
45
|
+
"husky": "^9.1.7",
|
|
46
|
+
"lint-staged": "^16.2.7",
|
|
47
|
+
"prettier": "^3.6.2",
|
|
48
|
+
"semantic-release": "^24.2.3",
|
|
49
|
+
"sort-package-json": "^3.6.1"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"typescript": "^5"
|
|
53
|
+
},
|
|
54
|
+
"publishConfig": {
|
|
55
|
+
"access": "public"
|
|
56
|
+
}
|
|
57
|
+
}
|