From aaed6775c243c36b05022b2356449d8ba7bd6683 Mon Sep 17 00:00:00 2001 From: Pat Lillis <53907200+patlillis@users.noreply.github.com> Date: Mon, 11 Nov 2019 10:56:15 -0700 Subject: [PATCH] Add types for nuclear-js package (#40221) * Add types from nuclear-js * Fix linting --- types/nuclear-js/index.d.ts | 263 +++++++++++++++++++++++++++ types/nuclear-js/nuclear-js-tests.ts | 181 ++++++++++++++++++ types/nuclear-js/package.json | 6 + types/nuclear-js/tsconfig.json | 24 +++ types/nuclear-js/tslint.json | 3 + 5 files changed, 477 insertions(+) create mode 100644 types/nuclear-js/index.d.ts create mode 100644 types/nuclear-js/nuclear-js-tests.ts create mode 100644 types/nuclear-js/package.json create mode 100644 types/nuclear-js/tsconfig.json create mode 100644 types/nuclear-js/tslint.json diff --git a/types/nuclear-js/index.d.ts b/types/nuclear-js/index.d.ts new file mode 100644 index 0000000000..75f017cfc7 --- /dev/null +++ b/types/nuclear-js/index.d.ts @@ -0,0 +1,263 @@ +// Type definitions for nuclear-js 1.4 +// Project: https://github.com/optimizely/nuclear-js +// Definitions by: Pat Lillis +// Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped +// TypeScript Version: 2.3 + +import * as _Immutable from 'immutable'; + +// Disable automatic exports. +export {}; + +// NuclearJS re-exports everything in ImmutableJS. +export import Immutable = _Immutable; + +interface ReactorConfig { + /** If true it will log the entire app state for every dispatch. */ + debug?: boolean; +} + +// Getters have a really complex, recursive type that can't be represented +// in TypeScript, but at a high level they are all Arrays. +type Getter = any[]; + +interface ReactMixin { + getInitialState(): any; + componentDidMount(): void; + componentWillUnmount(): void; +} + +interface Reactor { + prevReactorState: any; + reactorState: any; + observerState: any; + ReactMixin: ReactMixin; + + /** + * Dispatches a message to all registered Stores. + * + * This process is done synchronously, all registered Stores are passed + * this message and all components are re-evaluated (efficiently). After + * a dispatch, a Reactor will emit the new state on the + * reactor.changeEmitter. + */ + dispatch(actionType: string, payload?: any): void; + + /** + * Allows multiple dispatches within the `fn` function before notifying + * any observers. + */ + batch(fn: () => void): void; + + /** + * Returns the immutable value for some KeyPath or Getter in the reactor + * state. + * + * Returns `undefined` if a keyPath doesn't have a value. + */ + evaluate(getter: Getter): any; + + /** + * Returns a plain JS value for some KeyPath or Getter in the reactor + * state. + * + * Returns `undefined` if a keyPath doesn't have a value. + */ + evaluateToJS(getter: Getter): any; + + /** + * Adds a change observer that is invoked whenever any part of the + * reactor state changes. + */ + observe(handler: () => void): () => void; + + /** + * Adds a change observer that is invoked whenever any dependencies of + * the getter change. + * + * @returns An "unsubscribe" function + */ + observe(getter: Getter, handler: (value?: any) => void): () => void; + + /** + * Removes the change observer for the getter. + */ + unobserve(getter: Getter, handler: (value?: any) => void): void; + + /** + * Returns a plain JavaScript object representing the application state. + * + * By default this maps over all stores and returns `toJS(storeState)`. + */ + serialize(): any; + + /** + * Takes a plain JavaScript object and merges into the reactor state, + * using `store.deserialize()`. + * + * This can be useful if you need to load data already on the page. + */ + loadState(state: any): void; + + /** + * Registers stores. + */ + registerStores(stores: { [storeName: string]: Store }): void; + + /** + * Replace store implementation (handlers) without modifying the app + * state or calling `getInitialState`. + * + * Useful for hot reloading + */ + replaceStores(stores: { [storeName: string]: Store }): void; + + /** + * Resets the state of a reactor and returns it back to initial state. + */ + reset(): void; +} + +export const Reactor: { + /** + * State is stored in NuclearJS Reactors. Reactors contain a `state` object + * which is an Immutable.Map + * + * The only way Reactors can change state is by reacting to messages. To + * update state, Reactor's dispatch messages to all registered stores, and + * the store returns it's new state based on the message + */ + new (config?: ReactorConfig): Reactor; + + /** + * State is stored in NuclearJS Reactors. Reactors contain a `state` object + * which is an Immutable.Map + * + * The only way Reactors can change state is by reacting to messages. To + * update state, Reactor's dispatch messages to all registered stores, and + * the store returns it's new state based on the message + */ + (config?: ReactorConfig): Reactor; +}; + +interface Store extends StoreLike { + /** + * Takes a current reactor state, action type and payload, does the + * reaction, and returns the new state. + */ + handle(state: T, actionType: string, payload?: any): T; + + /** + * Binds an action type to a handler. + */ + on(actionType: string, handler: (state: T, payload?: any) => T): void; + + /** + * Pure function taking the current state of store and returning the new + * state after a NuclearJS reactor has been reset + */ + handleReset(this: Store, state: T): T; + + /** + * Serializes store state to plain JSON serializable JavaScript. + */ + serialize(this: Store, state: T): any; + + /** + * Deserializes plain JavaScript to store state. + */ + deserialize(this: Store, state: any): T; +} + +/** + * Stores are initialized like: + * + * ``` + * new Store({ + * initialize() { ... }, + * getInitialState() { ... }, + * }) + * ``` + * + * This type defines the functions for the object passed to the + * `new Store()` constructor. In additional, all of these functions are + * available on the base `Store` object itself. + */ +interface StoreLike { + /** + * Gets the initial state for this type of store + */ + getInitialState(this: Store): T; + + /** + * Sets up message handlers via `this.on` and to set up the initial + * state. + */ + initialize(this: Store): void; + + /** + * Pure function taking the current state of store and returning the new + * state after a NuclearJS reactor has been reset + */ + handleReset?(this: Store, state: T): T; + + /** + * Serializes store state to plain JSON serializable JavaScript. + */ + serialize?(this: Store, state: T): any; + + /** + * Deserializes plain JavaScript to store state. + */ + deserialize?(this: Store, state: any): T; +} + +export const Store: { + /** + * A Store defines how a certain domain of the application should respond to + * actions taken on the whole system. They manage their own section of the + * entire app state and have no knowledge about the other parts of the + * application state. + */ + new (config: StoreLike): Store; + + /** + * A Store defines how a certain domain of the application should respond to + * actions taken on the whole system. They manage their own section of the + * entire app state and have no knowledge about the other parts of the + * application state. + */ + (config: StoreLike): Store; +}; + +/** + * Checks if something is simply a keyPath and not a getter. + */ +export function isKeyPath(toTest: any): boolean; + +/** + * Checks if something is a getter literal. + * + * For example, `['dep1', 'dep2', function(dep1, dep2) {...}]`. + */ +export function isGetter(toTest: any): boolean; + +/** + * Converts an Immutable Sequence to JS object. + * + * Can be called on any type. + */ +export function toJS(arg: any): any; + +/** + * Converts a JS object to an Immutable object, if it's already Immutable its a + * no-op. + */ +export function toImmutable(arg: any): any; + +/** + * Returns true if the value is an ImmutableJS data structure. + */ +export function isImmutable(arg: any): boolean; + +export function createReactMixin(reactor: Reactor): ReactMixin; diff --git a/types/nuclear-js/nuclear-js-tests.ts b/types/nuclear-js/nuclear-js-tests.ts new file mode 100644 index 0000000000..71e4e88230 --- /dev/null +++ b/types/nuclear-js/nuclear-js-tests.ts @@ -0,0 +1,181 @@ +import { + Immutable, + Reactor, + Store, + isKeyPath, + isGetter, + toJS, + toImmutable, + isImmutable, + createReactMixin, +} from 'nuclear-js'; + +Immutable.Map({ a: 1 }); +Immutable.fromJS([5]); + +// Callable with or without `new`. +new Reactor(); +Reactor(); +new Reactor({ debug: true }); +Reactor({ debug: undefined }); +// Make sure that type checking succeeds with or without `new`. +const r1 = new Reactor(); +const r2 = Reactor(); +r1.dispatch('FETCH_ENTITY_SUCCESS'); +r1.dispatch('FETCH_ENTITY_SUCCESS', { data: 5 }); +r1.batch(() => null); +r1.evaluate(['keyPath']); +r1.evaluate([['keyPath'], (dep1: any) => 5]); +r1.evaluateToJS(['keyPath']); +r1.evaluateToJS([['keyPath'], (dep1: any) => 5]); +r1.observe(() => null)(); +r1.observe(['getter'], (x: any) => null)(); +r1.observe(['getter'], () => null)(); +r1.unobserve(['getter'], (x: any) => null); +r1.unobserve(['getter'], () => null); +r1.serialize(); +r1.loadState({}); +r1.registerStores({}); +r1.registerStores({ + numberStore: new Store({ + getInitialState() { + return 5; + }, + initialize() {}, + }), +}); +r1.replaceStores({}); +r1.replaceStores({ + numberStore: new Store({ + getInitialState() { + return 5; + }, + initialize() {}, + }), +}); +r1.prevReactorState; +r1.reactorState; +r1.observerState; +r1.ReactMixin.componentDidMount(); +r1.ReactMixin.componentWillUnmount(); +r1.ReactMixin.getInitialState(); +r2.reset(); +r2.dispatch('FETCH_ENTITY_SUCCESS'); +r2.dispatch('FETCH_ENTITY_SUCCESS', { data: 5 }); +r2.batch(() => null); +r2.evaluate(['keyPath']); +r2.evaluate([['keyPath'], (dep1: any) => 5]); +r2.evaluateToJS(['keyPath']); +r2.evaluateToJS([['keyPath'], (dep1: any) => 5]); +r2.observe(() => null)(); +r2.observe(['getter'], (x: any) => null)(); +r2.observe(['getter'], () => null)(); +r2.unobserve(['getter'], (x: any) => null); +r2.unobserve(['getter'], () => null); +r2.serialize(); +r2.loadState({}); +r2.registerStores({}); +r2.registerStores({ + numberStore: new Store({ + getInitialState() { + return 5; + }, + initialize() {}, + }), +}); +r2.replaceStores({}); +r2.replaceStores({ + numberStore: new Store({ + getInitialState() { + return 5; + }, + initialize() {}, + }), +}); +r2.reset(); +r2.prevReactorState; +r2.reactorState; +r2.observerState; +r2.ReactMixin.componentDidMount(); +r2.ReactMixin.componentWillUnmount(); +r2.ReactMixin.getInitialState(); + +// Callable with or without `new`. +new Store({ getInitialState() {}, initialize() {} }); +Store({ getInitialState() {}, initialize() {} }); +new Store({ getInitialState() {}, initialize() {} }); +Store({ getInitialState() {}, initialize() {} }); +// Make sure that type checking succeeds with or without `new`. +const s1 = new Store({ + getInitialState() { + return 5; + }, + initialize() { + this.on('FETCH_THING', (s: number, x: any) => 5); + }, +}); +const s2 = Store({ + getInitialState() { + return ''; + }, + initialize() { + this.on('FETCH_THING', (s: string) => '5'); + }, + handleReset(s: string) { + return '15'; + }, +}); +s1.getInitialState(); +s1.initialize(); +s1.handleReset(5); +s1.serialize(15); +s1.deserialize({}); +s1.handle(51, 'FETCH_THING', {}); +s1.on('FETCH_THING', (x: number, y: any) => 15); +s2.getInitialState(); +s2.initialize(); +s2.handleReset('5'); +s2.serialize('15'); +s2.deserialize({}); +s2.handle('51', 'FETCH_THING', {}); +s2.on('FETCH_THING', (x: string, y: any) => '15'); + +isKeyPath({}); +isKeyPath(['getter']); +isKeyPath(''); +isKeyPath(5); + +isGetter({}); +isGetter(['getter']); +isGetter(''); +isGetter(5); + +toJS({}); +toJS([]); +toJS(''); +toJS(5); + +toImmutable({}); +toImmutable([]); +toImmutable(''); +toImmutable(5); + +isImmutable({}); +isImmutable([]); +isImmutable(''); +isImmutable(5); + +createReactMixin(r1); +createReactMixin(r2); + +// Test default export. +import Nuclear = require('nuclear-js'); +Nuclear.Immutable.Map({ a: 1 }); +Nuclear.Reactor({ debug: true }); +Nuclear.Store({ getInitialState() {}, initialize() {} }); +Nuclear.isKeyPath({}); +Nuclear.isGetter({}); +Nuclear.toJS({}); +Nuclear.toImmutable({}); +Nuclear.isImmutable({}); +Nuclear.createReactMixin(r1); diff --git a/types/nuclear-js/package.json b/types/nuclear-js/package.json new file mode 100644 index 0000000000..aee6417ac1 --- /dev/null +++ b/types/nuclear-js/package.json @@ -0,0 +1,6 @@ +{ + "private": true, + "dependencies": { + "immutable": "^3.7.3" + } +} diff --git a/types/nuclear-js/tsconfig.json b/types/nuclear-js/tsconfig.json new file mode 100644 index 0000000000..c83c270aaa --- /dev/null +++ b/types/nuclear-js/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "module": "commonjs", + "lib": [ + "es6" + ], + "esModuleInterop": true, + "noImplicitAny": true, + "noImplicitThis": true, + "strictFunctionTypes": true, + "strictNullChecks": true, + "baseUrl": "../", + "typeRoots": [ + "../" + ], + "types": [], + "noEmit": true, + "forceConsistentCasingInFileNames": true + }, + "files": [ + "index.d.ts", + "nuclear-js-tests.ts" + ] +} diff --git a/types/nuclear-js/tslint.json b/types/nuclear-js/tslint.json new file mode 100644 index 0000000000..593f1d33c8 --- /dev/null +++ b/types/nuclear-js/tslint.json @@ -0,0 +1,3 @@ +{ + "extends": "dtslint/dt.json" + }