From 7286a62a802917872acffe9d1a2c170e6275e91b Mon Sep 17 00:00:00 2001 From: Konstantin Schaper Date: Wed, 12 May 2021 16:05:30 +0200 Subject: [PATCH] Implement api for extension point typings (#1638) Currently, the only way to explore available extension points is through our documentation or by browsing the source code. Once you find them, there is no guard rails and the usage is prone to user errors. This new api allows the declaration of extension points as types in code. This way, exposing an extension point is as easy as exporting it from a module. Both the implementation and the developer who uses the extension point work with the same shared type that allows auto-completion and type-checks for safety. This feature is backwards-compatible as the generic methods all have sensible defaults for the type parameters. Co-authored-by: Sebastian Sdorra Co-authored-by: Eduard Heimbuch --- docs/en/development/ui-extensions.md | 34 + gradle/changelog/extension-point-typings.yaml | 2 + scm-plugins/scm-git-plugin/package.json | 2 +- scm-plugins/scm-hg-plugin/package.json | 2 +- scm-plugins/scm-legacy-plugin/package.json | 2 +- scm-plugins/scm-svn-plugin/package.json | 2 +- scm-ui/ui-api/package.json | 2 +- scm-ui/ui-api/src/repositories.ts | 2 +- scm-ui/ui-components/package.json | 2 +- .../src/__snapshots__/storyshots.test.ts.snap | 3885 ++++---- .../ui-components/src/forms/FilterInput.tsx | 7 +- scm-ui/ui-extensions/package.json | 2 +- scm-ui/ui-extensions/src/binder.test.ts | 46 +- scm-ui/ui-extensions/src/binder.ts | 68 +- scm-ui/ui-extensions/src/index.ts | 2 +- scm-ui/ui-plugins/package.json | 2 +- scm-ui/ui-tests/package.json | 4 +- yarn.lock | 8586 ++++++----------- 18 files changed, 5335 insertions(+), 7317 deletions(-) create mode 100644 gradle/changelog/extension-point-typings.yaml diff --git a/docs/en/development/ui-extensions.md b/docs/en/development/ui-extensions.md index 1b3a22af3d..7f0821550f 100644 --- a/docs/en/development/ui-extensions.md +++ b/docs/en/development/ui-extensions.md @@ -107,3 +107,37 @@ binder.bind("repo.avatar", GitAvatar, (props) => props.type === "git"); ```javascript ``` + +### Typings + +Both extension points and extensions can share a common typescript type to define the contract between them. +This includes the `name`, the type of `props` passed to the predicate and what `type` the extensions themselves can be. + +Example: +```typescript + type CalculatorExtensionPoint = ExtensionPointDefinition<"extension.calculator", (input: number[]) => number, undefined>; + + const sum = (a: number, b: number) => a + b; + binder.bind("extension.calculator", (input: number[]) => input.reduce(sum, 0)); + const calculator = binder.getExtension("extension.calculator"); + const result = calculator([1, 2, 3]); +``` + +In this example, we use the base type `ExtensionPointDefinition` to declare a new extension point. + +As we do not need a predicate, we can define the `props` type parameter as `undefined`. This allows us to skip the `props` parameter in the +`getExtension` method and the `predicate` parameter in the `bind` method. + +When using `bind` to define an extension or `getExtension` to retrieve an extension, we can pass the new type as a type parameter. +By doing this, we allow typescript to help us with type-checks and offer us type-completion. + +Negative Example: +```typescript + type CalculatorExtensionPoint = ExtensionPointDefinition<"extension.calculator", (input: number[]) => number, undefined>; + + const sum = (a: number, b: number) => a + b; + binder.bind("extension.calculato", (input: number[]) => input.reduce(sum, 0)); +``` + +This code for example, would lead to a compile time type error because we made a typo in the `name` of the extension when binding it. +If we had used the `bind` method without the type parameter, we would not have gotten an error but run into problems at runtime. diff --git a/gradle/changelog/extension-point-typings.yaml b/gradle/changelog/extension-point-typings.yaml new file mode 100644 index 0000000000..0322582413 --- /dev/null +++ b/gradle/changelog/extension-point-typings.yaml @@ -0,0 +1,2 @@ +- type: added + description: Implement api for extension point typings ([#1638](https://github.com/scm-manager/scm-manager/pull/1638)) diff --git a/scm-plugins/scm-git-plugin/package.json b/scm-plugins/scm-git-plugin/package.json index a2aab63ff7..3396f49177 100644 --- a/scm-plugins/scm-git-plugin/package.json +++ b/scm-plugins/scm-git-plugin/package.json @@ -14,7 +14,7 @@ "@scm-manager/ui-plugins": "^2.18.1-SNAPSHOT" }, "devDependencies": { - "@scm-manager/babel-preset": "^2.11.1", + "@scm-manager/babel-preset": "^2.12.0", "@scm-manager/eslint-config": "^2.11.1", "@scm-manager/jest-preset": "^2.12.7", "@scm-manager/plugin-scripts": "^1.0.1", diff --git a/scm-plugins/scm-hg-plugin/package.json b/scm-plugins/scm-hg-plugin/package.json index 10c26de915..7b3c149a11 100644 --- a/scm-plugins/scm-hg-plugin/package.json +++ b/scm-plugins/scm-hg-plugin/package.json @@ -13,7 +13,7 @@ "@scm-manager/ui-plugins": "^2.18.1-SNAPSHOT" }, "devDependencies": { - "@scm-manager/babel-preset": "^2.11.1", + "@scm-manager/babel-preset": "^2.12.0", "@scm-manager/eslint-config": "^2.11.1", "@scm-manager/jest-preset": "^2.12.7", "@scm-manager/plugin-scripts": "^1.0.1", diff --git a/scm-plugins/scm-legacy-plugin/package.json b/scm-plugins/scm-legacy-plugin/package.json index 437e40623e..03c43c145d 100644 --- a/scm-plugins/scm-legacy-plugin/package.json +++ b/scm-plugins/scm-legacy-plugin/package.json @@ -13,7 +13,7 @@ "@scm-manager/ui-plugins": "^2.18.1-SNAPSHOT" }, "devDependencies": { - "@scm-manager/babel-preset": "^2.11.1", + "@scm-manager/babel-preset": "^2.12.0", "@scm-manager/eslint-config": "^2.11.1", "@scm-manager/jest-preset": "^2.12.7", "@scm-manager/plugin-scripts": "^1.0.1", diff --git a/scm-plugins/scm-svn-plugin/package.json b/scm-plugins/scm-svn-plugin/package.json index 50bc029b9d..b4b62262f1 100644 --- a/scm-plugins/scm-svn-plugin/package.json +++ b/scm-plugins/scm-svn-plugin/package.json @@ -13,7 +13,7 @@ "@scm-manager/ui-plugins": "^2.18.1-SNAPSHOT" }, "devDependencies": { - "@scm-manager/babel-preset": "^2.11.1", + "@scm-manager/babel-preset": "^2.12.0", "@scm-manager/eslint-config": "^2.11.1", "@scm-manager/jest-preset": "^2.12.7", "@scm-manager/plugin-scripts": "^1.0.1", diff --git a/scm-ui/ui-api/package.json b/scm-ui/ui-api/package.json index d4a8af91e7..25f9c17546 100644 --- a/scm-ui/ui-api/package.json +++ b/scm-ui/ui-api/package.json @@ -15,7 +15,7 @@ "typecheck": "tsc" }, "devDependencies": { - "@scm-manager/babel-preset": "^2.11.2", + "@scm-manager/babel-preset": "^2.12.0", "@scm-manager/eslint-config": "^2.10.1", "@scm-manager/jest-preset": "^2.12.7", "@scm-manager/prettier-config": "^2.10.1", diff --git a/scm-ui/ui-api/src/repositories.ts b/scm-ui/ui-api/src/repositories.ts index 3f14afe2ad..63506740bf 100644 --- a/scm-ui/ui-api/src/repositories.ts +++ b/scm-ui/ui-api/src/repositories.ts @@ -284,7 +284,7 @@ const EXPORT_MEDIA_TYPE = "application/vnd.scmm-repositoryExport+json;v=2"; export const useExportRepository = () => { const queryClient = useQueryClient(); - const [intervalId, setIntervalId] = useState(); + const [intervalId, setIntervalId] = useState>(); useEffect(() => { return () => { if (intervalId) { diff --git a/scm-ui/ui-components/package.json b/scm-ui/ui-components/package.json index 290c7232ff..1b1546a772 100644 --- a/scm-ui/ui-components/package.json +++ b/scm-ui/ui-components/package.json @@ -20,7 +20,7 @@ "update-storyshots": "jest --testPathPattern=\"storyshots.test.ts\" --collectCoverage=false -u" }, "devDependencies": { - "@scm-manager/babel-preset": "^2.11.2", + "@scm-manager/babel-preset": "^2.12.0", "@scm-manager/eslint-config": "^2.12.0", "@scm-manager/jest-preset": "^2.12.7", "@scm-manager/prettier-config": "^2.10.1", diff --git a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap index b6765931b0..4ff705e8e8 100644 --- a/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap +++ b/scm-ui/ui-components/src/__snapshots__/storyshots.test.ts.snap @@ -2,7 +2,7 @@ exports[`Storyshots Annotate Default 1`] = `
Arthur Dent
1
2
Tricia Marie McMillan
3
4
Arthur Dent
5
Ford Prefect
6
Arthur Dent
7
8
Arthur Dent
1
2
Tricia Marie McMillan
3
4
Arthur Dent
5
Ford Prefect
6
Arthur Dent
7
8
Arthur Dent Arthur Dent
1
2
Tricia Marie McMillan Tricia Marie McMillan
3
4
Arthur Dent Arthur Dent
5
Ford Prefect Ford Prefect
6
Arthur Dent Arthur Dent
7
8