-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Introduce package for Features (#7242)
Signed-off-by: Janne Savolainen <janne.savolainen@live.fi>
- Loading branch information
Showing
13 changed files
with
730 additions
and
0 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
# @k8slens/feature-core | ||
|
||
Feature is set of injectables that are registered and deregistered simultaneously. | ||
|
||
## Install | ||
```bash | ||
$ npm install @k8slens/feature-core | ||
``` | ||
|
||
## Usage | ||
|
||
```typescript | ||
import { createContainer } from "@ogre-tools/injectable" | ||
import { getFeature, registerFeature, deregisterFeature } from "@k8slens/feature-core" | ||
|
||
// Notice that this Feature is usually exported from another NPM package. | ||
const someFeature = getFeature({ | ||
id: "some-feature", | ||
|
||
register: (di) => { | ||
di.register(someInjectable, someOtherInjectable); | ||
}, | ||
|
||
// Feature dependencies are automatically registered and | ||
// deregistered when necessary. | ||
dependencies: [someOtherFeature] | ||
}); | ||
|
||
const di = createContainer("some-container"); | ||
|
||
registerFeature(di, someFeature); | ||
|
||
// Or perhaps you want to deregister? | ||
deregisterFeature(di, someFeature); | ||
``` | ||
|
||
## Need to know | ||
|
||
#### NPM packages exporting a Feature | ||
- Prefer `peerDependencies` since they are installed from the application and are not allowed to be in the built bundle. | ||
- Prefer exporting `injectionToken` instead of `injectable` for not allowing other features to access technical details like the `injectable` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export { getFeature } from "./src/feature"; | ||
export { registerFeature } from "./src/register-feature"; | ||
export type { Feature, GetFeatureArgs } from "./src/feature"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
module.exports = | ||
require("@k8slens/jest").monorepoPackageConfig(__dirname).configForReact; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
{ | ||
"name": "@k8slens/feature-core", | ||
"private": false, | ||
"version": "0.0.1", | ||
"description": "Code that is common to all Features and those registering them.", | ||
"type": "commonjs", | ||
"files": [ | ||
"dist" | ||
], | ||
"repository": { | ||
"type": "git", | ||
"url": "git+https://github.com/lensapp/lens.git" | ||
}, | ||
"main": "dist/index.js", | ||
"types": "dist/index.d.ts", | ||
"author": { | ||
"name": "OpenLens Authors", | ||
"email": "info@k8slens.dev" | ||
}, | ||
"license": "MIT", | ||
"homepage": "https://github.com/lensapp/lens", | ||
"scripts": { | ||
"build": "webpack", | ||
"dev": "webpack --mode=development --watch", | ||
"test": "jest --coverage --runInBand" | ||
}, | ||
"peerDependencies": { | ||
"@ogre-tools/injectable": "^15.1.1" | ||
} | ||
} |
82 changes: 82 additions & 0 deletions
82
packages/technical-features/feature-core/src/deregister-feature.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
import type { DiContainer } from "@ogre-tools/injectable"; | ||
import type { Feature } from "./feature"; | ||
import { featureContextMapInjectable } from "./feature-context-map-injectable"; | ||
|
||
export const deregisterFeature = (di: DiContainer, ...features: Feature[]) => { | ||
features.forEach((feature) => { | ||
deregisterFeatureRecursed(di, feature); | ||
}); | ||
}; | ||
|
||
const deregisterFeatureRecursed = ( | ||
di: DiContainer, | ||
feature: Feature, | ||
dependedBy?: Feature | ||
) => { | ||
const featureContextMap = di.inject(featureContextMapInjectable); | ||
|
||
const featureContext = featureContextMap.get(feature); | ||
|
||
if (!featureContext) { | ||
throw new Error( | ||
`Tried to deregister feature "${feature.id}", but it was not registered.` | ||
); | ||
} | ||
|
||
featureContext.numberOfRegistrations--; | ||
|
||
const getDependingFeatures = getDependingFeaturesFor(featureContextMap); | ||
|
||
const dependingFeatures = getDependingFeatures(feature); | ||
|
||
if (!dependedBy && dependingFeatures.length) { | ||
throw new Error( | ||
`Tried to deregister Feature "${ | ||
feature.id | ||
}", but it is the dependency of Features "${dependingFeatures.join( | ||
", " | ||
)}"` | ||
); | ||
} | ||
|
||
if (dependedBy) { | ||
const oldNumberOfDependents = featureContext.dependedBy.get(dependedBy)!; | ||
const newNumberOfDependants = oldNumberOfDependents - 1; | ||
featureContext.dependedBy.set(dependedBy, newNumberOfDependants); | ||
|
||
if (newNumberOfDependants === 0) { | ||
featureContext.dependedBy.delete(dependedBy); | ||
} | ||
} | ||
|
||
if (featureContext.numberOfRegistrations === 0) { | ||
featureContextMap.delete(feature); | ||
|
||
featureContext.deregister(); | ||
} | ||
|
||
feature.dependencies?.forEach((dependency) => { | ||
deregisterFeatureRecursed(di, dependency, feature); | ||
}); | ||
}; | ||
|
||
const getDependingFeaturesFor = ( | ||
featureContextMap: Map<Feature, { dependedBy: Map<Feature, number> }> | ||
) => { | ||
const getDependingFeaturesForRecursion = ( | ||
feature: Feature, | ||
atRoot = true | ||
): string[] => { | ||
const context = featureContextMap.get(feature); | ||
|
||
if (context?.dependedBy.size) { | ||
return [...context!.dependedBy.entries()].flatMap(([dependant]) => | ||
getDependingFeaturesForRecursion(dependant, false) | ||
); | ||
} | ||
|
||
return atRoot ? [] : [feature.id]; | ||
}; | ||
|
||
return getDependingFeaturesForRecursion; | ||
}; |
27 changes: 27 additions & 0 deletions
27
packages/technical-features/feature-core/src/feature-context-map-injectable.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { getInjectable, getInjectionToken } from "@ogre-tools/injectable"; | ||
import type { Feature } from "./feature"; | ||
|
||
export type FeatureContextMap = Map< | ||
Feature, | ||
{ | ||
register: () => void; | ||
deregister: () => void; | ||
dependedBy: Map<Feature, number>; | ||
numberOfRegistrations: number; | ||
} | ||
>; | ||
|
||
export const featureContextMapInjectionToken = | ||
getInjectionToken<FeatureContextMap>({ | ||
id: "feature-context-map-injection-token", | ||
}); | ||
|
||
const featureContextMapInjectable = getInjectable({ | ||
id: "feature-store", | ||
|
||
instantiate: (): FeatureContextMap => new Map(), | ||
|
||
injectionToken: featureContextMapInjectionToken, | ||
}); | ||
|
||
export { featureContextMapInjectable }; |
Oops, something went wrong.