Skip to content

Commit

Permalink
refactor: features are now classes and must be built with FeatureBuilder
Browse files Browse the repository at this point in the history
BREAKING CHANGE: `makeFeature` has been dropped in favor of
FeatureBuilder. Also, feature bust now be instantiated instead of
calling `assemble` member. All imports must be upper-cased.
Implementing feature is documented here:
https://formidable-webview.github.io/webshell/docs/implementing-features
  • Loading branch information
jsamr committed Sep 25, 2020
1 parent b6cc99d commit d028853
Show file tree
Hide file tree
Showing 2 changed files with 164 additions and 0 deletions.
66 changes: 66 additions & 0 deletions packages/core/src/Feature.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import type { FeatureDefinition, PropsFromSpecs, PropsSpecs } from './types';

/**
* A lookup type to infer the additional props from a feature.
*
* @public
*/
export type PropsFromFeature<F> = F extends Feature<any, infer S>
? PropsFromSpecs<S>
: never;

/**
* A feature constructor function, aka class.
*
* @public
*/
export interface FeatureConstructor<
O extends {},
S extends PropsSpecs<any> = []
> {
new (...args: O extends Partial<O> ? [] | [O] : [O]): Feature<O, S>;
name: string;
identifier: string;
}

/**
* A lookup type to extract the instance from a {@link FeatureConstructor}.
*
* @public
*/
export type FeatureInstanceOf<F> = F extends FeatureConstructor<
infer O,
infer S
>
? Feature<O, S>
: never;

/**
* A feature encapsulates injectable behaviors in a WebView.
*
* @remarks
* You should never instantiate that class directly. Use {@link FeatureBuilder} instead.
*
* @param params - An object to specify attributes of the feature.
* @typeparam O - The shape of the JSON-serializable object that will be passed to the DOM script.
* @typeparam S - Specifications for the new properties added to webshell.
* @public
*/
export abstract class Feature<O extends {}, S extends PropsSpecs<any> = []>
implements FeatureDefinition<O> {
/**
* {@inheritdoc FeatureDefinition.script}
*/
readonly script: string;
readonly featureIdentifier: string;
readonly propSpecs: S;
readonly defaultOptions: O;
readonly options: O;
constructor(params: FeatureDefinition<O> & { propSpecs: S }, options: O) {
this.script = params.script;
this.featureIdentifier = params.featureIdentifier;
this.propSpecs = params.propSpecs;
this.defaultOptions = params.defaultOptions;
this.options = { ...params.defaultOptions, ...options };
}
}
98 changes: 98 additions & 0 deletions packages/core/src/FeatureBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/* eslint-disable no-spaced-func */
import { Feature } from './Feature';
import type { FeatureConstructor } from './Feature';
import type { FeatureDefinition, PropDefinition, PropsSpecs } from './types';

/**
* See {@link FeatureBuilder}.
*
* @public
*/
export interface FeatureBuilderConfig<
O extends {},
S extends PropsSpecs<any> = []
> extends FeatureDefinition<O> {
/**
* @internal
*/
__propSpecs?: S;
/**
* When present, the returned constructor will be given this name.
* See {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/name | Function.name}
*/
className?: string;
}

/**
* A utility to create feature classes.
*
* @param config - An object to specify attributes of the feature.
*
* @typeparam O - The shape of the JSON-serializable object that will be passed to the DOM script.
* @typeparam S - Specifications for the new properties added by the built feature.
* @public
*/
export class FeatureBuilder<O extends {}, S extends PropsSpecs<any> = []> {
private config: FeatureBuilderConfig<O, S>;

public constructor(config: FeatureBuilderConfig<O, S>) {
this.config = config;
}
/**
* Signal that the feature will receive events from the DOM, and the shell
* will provide a new handler prop.
*
* @param eventHandlerName - The name of the handler prop added to the shell.
* It is advised to follow the convention of prefixing all these handlers
* with `onDom` to avoid collisions with `WebView` own props.
*/
withEventHandlerProp<P, H extends string>(eventHandlerName: H) {
const propDefinition: PropDefinition<{ [k in H]?: (p: P) => void }> = {
name: eventHandlerName,
featureIdentifier: this.config.featureIdentifier,
type: 'handler'
};
return new FeatureBuilder<
O,
S[number] extends never
? [PropDefinition<{ [k in H]?: (p: P) => void }>]
: [PropDefinition<{ [k in H]?: (p: P) => void }>, ...S[number][]]
>({
...this.config,
__propSpecs: [...(this.config.__propSpecs || []), propDefinition] as any
});
}
/**
* Assemble this configuration into a feature class.
*/
build(): FeatureConstructor<O, S> {
const {
script,
featureIdentifier,
className,
__propSpecs: propSpecs,
defaultOptions
} = this.config;
const ctor = class extends Feature<O, S> {
static identifier = featureIdentifier;
constructor(...args: O extends Partial<O> ? [] | [O] : [O]) {
super(
{
script,
featureIdentifier,
defaultOptions,
propSpecs: (propSpecs || []) as S
},
(args[0] || {}) as O
);
}
};
Object.defineProperty(ctor, 'name', {
configurable: true,
enumerable: false,
writable: false,
value: className || 'AnonymousFeature'
});
return ctor;
}
}

0 comments on commit d028853

Please sign in to comment.