Skip to content

feat: make Base.plugin() accept an array instead of multiple arguments, and change APIs from Base.plugin() / Base.defaults() / Base.defaultOptions to Base.withPlugins() / Base.withDefaults() / Base.defaults #64

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Jul 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 33 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
[![@latest](https://img.shields.io/npm/v/javascript-plugin-architecture-with-typescript-definitions.svg)](https://www.npmjs.com/package/javascript-plugin-architecture-with-typescript-definitions)
[![Build Status](https://github.com/gr2m/javascript-plugin-architecture-with-typescript-definitions/workflows/Test/badge.svg)](https://github.com/gr2m/javascript-plugin-architecture-with-typescript-definitions/actions/workflows/test.yml)

The goal of this repository is to provide a template of a simple plugin Architecture which allows plugins to created and authored as separate npm modules and shared as official or 3rd party plugins.
The goal of this repository is to provide a template of a simple plugin Architecture which allows plugins to be created and authored as separate npm modules and shared as official or 3rd party plugins. It also permits the plugins to extend the types for the constructor options.

## Usage

[Try it in TypeScript's playground editor](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgIQIYGcCmcC+cBmUEIcARAFaoBuGAxlMGDALRgA2ArgObAB2zqKLQAWwGJlowOUTMwDuY4cxgBPMJnT1GLACaZ8fMcAi90pANwAoS-g69Jx3nBAqAYhAgAFTj14AKPnQYVHtMAC4UDEwASkRLODgZKSgnBHiEgg8Iv1iAXgA+MnwPUgAadJwrHGtbexhHZxU0KG9uPgDTYNCItCxYtISk6VT0hIAjQWy8wtIJqDKKqpq7BxM4DCxYAGUYBl4uP3QOMfIJGAigva5+6staEyC4dwgAFQ14XMisADp2Nv8XM9Wr5olZ7p1Mq93nBPrxMHInh43kEclYNphtrs+AdilCgt9cTlQdZwY9ns1kR8vphfj52oCPMC+KVGs0mbxiaT4LiKdDYfDERBeSjiejMVc-DzBJSCR4iWj0JsYDsJVKoDK5vKgA)
[Try it in TypeScript's playground editor](https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgIQIYGcCmcC+cBmUEIcARAFaoBuGAxlMGDALRgA2ArgObAB2zqKLQAWwGJlowOUTMwDuY4cxgBPMJnT1GLACaZ8fMcAi90pANwAoS-g69Jx3nBAqAYhAgAFTj14AKPnQYVHtMAC4UDEwASkRLODgZKSgnBHiEgg8Iv1iAXgA+MnwPUgAadJwrHGtbexhHZxU0KG9uPgDTYNCItCxYtISk6VT0hIAjQWy8wtIJqDKKqpq7BxM4DCxYAGUYBl4uP3QOMfIJGAigva5+6staEyC4dwgAFQ14XMisADoFGGFWr50H4ANouZ6AvgAXWiVnunUyr3ecE+vEwcieHjeQRyVg2mG2uz4B2KSKC31JOVh1nhj2ezWxHy+mF+ikhplB4I87NKjWa7JhcIe8FJDORqPRmIgYpx1PxhKuflFgkZFI8VLx6E2MB2iuVUFVcw1QA)

```ts
import { Base } from "javascript-plugin-architecture-with-typescript-definitions";
Expand All @@ -26,31 +26,56 @@ function myBarPlugin(instance: Base) {
};
}

const FooTest = Base.plugin(myFooPlugin);
const FooTest = Base.withPlugins([myFooPlugin]);
const fooTest = new FooTest();
fooTest.foo(); // has full TypeScript intellisense

const FooBarTest = Base.plugin(myFooPlugin, myBarPlugin);
const FooBarTest = Base.withPlugins([myFooPlugin, myBarPlugin]);
const fooBarTest = new FooBarTest();
fooBarTest.foo(); // has full TypeScript intellisense
fooBarTest.bar(); // has full TypeScript intellisense
```

The constructor accepts an optional `options` object which is passed to the plugins as second argument and stored in `instance.options`. Default options can be set using `Base.defaults(options)`
The constructor accepts an optional `options` object which is passed to the plugins as second argument and stored in `instance.options`. Default options can be set using `Base.withDefaults(options)`.

```js
const BaseWithOptions = Base.defaults({ foo: "bar" });
const BaseWithOptions = Base.withDefaults({ foo: "bar" });
const instance = new BaseWithOptions();
instance.options; // {foo: 'bar'}
```

Note that in for TypeScript to recognize the new option, you have to extend the `Base.Option` intererface.

```ts
declare module "javascript-plugin-architecture-with-typescript-definitions" {
namespace Base {
interface Options {
foo: string;
}
}
}
```

See also the [`required-options` example](examples/required-options).

The `Base` class also has two static properties

- `.defaults`: the default options for all instances
- `.plugins`: the list of plugins applied to all instances

When creating a new class with `.withPlugins()` and `.defaults()`, the static properties of the returned class are set accordingly.

```js
const MyBase = Base.withDefaults({ foo: "bar" });
```

### Defaults

TypeScript will not complain when chaining `.defaults()` calls endlessly: the static `.defaultOptions` property will be set correctly. However, when instantiating from a class with 4+ chained `.defaults()` calls, then only the defaults from the first 3 calls are supported. See [#57](https://github.com/gr2m/javascript-plugin-architecture-with-typescript-definitions/pull/57) for details.
TypeScript will not complain when chaining `.withDefaults()` calls endlessly: the static `.defaults` property will be set correctly. However, when instantiating from a class with 4+ chained `.withDefaults()` calls, then only the defaults from the first 3 calls are supported. See [#57](https://github.com/gr2m/javascript-plugin-architecture-with-typescript-definitions/pull/57) for details.

## Credit

This plugin architecture was extracted from [`@octokit/core`](https://github.com/octokit/core.js). The implementation was made possible by help from [@karol-majewski](https://github.com/karol-majewski), [@dragomirtitian](https://github.com/dragomirtitian), and [StackOverflow user "hackape"](https://stackoverflow.com/a/58706699/206879).
This plugin architecture was extracted from [`@octokit/core`](https://github.com/octokit/core.js). The implementation was made possible by help from [@karol-majewski](https://github.com/karol-majewski), [@dragomirtitian](https://github.com/dragomirtitian), [StackOverflow user "hackape"](https://stackoverflow.com/a/58706699/206879), and [@JoshuaKGoldberg](https://github.com/JoshuaKGoldberg).

## LICENSE

Expand Down
2 changes: 1 addition & 1 deletion examples/required-options/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ function pluginRequiringOption(base, options) {
}
}

export const MyBase = Base.plugin(pluginRequiringOption);
export const MyBase = Base.withPlugins([pluginRequiringOption]);
2 changes: 1 addition & 1 deletion examples/required-options/index.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ new MyBase({
myRequiredUserOption: "",
});

const MyBaseWithDefaults = MyBase.defaults({
const MyBaseWithDefaults = MyBase.withDefaults({
myRequiredUserOption: "",
});

Expand Down
40 changes: 23 additions & 17 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export declare namespace Base {
declare type ApiExtension = {
[key: string]: unknown;
};
declare type Plugin = (
export declare type Plugin = (
instance: Base,
options: Base.Options
) => ApiExtension | void;
Expand Down Expand Up @@ -51,11 +51,11 @@ type RequiredIfRemaining<PredefinedOptions, NowProvided> = NonOptionalKeys<
NowProvided
];

type ConstructorRequiringVersion<
type ConstructorRequiringOptionsIfNeeded<
Class extends ClassWithPlugins,
PredefinedOptions
> = {
defaultOptions: PredefinedOptions;
defaults: PredefinedOptions;
} & {
new <NowProvided>(
...options: RequiredIfRemaining<PredefinedOptions, NowProvided>
Expand All @@ -65,8 +65,6 @@ type ConstructorRequiringVersion<
};

export declare class Base<TOptions extends Base.Options = Base.Options> {
static plugins: Plugin[];

/**
* Pass one or multiple plugin functions to extend the `Base` class.
* The instance of the new class will be extended with any keys returned by the passed plugins.
Expand All @@ -81,17 +79,17 @@ export declare class Base<TOptions extends Base.Options = Base.Options> {
* };
* }
*
* const MyBase = Base.plugin(helloWorld);
* const MyBase = Base.withPlugins([helloWorld]);
* const base = new MyBase();
* base.helloWorld(); // `base.helloWorld` is typed as function
* ```
*/
static plugin<
static withPlugins<
Class extends ClassWithPlugins,
Plugins extends [Plugin, ...Plugin[]]
>(
this: Class,
...plugins: Plugins
plugins: Plugins
): Class & {
plugins: [...Class["plugins"], ...Plugins];
} & Constructor<UnionToIntersection<ReturnTypeOf<Plugins>>>;
Expand All @@ -100,37 +98,37 @@ export declare class Base<TOptions extends Base.Options = Base.Options> {
* Set defaults for the constructor
*
* ```js
* const MyBase = Base.defaults({ version: '1.0.0', otherDefault: 'value' });
* const MyBase = Base.withDefaults({ version: '1.0.0', otherDefault: 'value' });
* const base = new MyBase({ option: 'value' }); // `version` option is not required
* base.options // typed as `{ version: string, otherDefault: string, option: string }`
* ```
* @remarks
* Ideally, we would want to make this infinitely recursive: allowing any number of
* .defaults({ ... }).defaults({ ... }).defaults({ ... }).defaults({ ... })...
* .withDefaults({ ... }).withDefaults({ ... }).withDefaults({ ... }).withDefaults({ ... })...
* However, we don't see a clean way in today's TypeScript syntax to do so.
* We instead artificially limit accurate type inference to just three levels,
* since real users are not likely to go past that.
* @see https://github.com/gr2m/javascript-plugin-architecture-with-typescript-definitions/pull/57
*/
static defaults<
static withDefaults<
PredefinedOptionsOne,
ClassOne extends Constructor<Base<Base.Options & PredefinedOptionsOne>> &
ClassWithPlugins
>(
this: ClassOne,
defaults: PredefinedOptionsOne
): ConstructorRequiringVersion<ClassOne, PredefinedOptionsOne> & {
defaults<ClassTwo, PredefinedOptionsTwo>(
): ConstructorRequiringOptionsIfNeeded<ClassOne, PredefinedOptionsOne> & {
withDefaults<ClassTwo, PredefinedOptionsTwo>(
this: ClassTwo,
defaults: PredefinedOptionsTwo
): ConstructorRequiringVersion<
): ConstructorRequiringOptionsIfNeeded<
ClassOne & ClassTwo,
PredefinedOptionsOne & PredefinedOptionsTwo
> & {
defaults<ClassThree, PredefinedOptionsThree>(
withDefaults<ClassThree, PredefinedOptionsThree>(
this: ClassThree,
defaults: PredefinedOptionsThree
): ConstructorRequiringVersion<
): ConstructorRequiringOptionsIfNeeded<
ClassOne & ClassTwo & ClassThree,
PredefinedOptionsOne & PredefinedOptionsTwo & PredefinedOptionsThree
> &
Expand All @@ -141,7 +139,15 @@ export declare class Base<TOptions extends Base.Options = Base.Options> {
ClassTwo;
} & ClassOne;

static defaultOptions: {};
/**
* list of plugins that will be applied to all instances
*/
static plugins: Plugin[];

/**
* list of default options that will be applied to all instances
*/
static defaults: {};

/**
* options passed to the constructor as constructor defaults
Expand Down
9 changes: 4 additions & 5 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export class Base {
});
}

static plugin(...newPlugins) {
static withPlugins(newPlugins) {
const currentPlugins = this.plugins;
return class extends this {
static plugins = currentPlugins.concat(
Expand All @@ -15,7 +15,7 @@ export class Base {
};
}

static defaults(defaults) {
static withDefaults(defaults) {
return class extends this {
constructor(options) {
super({
Expand All @@ -24,11 +24,10 @@ export class Base {
});
}

static defaultOptions = { ...defaults, ...this.defaultOptions };
static defaults = { ...defaults, ...this.defaults };
};
}

static defaultOptions = {};

static plugins = [];
static defaults = {};
}
Loading