Skip to content

Commit

Permalink
chore(wip): polyfill symbol observable and typing
Browse files Browse the repository at this point in the history
  • Loading branch information
iamogbz committed Sep 17, 2020
1 parent e5552d2 commit df9bf9c
Show file tree
Hide file tree
Showing 22 changed files with 2,731 additions and 1,424 deletions.
5 changes: 2 additions & 3 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,8 @@
"no-console": 1,
"no-param-reassign": ["error", { "props": false }],
"no-trailing-spaces": 2,
"no-use-before-define": [2, {
"functions": false
}],
"no-unused-vars":"off",
"no-use-before-define": "off",
"object-curly-newline": [2, {
"multiline": true,
"consistent": true
Expand Down
3,978 changes: 2,620 additions & 1,358 deletions package-lock.json

Large diffs are not rendered by default.

15 changes: 7 additions & 8 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,7 @@
}
},
"peerDependencies": {
"react": "^16.13.0",
"hoist-non-react-statics": "^3.3.0"
"react": "^16.13.0"
},
"devDependencies": {
"@commitlint/cli": "^11.0.0",
Expand All @@ -97,12 +96,12 @@
"@types/node": "^14.10.2",
"@types/react": "^16.9.49",
"@types/react-is": "^16.7.1",
"@typescript-eslint/eslint-plugin": "^2.34.0",
"@typescript-eslint/parser": "^2.34.0",
"@typescript-eslint/eslint-plugin": "^4.1.1",
"@typescript-eslint/parser": "^4.1.1",
"acorn": "^8.0.1",
"commitizen": "^4.2.1",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^6.8.0",
"eslint": "^7.9.0",
"eslint-config-airbnb": "^18.2.0",
"eslint-config-prettier": "^6.11.0",
"eslint-plugin-import": "^2.22.0",
Expand All @@ -112,7 +111,7 @@
"eslint-plugin-react": "^7.20.6",
"eslint-plugin-react-hooks": "^4.1.2",
"husky": "^4.3.0",
"jest": "^25.5.4",
"jest": "^26.4.2",
"lint-staged": "^10.3.0",
"prettier": "^2.1.2",
"prettier-eslint": "^11.0.0",
Expand All @@ -126,8 +125,8 @@
"stylelint-config-standard": "^20.0.0",
"stylelint-config-styled-components": "^0.1.1",
"stylelint-processor-styled-components": "^1.10.0",
"ts-jest": "^25.5.1",
"ts-jest": "^26.3.0",
"ts-node": "^9.0.0",
"typescript": "^3.9.7"
"typescript": "^4.0.2"
}
}
4 changes: 2 additions & 2 deletions src/createConnect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { connect } from "./utils/connect";
export function createConnect<S, I, T extends string, P, K>(
Context?: Context<S, T, P>,
) {
// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
return function connectContext(
mapStateToProps?: MapStateToProps<S, I, K>,
mapDispatchToProps?:
Expand All @@ -14,7 +14,7 @@ export function createConnect<S, I, T extends string, P, K>(
dispatchProps?: ActionDispatcherMapping<T, P>,
ownProps?: I,
) => I & K & ActionDispatcherMapping<T, P>,
options?: ConnectOptions<S, T, P, I, K>,
options?: ConnectOptions<S, T, P>,
) {
return connect(mapStateToProps, mapDispatchToProps, mergeProps, {
...options,
Expand Down
16 changes: 3 additions & 13 deletions src/createContext.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,14 @@
import * as React from "react";
import { SymbolObservable } from "./utils/symbolObservable";
import "./utils/polyfillSymbol";
import { setGlobalContext } from "./components/Context";

function createUnimplemented(objectName?: string): (m: string) => () => never {
const prefix = objectName ? `${objectName}.` : "";
return function createUnimplemented(methodName) {
return function unimplemented(): never {
throw new Error(`Unimplemented method: ${prefix}${methodName}`);
};
};
}

export function createContextWithValue<S, T extends string, P>(
value: Optional<ContextValue<S, T, P>, "dispatch" | "getState">,
value: MustHave<ContextValue<S, T, P>, "reducer" | "state">,
): Context<S, T, P> {
return React.createContext<ContextValue<S, T, P>>({
dispatch: async (a) => a,
getState: () => value.state,
subscribe: () => (): void => undefined,
...value,
});
}
Expand All @@ -28,12 +20,10 @@ export function createContext<S, T extends string, P>(
displayName?: string,
global = false,
): Context<S, T, P> {
const unimplemented = createUnimplemented(`Context(${displayName ?? ""})`);
const Context = createContextWithValue({
enhancer,
reducer: rootReducer,
state: preloadedState,
[SymbolObservable]: unimplemented(SymbolObservable.toString()),
});
if (displayName) {
Context.displayName = displayName;
Expand Down
8 changes: 4 additions & 4 deletions src/createRootDuck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,22 @@ import { combineReducers } from "./utils/combineReducers";
import { combineSelectors } from "./utils/combineSelectors";

export function createRootDuck<
D extends Duck<S, N, T, P, R, Q, U>[],
S = any, // eslint-disable-line @typescript-eslint/no-explicit-any
N extends string = string,
U extends string = string,
V extends string = string,
T extends U | V = U | V,
P = any, // eslint-disable-line @typescript-eslint/no-explicit-any
R = any, // eslint-disable-line @typescript-eslint/no-explicit-any
Q extends string = string
>(...ducks: D): RootDuck<S, N, T, P, R, Q, U> {
Q extends string = string,
D extends Duck<S, N, T, P, R, Q, U>[] = Duck<S, N, T, P, R, Q, U>[]
>(...ducks: D): RootDuck<S, N, U, V, T, P, R, Q> {
const rootDuck = {
actions: {},
initialState: {},
names: new Set(ducks.map((d) => d.name)),
selectors: {},
} as RootDuck<S, N, T, P, R, Q, U>;
} as RootDuck<S, N, U, V, T, P, R, Q>;
const reducerMapping = {} as DuckReducerMapping<S, N, T, P>;
for (const duck of ducks) {
const duckName = duck.name;
Expand Down
2 changes: 1 addition & 1 deletion src/createRootProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ export function createRootProvider<S, T extends string, P>(
) {
return function RootProvider({
children,
}: React.PropsWithChildren<{}>): React.ReactElement {
}: React.PropsWithChildren<unknown>): React.ReactElement {
return <Provider Context={Context}>{children}</Provider>;
};
}
6 changes: 3 additions & 3 deletions src/utils/bindActionCreators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
export function bindActionCreator<T extends string, P>(
actionCreator: Nullable<ActionCreator<T, P>>,
dispatch: ContextDispatch<T, P>,
): Nullable<ActionDispatcher<T, P>> {
): Nullable<ActionDispatcher<P>> {
if (typeof actionCreator !== "function") return;
return function dispatchAction(...args: P[]): void {
dispatch(actionCreator(...args));
Expand All @@ -18,14 +18,14 @@ export function bindActionCreator<T extends string, P>(
export function bindActionCreators<T extends string, P>(
actionCreator: Nullable<ActionCreator<T, P>>,
dispatch: ContextDispatch<T, P>,
): ActionDispatcher<T, P>;
): ActionDispatcher<P>;

export function bindActionCreators<T extends string, P, S>(
actionCreators: Nullable<ActionCreatorMapping<T, P, S>>,
dispatch: ContextDispatch<T, P>,
): ActionDispatcherMapping<T, P>;

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function bindActionCreators(
actionCreators: Nullable<ActionCreator | ActionCreatorMapping>,
dispatch: ContextDispatch,
Expand Down
1 change: 1 addition & 0 deletions src/utils/compose.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/ban-types */
// github.com/reduxjs/redux/blob/f1fc7ce/src/compose.ts
type Func<T extends unknown[], R> = (...a: T) => R;

Expand Down
7 changes: 2 additions & 5 deletions src/utils/connect.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from "react";
import { GlobalContext } from "../components/Context";
import { bindActionCreators } from "./bindActionCreators";
import { isFunction } from "./isFunction";

function defaultMergeProps<T extends string, P, I, K, J>(
stateProps?: K,
Expand All @@ -10,10 +11,6 @@ function defaultMergeProps<T extends string, P, I, K, J>(
return ({ ...ownProps, ...stateProps, ...dispatchProps } as unknown) as J;
}

function isFunction<F>(maybeFunction: F | unknown): maybeFunction is F {
return typeof maybeFunction === "function";
}

function asMapDispatchToPropsFn<S, T extends string, P, I>(
actionCreators?: ActionCreatorMapping<T, P, S>,
): Nullable<MapDispatchToProps<T, P, I>> {
Expand Down Expand Up @@ -44,7 +41,7 @@ export function connect<
dispatchProps?: ActionDispatcherMapping<T, P>,
ownProps?: I,
) => J,
options?: ConnectOptions<S, T, P, I, K, J>,
options?: ConnectOptions<S, T, P>,
): (
// eslint-disable-next-line @typescript-eslint/no-explicit-any
component: ReactComponent<any>,
Expand Down
3 changes: 3 additions & 0 deletions src/utils/isFunction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export function isFunction<F>(maybeFunction: F | unknown): maybeFunction is F {
return typeof maybeFunction === "function";
}
7 changes: 7 additions & 0 deletions src/utils/polyfillSymbol.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
function polyfillSymbol(name: string): void {
if (!Symbol[name]) {
Object.defineProperty(Symbol, name, { value: Symbol(name) });
}
}

polyfillSymbol("observable");
8 changes: 0 additions & 8 deletions src/utils/symbolObservable.ts

This file was deleted.

4 changes: 2 additions & 2 deletions tests/__snapshots__/createContext.test.tsx.snap
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`createContext has unimplemented observable symbol 1`] = `
exports[`createContext has expected default context value 1`] = `
Object {
"dispatch": [Function],
"enhancer": undefined,
"getState": [Function],
"reducer": [Function],
"state": Object {},
Symbol(@@observable): [Function],
"subscribe": [Function],
}
`;
8 changes: 1 addition & 7 deletions tests/createContext.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import * as React from "react";
import { act, render } from "@testing-library/react";
import { renderHook } from "@testing-library/react-hooks";
import { Provider, applyMiddleware, createContext } from "src";
import { SymbolObservable } from "src/utils/symbolObservable";

describe("createContext", () => {
it("creates context without displayname", () => {
Expand Down Expand Up @@ -61,14 +60,9 @@ describe("createContext", () => {
expect(enhancer?.(value)).toMatchObject(value);
});

it("has unimplemented observable symbol", () => {
it("has expected default context value", () => {
const Context = createContext((s) => s, {});
const { result } = renderHook(() => React.useContext(Context));
expect(result.current).toMatchSnapshot();
expect(
((result.current as unknown) as Record<string, unknown>)[
(SymbolObservable as unknown) as string
],
).toThrow("Unimplemented method");
});
});
2 changes: 1 addition & 1 deletion tests/index.mock.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from "src";
import { ActionTypes } from "src/utils/actionTypes";

// eslint-disable-next-line @typescript-eslint/explicit-function-return-type
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
export function createMocks() {
const dummyMiddleware: Middleware<
Record<string, unknown>,
Expand Down
2 changes: 1 addition & 1 deletion tests/index.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ describe("integration", (): void => {
type Props = {
count: number;
isInitialised: boolean;
increment: ActionDispatcher<"counter/increment", never>;
increment: ActionDispatcher<never>;
};
function DumbComponent(props: Props): React.ReactElement {
return (
Expand Down
8 changes: 4 additions & 4 deletions typings/connect.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,10 @@ type MergeProps<
type ConnectOptions<
S = unknown,
T extends string = string,
P = unknown,
I extends Record = Record, // Component own props
K extends Record = Record, // Mapped state props
J extends Record = I & K & ActionDispatcherMapping<T, P> // Merged props i.e. own & state & mapped dispatch props
P = unknown
// I extends Record = Record, // Component own props
// K extends Record = Record, // Mapped state props
// J extends Record = I & K & ActionDispatcherMapping<T, P> // Merged props i.e. own & state & mapped dispatch props
> = {
// areMergedPropsEqual?: (next: J, prev: J) => boolean;
// areOwnPropsEqual?: (next: I, prev: I) => boolean;
Expand Down
2 changes: 1 addition & 1 deletion typings/context.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ type ContextValue<S = unknown, T extends string = string, P = unknown> = {
enhancer?: ContextEnhance<S, T, P>;
reducer: Reducer<S, T, P>;
state: S;
} & MiddlewareAPI<S, T, P>;
} & (MiddlewareAPI<S, T, P> & Observable);

type Context<
S = unknown,
Expand Down
7 changes: 4 additions & 3 deletions typings/duck.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ type ActionCreatorMapping<
C extends string = T /* Action creator mapping keys */
> = Record<C, Nullable<ActionCreator<T, P, S>>>;

type ActionDispatcher<T extends string, P = unknown> = (...args: P[]) => void;
type ActionDispatcher<P = unknown> = (...args: P[]) => void;

type ActionDispatcherMapping<
T extends string = string,
Expand Down Expand Up @@ -80,11 +80,12 @@ type DuckReducerMapping<
type RootDuck<
S = unknown,
N extends string = string /* All possible duck names */,
U extends string = string,
V extends string = string,
T extends U | V = string,
P = unknown,
R = unknown,
Q extends string = string,
U extends string = string
Q extends string = string
> = {
actions: Record<N, ActionCreatorMapping<T, P, S, U>>;
initialState: Record<N, S>;
Expand Down
61 changes: 61 additions & 0 deletions typings/observable.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// github.com/tc39/proposal-observable
interface SymbolConstructor {
readonly observable: symbol;
readonly [key: string]: symbol;
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
interface Observable {
// Subscribes to the sequence with an observer
subscribe: SubscriberFunction;

// Subscribes to the sequence with callbacks
subscribe: SubscriberFunctions;

// Returns itself
[Symbol.observable]?(): Observable;

// Converts items to an Observable
static of?(...items): Observable;

// Converts an observable or iterable to an Observable
static from?(observable): Observable;
}

type SubscriberFunction = (
observer: SubscriptionObserver,
) => UnsubscriberFunction | Subscription;

type SubscriberFunctions = (
onNext: OnNextFunction,
onError?: OnErrorFunction,
onComplete?: OnCompleteFunction,
) => Subscription;

interface Subscription {
// Cancels the subscription
unsubscribe: UnsubscriberFunction;

// A boolean value indicating whether the subscription is closed
closed: boolean;
}

type OnNextFunction = (value) => void;
type OnErrorFunction = (errorValue) => void;
type OnCompleteFunction = () => void;

interface SubscriptionObserver {
// Sends the next value in the sequence
next: OnNextFunction;

// Sends the sequence error
error: OnErrorFunction;

// Sends the completion notification
complete: OnCompleteFunction;

// A boolean value indicating whether the subscription is closed
closed: boolean;
}

type UnsubscriberFunction = () => void;

0 comments on commit df9bf9c

Please sign in to comment.