feature-core 0.0.1
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/LICENSE +21 -0
- package/README.md +222 -0
- package/dist/cjs/index.js +1 -0
- package/dist/esm/index.js +1 -0
- package/dist/types/index.d.ts +121 -0
- package/package.json +47 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 @bennobuilder
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
<h1 align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/builder-group/community/develop/packages/feature-core/.github/banner.svg" alt="feature-core banner">
|
|
3
|
+
</h1>
|
|
4
|
+
|
|
5
|
+
<p align="left">
|
|
6
|
+
<a href="https://github.com/builder-group/community/blob/develop/LICENSE">
|
|
7
|
+
<img src="https://img.shields.io/github/license/builder-group/community.svg?label=license&style=flat&colorA=293140&colorB=FDE200" alt="GitHub License"/>
|
|
8
|
+
</a>
|
|
9
|
+
<a href="https://www.npmjs.com/package/feature-core">
|
|
10
|
+
<img src="https://img.shields.io/bundlephobia/minzip/feature-core.svg?label=minzipped%20size&style=flat&colorA=293140&colorB=FDE200" alt="NPM bundle minzipped size"/>
|
|
11
|
+
</a>
|
|
12
|
+
<a href="https://www.npmjs.com/package/feature-core">
|
|
13
|
+
<img src="https://img.shields.io/npm/dt/feature-core.svg?label=downloads&style=flat&colorA=293140&colorB=FDE200" alt="NPM total downloads"/>
|
|
14
|
+
</a>
|
|
15
|
+
<a href="https://discord.gg/w4xE3bSjhQ">
|
|
16
|
+
<img src="https://img.shields.io/discord/795291052897992724.svg?label=&logo=discord&logoColor=000000&color=293140&labelColor=FDE200" alt="Join Discord"/>
|
|
17
|
+
</a>
|
|
18
|
+
</p>
|
|
19
|
+
|
|
20
|
+
> Status: Experimental
|
|
21
|
+
|
|
22
|
+
`feature-core` is a small, typesafe foundation for building feature-based JavaScript and TypeScript libraries.
|
|
23
|
+
|
|
24
|
+
- **Lightweight & Tree Shakable**: Object-based composition with no class hierarchy
|
|
25
|
+
- **Modular & Extendable**: Add capabilities with `.with(feature())` instead of nested wrappers
|
|
26
|
+
- **Typesafe**: Feature APIs and dependencies are tracked in TypeScript
|
|
27
|
+
- **Author Friendly**: Custom features are plain objects created with `defineFeature()`
|
|
28
|
+
- **Framework Agnostic**: Works for state containers, fetch clients, loggers, and other object-based libraries
|
|
29
|
+
|
|
30
|
+
### 🌟 Motivation
|
|
31
|
+
|
|
32
|
+
Feature-based libraries often repeat the same difficult parts: applying feature APIs, tracking installed features, validating dependencies, and preserving strong TypeScript inference. `feature-core` centralizes those mechanics so libraries can expose a consistent `.with(...)` extension model while feature authors only define a key, optional requirements, and an install function.
|
|
33
|
+
|
|
34
|
+
## 📖 Usage
|
|
35
|
+
|
|
36
|
+
Consumers compose features on a host object:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
const state = createState(0)
|
|
40
|
+
.with(undoFeature(4))
|
|
41
|
+
.with(storageFeature(storage, 'count'))
|
|
42
|
+
.with(loggerFeature());
|
|
43
|
+
|
|
44
|
+
state.undo();
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Features can also be applied in order through one call:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
const state = createState(0).with(
|
|
51
|
+
undoFeature(4),
|
|
52
|
+
storageFeature(storage, 'count'),
|
|
53
|
+
loggerFeature()
|
|
54
|
+
);
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The variadic form is typed in install order, so each feature is checked against the host produced by the features before it. Prefer chained `.with(...)` calls when a long feature list becomes harder to scan.
|
|
58
|
+
|
|
59
|
+
## 📙 Building Libraries
|
|
60
|
+
|
|
61
|
+
Libraries wrap their base object with `createFeatureHost()`.
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
import { createFeatureHost, type TFeatureHost } from 'feature-core';
|
|
65
|
+
|
|
66
|
+
interface TCounterBase {
|
|
67
|
+
get: () => number;
|
|
68
|
+
set: (nextValue: number) => void;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
type TCounterFeature = TResetFeature | TResetTwiceFeature;
|
|
72
|
+
|
|
73
|
+
export type TCounter<GFeatures extends TCounterFeature[]> = TFeatureHost<
|
|
74
|
+
TCounterBase,
|
|
75
|
+
GFeatures
|
|
76
|
+
>;
|
|
77
|
+
|
|
78
|
+
export function createCounter(initialValue: number): TCounter<[]> {
|
|
79
|
+
let value = initialValue;
|
|
80
|
+
|
|
81
|
+
return createFeatureHost({
|
|
82
|
+
get() {
|
|
83
|
+
return value;
|
|
84
|
+
},
|
|
85
|
+
set(nextValue) {
|
|
86
|
+
value = nextValue;
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface TResetFeature {
|
|
92
|
+
key: 'reset';
|
|
93
|
+
api: {
|
|
94
|
+
reset: () => void;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface TResetTwiceFeature {
|
|
99
|
+
key: 'resetTwice';
|
|
100
|
+
api: {
|
|
101
|
+
resetTwice: () => void;
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Feature hosts include `_features` metadata for `feature-core` internals. It is visible for transparency, marked internal in the type docs, and readonly in the public type. Prefer `hasFeature(host, key)` for app-level feature checks.
|
|
107
|
+
|
|
108
|
+
### Type Names
|
|
109
|
+
|
|
110
|
+
- `TFeature` is the runtime object returned by `defineFeature()`
|
|
111
|
+
- `TInstalledFeature` is the `{ key, api }` contract once a feature is installed
|
|
112
|
+
- `TDeclaredFeature` is a runtime feature with an explicit installed feature contract
|
|
113
|
+
- `TFeatureHost` is the base object plus installed feature APIs and `.with()`
|
|
114
|
+
|
|
115
|
+
## 📙 Creating Features
|
|
116
|
+
|
|
117
|
+
Feature authors use `defineFeature()`.
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
import { defineFeature, type TDeclaredFeature } from 'feature-core';
|
|
121
|
+
|
|
122
|
+
export function resetFeature(): TDeclaredFeature<TResetFeature> {
|
|
123
|
+
return defineFeature({
|
|
124
|
+
key: 'reset',
|
|
125
|
+
install<GFeatures extends TCounterFeature[]>(counter: TCounter<GFeatures>) {
|
|
126
|
+
const initialValue = counter.get();
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
reset() {
|
|
130
|
+
counter.set(initialValue);
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The `TDeclaredFeature<TResetFeature>` return type is optional but recommended for reusable library features. It checks that the runtime key and returned API match the declared installed feature contract, and `.with()` carries that named feature into the host feature list.
|
|
139
|
+
|
|
140
|
+
Prefer closing over the host passed to `install()` instead of relying on `this`. A returned method can still declare a `this` type, but closure-based methods are easier to write, refactor, and infer.
|
|
141
|
+
|
|
142
|
+
### Dependent Features
|
|
143
|
+
|
|
144
|
+
If a feature depends on another feature, declare the required installed feature contracts, runtime `requires`, and a constrained install host:
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
export function resetTwiceFeature(): TDeclaredFeature<TResetTwiceFeature, [TResetFeature]> {
|
|
148
|
+
return defineFeature({
|
|
149
|
+
key: 'resetTwice',
|
|
150
|
+
requires: ['reset'] as const,
|
|
151
|
+
install(counter: TCounter<[TResetFeature]>) {
|
|
152
|
+
return {
|
|
153
|
+
resetTwice() {
|
|
154
|
+
counter.reset();
|
|
155
|
+
counter.reset();
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
`TDeclaredFeature<TFeature, [TDependency]>` and `requires` provide install-order validation. The constrained `install()` host lets the implementation use dependency APIs without casts. Using all three gives better errors for humans and agents.
|
|
164
|
+
|
|
165
|
+
The second `TDeclaredFeature` generic lists required installed feature contracts. `feature-core` derives the required key tuple from those contracts and checks it against the runtime `requires` value.
|
|
166
|
+
|
|
167
|
+
### API-less Features
|
|
168
|
+
|
|
169
|
+
Some features only mutate internal configuration and do not expose new methods. Return an empty object for those features:
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
export function cacheFeature() {
|
|
173
|
+
return defineFeature({
|
|
174
|
+
key: 'cache',
|
|
175
|
+
install<GFeatures extends TFetchFeature[]>(client: TFetchClient<GFeatures>) {
|
|
176
|
+
client._config.requestMiddlewares.push(cacheMiddleware());
|
|
177
|
+
return {};
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## 🔍 Feature Checks
|
|
184
|
+
|
|
185
|
+
Use `hasFeature()` for runtime checks:
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
if (hasFeature(counter, 'reset')) {
|
|
189
|
+
console.log('Reset is installed');
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Pass the feature type when you want TypeScript to narrow the API:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
if (hasFeature<TResetFeature>(value, 'reset')) {
|
|
197
|
+
value.reset();
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## ❓ FAQ
|
|
202
|
+
|
|
203
|
+
### Why features instead of plugins?
|
|
204
|
+
|
|
205
|
+
`feature-core` composes typed capabilities directly onto a host object. "Feature" describes that narrower contract better than "plugin", which often implies discovery, lifecycle hooks, registries, or installable packages.
|
|
206
|
+
|
|
207
|
+
### Can features overwrite existing properties?
|
|
208
|
+
|
|
209
|
+
No. Feature keys must be unique per host, and returned API keys must not overwrite existing host properties. `feature-core` throws when a feature is installed twice, when required features are missing, or when a feature tries to overwrite an existing property.
|
|
210
|
+
|
|
211
|
+
### Should features use `this`?
|
|
212
|
+
|
|
213
|
+
Prefer closing over the host passed to `install()`. `this` methods can work, but they are easier to call incorrectly and harder for agents to author consistently.
|
|
214
|
+
|
|
215
|
+
### What should feature authors remember?
|
|
216
|
+
|
|
217
|
+
- Use a unique feature key
|
|
218
|
+
- Declare `requires` for runtime dependencies
|
|
219
|
+
- Type the `install()` host when the feature depends on another feature
|
|
220
|
+
- Return only new API keys
|
|
221
|
+
- Return `{}` for features that only mutate internal configuration
|
|
222
|
+
- Prefer chained `.with(...)` calls for long feature lists
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"use strict";function s(e){return w(e),Object.assign(e,{_features:[],with:a})}function u(e){var r;return{key:e.key,requires:(r=e.requires)!=null?r:[],install:e.install}}function i(e,r){f(e,r),c(e,r);const t=r.install(e);return l(e,r,t),Object.assign(e,t),e._features.push(r.key),e}function o(e,r){return typeof e!="object"||e==null||!("_features"in e)||!Array.isArray(e._features)||!("with"in e)||typeof e.with!="function"?!1:e._features.includes(r)}function a(...e){for(const r of e)i(this,r);return this}function f(e,r){const t=r.requires.find(n=>!e._features.includes(n));if(t!=null)throw new Error(`Feature "${r.key}" requires missing feature "${t}"`)}function c(e,r){if(e._features.includes(r.key))throw new Error(`Feature "${r.key}" is already installed`)}function l(e,r,t){for(const n of Reflect.ownKeys(t))if(n in e)throw new Error(`Feature "${r.key}" cannot overwrite existing property "${String(n)}"`)}function w(e){for(const r of y)if(r in e)throw new Error(`Feature host cannot overwrite existing property "${r}"`)}const y=["_features","with"];exports.createFeatureHost=s,exports.defineFeature=u,exports.hasFeature=o,exports.installFeature=i;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
function s(r){return w(r),Object.assign(r,{_features:[],with:a})}function o(r){var e;return{key:r.key,requires:(e=r.requires)!=null?e:[],install:r.install}}function i(r,e){f(r,e),c(r,e);const t=e.install(r);return l(r,e,t),Object.assign(r,t),r._features.push(e.key),r}function u(r,e){return typeof r!="object"||r==null||!("_features"in r)||!Array.isArray(r._features)||!("with"in r)||typeof r.with!="function"?!1:r._features.includes(e)}function a(...r){for(const e of r)i(this,e);return this}function f(r,e){const t=e.requires.find(n=>!r._features.includes(n));if(t!=null)throw new Error(`Feature "${e.key}" requires missing feature "${t}"`)}function c(r,e){if(r._features.includes(e.key))throw new Error(`Feature "${e.key}" is already installed`)}function l(r,e,t){for(const n of Reflect.ownKeys(t))if(n in r)throw new Error(`Feature "${e.key}" cannot overwrite existing property "${String(n)}"`)}function w(r){for(const e of y)if(e in r)throw new Error(`Feature host cannot overwrite existing property "${e}"`)}const y=["_features","with"];export{s as createFeatureHost,o as defineFeature,u as hasFeature,i as installFeature};
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Adds feature metadata and `.with()` to a plain object.
|
|
3
|
+
*
|
|
4
|
+
* Mutates and returns the provided object.
|
|
5
|
+
*/
|
|
6
|
+
export declare function createFeatureHost<GBase extends object>(base: GBase): TFeatureHost<GBase, []>;
|
|
7
|
+
/**
|
|
8
|
+
* Defines a composable feature that can be installed on a feature host.
|
|
9
|
+
*/
|
|
10
|
+
export declare function defineFeature<const GKey extends string, const GRequiredFeatureKeys extends readonly string[] = [], GInstall extends TFeatureInstall = TFeatureInstall>(options: TDefineFeatureOptions<GKey, GRequiredFeatureKeys, GInstall>): TFeature<GKey, GRequiredFeatureKeys, GInstall>;
|
|
11
|
+
/**
|
|
12
|
+
* Installs one feature on a host.
|
|
13
|
+
*
|
|
14
|
+
* Mutates and returns the host. Throws when requirements are missing, the feature is already
|
|
15
|
+
* installed, or the feature API would overwrite an existing property.
|
|
16
|
+
*/
|
|
17
|
+
export declare function installFeature<GBase extends object, GFeatures extends TInstalledFeature[], GFeature extends TAnyFeature>(host: TFeatureHost<GBase, GFeatures>, feature: GFeature & TFeatureRequirementConstraint<GFeature, GFeatures> & TFeatureInstallConstraint<GFeature, TFeatureHost<GBase, GFeatures>>): TFeatureHost<GBase, TInstalledFeaturesAfter<GBase, GFeatures, GFeature>>;
|
|
18
|
+
/**
|
|
19
|
+
* Checks whether a value is a feature host with a specific installed feature.
|
|
20
|
+
*/
|
|
21
|
+
export declare function hasFeature<GFeature extends TInstalledFeature>(host: unknown, key: GFeature['key']): host is TFeatureHost<object, [GFeature]>;
|
|
22
|
+
export declare function hasFeature<GKey extends string>(host: unknown, key: GKey): host is TFeatureHost<object, [{
|
|
23
|
+
key: GKey;
|
|
24
|
+
api: object;
|
|
25
|
+
}]>;
|
|
26
|
+
export interface TDefineFeatureOptions<GKey extends string, GRequiredFeatureKeys extends readonly string[], GInstall extends TFeatureInstall> {
|
|
27
|
+
/**
|
|
28
|
+
* Unique feature key used for runtime dependency checks.
|
|
29
|
+
*/
|
|
30
|
+
key: GKey;
|
|
31
|
+
/**
|
|
32
|
+
* Feature keys that must already be installed before this feature can be installed.
|
|
33
|
+
*/
|
|
34
|
+
requires?: GRequiredFeatureKeys;
|
|
35
|
+
/**
|
|
36
|
+
* Adds behavior to the host and returns the public API exposed by this feature.
|
|
37
|
+
*/
|
|
38
|
+
install: GInstall;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Runtime feature object created by `defineFeature()`.
|
|
42
|
+
*/
|
|
43
|
+
export interface TFeature<GKey extends string = string, GRequiredFeatureKeys extends readonly string[] = readonly string[], GInstall extends TFeatureInstall = TFeatureInstall, GInstalledFeature extends TInstalledFeature<GKey, object> = never> {
|
|
44
|
+
key: GKey;
|
|
45
|
+
requires: GRequiredFeatureKeys;
|
|
46
|
+
install: GInstall;
|
|
47
|
+
/**
|
|
48
|
+
* @internal Type-only link to the installed feature contract.
|
|
49
|
+
*/
|
|
50
|
+
readonly __installedFeature?: GInstalledFeature;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Feature contract after a runtime feature has been installed on a host.
|
|
54
|
+
*/
|
|
55
|
+
export interface TInstalledFeature<GKey extends string = string, GApi extends object = object> {
|
|
56
|
+
key: GKey;
|
|
57
|
+
api: GApi;
|
|
58
|
+
}
|
|
59
|
+
export type TFeatureHost<GBase extends object, GFeatures extends TInstalledFeature[]> = Omit<GBase, keyof TFeatureHostApi<object, TInstalledFeature[]>> & TFeatureApis<GFeatures> & TFeatureHostApi<GBase, GFeatures>;
|
|
60
|
+
export interface TFeatureHostApi<GBase extends object, GFeatures extends TInstalledFeature[]> {
|
|
61
|
+
/**
|
|
62
|
+
* @internal Feature metadata used by feature-core. Prefer `hasFeature()` for app code.
|
|
63
|
+
*/
|
|
64
|
+
readonly _features: readonly TFeatureKeys<GFeatures>[];
|
|
65
|
+
/**
|
|
66
|
+
* Installs features on this host.
|
|
67
|
+
*/
|
|
68
|
+
with: TWithFeatureMethod<GBase, GFeatures>;
|
|
69
|
+
}
|
|
70
|
+
export interface TWithFeatureMethod<GBase extends object, GFeatures extends TInstalledFeature[]> {
|
|
71
|
+
<const GFeaturesToInstall extends TAnyFeature[]>(...features: GFeaturesToInstall & TInstallableFeatures<GBase, GFeatures, GFeaturesToInstall>): TFeatureHost<GBase, TInstalledFeaturesAfterAll<GBase, GFeatures, GFeaturesToInstall>>;
|
|
72
|
+
}
|
|
73
|
+
export type TAnyFeature = TFeature<string, readonly string[], TFeatureInstall, TInstalledFeature<string, object>>;
|
|
74
|
+
/**
|
|
75
|
+
* Runtime feature object with an explicit installed feature contract.
|
|
76
|
+
*/
|
|
77
|
+
export type TDeclaredFeature<GInstalledFeature extends TInstalledFeature, GRequiredInstalledFeatures extends readonly TInstalledFeature[] = [], GInstall extends TFeatureInstall<GInstalledFeature['api']> = TFeatureInstall<GInstalledFeature['api']>> = TFeature<GInstalledFeature['key'], TFeatureKeyTuple<GRequiredInstalledFeatures>, GInstall, GInstalledFeature>;
|
|
78
|
+
/**
|
|
79
|
+
* Generic feature install function. The `never` host keeps unconstrained feature installs
|
|
80
|
+
* assignable while concrete features can still narrow the host parameter.
|
|
81
|
+
*/
|
|
82
|
+
export type TFeatureInstall<GApi extends object = object> = (host: never) => GApi;
|
|
83
|
+
export type TInstalledFeaturesAfter<GBase extends object, GFeatures extends TInstalledFeature[], GFeature extends TAnyFeature> = [...GFeatures, TInstalledFeatureFrom<GFeature, TFeatureHost<GBase, GFeatures>>];
|
|
84
|
+
export type TInstalledFeaturesAfterAll<GBase extends object, GFeatures extends TInstalledFeature[], GFeaturesToInstall extends TAnyFeature[]> = GFeaturesToInstall extends [
|
|
85
|
+
infer GFeature extends TAnyFeature,
|
|
86
|
+
...infer GRestFeatures extends TAnyFeature[]
|
|
87
|
+
] ? TInstalledFeaturesAfterAll<GBase, TInstalledFeaturesAfter<GBase, GFeatures, GFeature>, GRestFeatures> : GFeatures;
|
|
88
|
+
export type TInstallableFeatures<GBase extends object, GFeatures extends TInstalledFeature[], GFeaturesToInstall extends TAnyFeature[]> = GFeaturesToInstall extends [
|
|
89
|
+
infer GFeature extends TAnyFeature,
|
|
90
|
+
...infer GRestFeatures extends TAnyFeature[]
|
|
91
|
+
] ? [
|
|
92
|
+
TInstallableFeature<GFeature, TFeatureHost<GBase, GFeatures>>,
|
|
93
|
+
...TInstallableFeatures<GBase, TInstalledFeaturesAfter<GBase, GFeatures, GFeature>, GRestFeatures>
|
|
94
|
+
] : [];
|
|
95
|
+
export type TInstalledFeatureFrom<GFeature extends TAnyFeature, GHost extends TFeatureHost<object, TInstalledFeature[]>> = TDeclaredInstalledFeatureOf<GFeature> extends TInstalledFeature ? TDeclaredInstalledFeatureOf<GFeature> : TInstalledFeature<GFeature['key'], TFeatureApiOf<GFeature, GHost>>;
|
|
96
|
+
export type TDeclaredInstalledFeatureOf<GFeature extends TAnyFeature> = GFeature extends TFeature<string, readonly string[], TFeatureInstall, infer GInstalledFeature> ? [GInstalledFeature] extends [never] ? undefined : GInstalledFeature : undefined;
|
|
97
|
+
export type TFeatureApiOf<GFeature extends TAnyFeature, GHost extends TFeatureHost<object, TInstalledFeature[]>> = GFeature extends {
|
|
98
|
+
install: (host: GHost) => infer GApi;
|
|
99
|
+
} ? GApi extends object ? GApi : object : object;
|
|
100
|
+
export type TInstallableFeature<GFeature extends TAnyFeature, GHost extends TFeatureHost<object, TInstalledFeature[]>> = TFeatureRequirementConstraint<GFeature, TInstalledFeaturesOf<GHost>> & TFeatureInstallConstraint<GFeature, GHost>;
|
|
101
|
+
export type TFeatureRequirementConstraint<GFeature extends TAnyFeature, GFeatures extends TInstalledFeature[]> = TMissingFeatureKeys<GFeatures, GFeature['requires']> extends never ? GFeature : TMissingFeatureError<TMissingFeatureKeys<GFeatures, GFeature['requires']>>;
|
|
102
|
+
export type TFeatureInstallConstraint<GFeature extends TAnyFeature, GHost extends TFeatureHost<object, TInstalledFeature[]>> = GFeature extends {
|
|
103
|
+
install: (host: infer GInstallHost) => object;
|
|
104
|
+
} ? [GInstallHost] extends [never] ? GFeature : GHost extends GInstallHost ? GFeature : TIncompatibleFeatureHostError : GFeature;
|
|
105
|
+
export interface TMissingFeatureError<GMissingFeatures extends string> {
|
|
106
|
+
error: 'Missing required features';
|
|
107
|
+
missing: GMissingFeatures;
|
|
108
|
+
}
|
|
109
|
+
export interface TIncompatibleFeatureHostError {
|
|
110
|
+
error: 'Feature install host is incompatible';
|
|
111
|
+
}
|
|
112
|
+
export type TInstalledFeaturesOf<GHost> = GHost extends TFeatureHostApi<object, infer GFeatures> ? GFeatures : [];
|
|
113
|
+
export type TFeatureKeys<GFeatures extends TInstalledFeature[]> = GFeatures[number]['key'];
|
|
114
|
+
export type TFeatureKeyTuple<GFeatures extends readonly TInstalledFeature[]> = Readonly<{
|
|
115
|
+
[GIndex in keyof GFeatures]: GFeatures[GIndex] extends TInstalledFeature<infer GKey> ? GKey : never;
|
|
116
|
+
}>;
|
|
117
|
+
export type TMissingFeatureKeys<GFeatures extends TInstalledFeature[], GRequiredFeatureKeys extends readonly string[]> = Exclude<GRequiredFeatureKeys[number], TFeatureKeys<GFeatures>>;
|
|
118
|
+
export type TFeatureApis<GFeatures extends TInstalledFeature[]> = TIntersectAll<{
|
|
119
|
+
[GIndex in keyof GFeatures]: GFeatures[GIndex] extends TInstalledFeature<string, infer GApi> ? GApi : object;
|
|
120
|
+
}>;
|
|
121
|
+
export type TIntersectAll<GValues> = GValues extends readonly [infer GFirst, ...infer GRest] ? GFirst & TIntersectAll<GRest> : object;
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "feature-core",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Composable feature primitives for TypeScript libraries",
|
|
6
|
+
"keywords": [],
|
|
7
|
+
"homepage": "https://builder.group/?utm_source=package-json",
|
|
8
|
+
"bugs": {
|
|
9
|
+
"url": "https://github.com/builder-group/community/issues"
|
|
10
|
+
},
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/builder-group/community.git"
|
|
14
|
+
},
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "@bennobuilder",
|
|
17
|
+
"main": "./dist/cjs/index.js",
|
|
18
|
+
"module": "./dist/esm/index.js",
|
|
19
|
+
"source": "./src/index.ts",
|
|
20
|
+
"types": "./dist/types/index.d.ts",
|
|
21
|
+
"files": [
|
|
22
|
+
"dist",
|
|
23
|
+
"README.md"
|
|
24
|
+
],
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@blgc/config": "0.0.42",
|
|
27
|
+
"rollup-presets": "0.0.27"
|
|
28
|
+
},
|
|
29
|
+
"size-limit": [
|
|
30
|
+
{
|
|
31
|
+
"path": "dist/esm/index.js"
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "shx rm -rf dist && rollup -c rollup.config.js",
|
|
36
|
+
"build:prod": "export NODE_ENV=production && pnpm build",
|
|
37
|
+
"clean": "shx rm -rf dist && shx rm -rf .turbo && shx rm -rf node_modules",
|
|
38
|
+
"install:clean": "pnpm run clean && pnpm install",
|
|
39
|
+
"lint": "eslint . --fix",
|
|
40
|
+
"publish:patch": "pnpm build:prod && pnpm version patch && pnpm publish --no-git-checks --access=public",
|
|
41
|
+
"size": "size-limit --why",
|
|
42
|
+
"start:dev": "tsc -w",
|
|
43
|
+
"test": "vitest run",
|
|
44
|
+
"test:types": "vitest run --typecheck.only",
|
|
45
|
+
"update:latest": "pnpm update --latest"
|
|
46
|
+
}
|
|
47
|
+
}
|