move app-bridge code and tests to app-sdk
This commit is contained in:
parent
a59519aede
commit
8110818251
14 changed files with 3322 additions and 126 deletions
|
@ -57,7 +57,7 @@
|
||||||
"import/no-cycle": "off", // pathpidia issue
|
"import/no-cycle": "off", // pathpidia issue
|
||||||
"import/prefer-default-export": "off",
|
"import/prefer-default-export": "off",
|
||||||
"@typescript-eslint/no-misused-promises": ["error"],
|
"@typescript-eslint/no-misused-promises": ["error"],
|
||||||
"@typescript-eslint/no-floating-promises": ["error"]
|
"@typescript-eslint/no-floating-promises": ["error"],
|
||||||
},
|
},
|
||||||
"settings": {
|
"settings": {
|
||||||
"import/parsers": {
|
"import/parsers": {
|
||||||
|
|
20
package.json
20
package.json
|
@ -10,7 +10,8 @@
|
||||||
"build": "tsup-node src/* --format esm,cjs --dts && clear-package-json package.json -o dist/package.json --fields publishConfig",
|
"build": "tsup-node src/* --format esm,cjs --dts && clear-package-json package.json -o dist/package.json --fields publishConfig",
|
||||||
"test": "vitest",
|
"test": "vitest",
|
||||||
"prepare": "husky install",
|
"prepare": "husky install",
|
||||||
"lint": "prettier --loglevel warn --write . && eslint --fix ."
|
"lint": "prettier --loglevel warn --write . && eslint --fix .",
|
||||||
|
"release": "np"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": "",
|
||||||
|
@ -19,7 +20,8 @@
|
||||||
"fast-glob": "^3.2.11",
|
"fast-glob": "^3.2.11",
|
||||||
"graphql": "^16.5.0",
|
"graphql": "^16.5.0",
|
||||||
"jose": "^4.8.3",
|
"jose": "^4.8.3",
|
||||||
"retes": "^0.32.0"
|
"retes": "^0.32.0",
|
||||||
|
"uuid": "^8.3.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^18.6.5",
|
"@types/node": "^18.6.5",
|
||||||
|
@ -40,7 +42,14 @@
|
||||||
"tsm": "^2.2.2",
|
"tsm": "^2.2.2",
|
||||||
"tsup": "^6.2.1",
|
"tsup": "^6.2.1",
|
||||||
"typescript": "^4.7.4",
|
"typescript": "^4.7.4",
|
||||||
"vitest": "^0.21.1"
|
"vitest": "^0.21.1",
|
||||||
|
"watchlist": "^0.3.1",
|
||||||
|
"np": "^7.6.1",
|
||||||
|
"@types/uuid": "^8.3.4",
|
||||||
|
"@testing-library/dom": "^8.17.1",
|
||||||
|
"@vitejs/plugin-react": "^2.0.0",
|
||||||
|
"jsdom": "^20.0.0",
|
||||||
|
"vite": "^3.0.5"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
"*.{js,ts,tsx}": "eslint --cache --fix",
|
"*.{js,ts,tsx}": "eslint --cache --fix",
|
||||||
|
@ -63,6 +72,11 @@
|
||||||
"import": "./urls.mjs",
|
"import": "./urls.mjs",
|
||||||
"require": "./urls.js"
|
"require": "./urls.js"
|
||||||
},
|
},
|
||||||
|
"./app-bridge": {
|
||||||
|
"types": "./app-bridge/index.d.ts",
|
||||||
|
"import": "./app-bridge/index.mjs",
|
||||||
|
"require": "./app-bridge/index.js"
|
||||||
|
},
|
||||||
".": {
|
".": {
|
||||||
"types": "./index.d.ts",
|
"types": "./index.d.ts",
|
||||||
"import": "./index.mjs",
|
"import": "./index.mjs",
|
||||||
|
|
2894
pnpm-lock.yaml
2894
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
80
src/app-bridge/actions.ts
Normal file
80
src/app-bridge/actions.ts
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
|
||||||
|
import { Values } from "./helpers";
|
||||||
|
|
||||||
|
// Using constants over Enums, more info: https://fettblog.eu/tidy-typescript-avoid-enums/
|
||||||
|
export const ActionType = {
|
||||||
|
redirect: "redirect",
|
||||||
|
notification: "notification",
|
||||||
|
} as const;
|
||||||
|
export type ActionType = Values<typeof ActionType>;
|
||||||
|
|
||||||
|
type Action<Name extends ActionType, Payload extends {}> = {
|
||||||
|
payload: Payload;
|
||||||
|
type: Name;
|
||||||
|
};
|
||||||
|
|
||||||
|
type ActionWithId<Name extends ActionType, Payload extends {}> = {
|
||||||
|
payload: Payload & { actionId: string };
|
||||||
|
type: Name;
|
||||||
|
};
|
||||||
|
|
||||||
|
function withActionId<Name extends ActionType, Payload extends {}, T extends Action<Name, Payload>>(
|
||||||
|
action: T
|
||||||
|
): ActionWithId<Name, Payload> {
|
||||||
|
const actionId = uuidv4();
|
||||||
|
|
||||||
|
return {
|
||||||
|
...action,
|
||||||
|
payload: {
|
||||||
|
...action.payload,
|
||||||
|
actionId,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RedirectPayload = {
|
||||||
|
/**
|
||||||
|
* Relative (inside Dashboard) or absolute URL path.
|
||||||
|
*/
|
||||||
|
to: string;
|
||||||
|
newContext?: boolean;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* Redirects Dashboard user.
|
||||||
|
*/
|
||||||
|
export type RedirectAction = ActionWithId<"redirect", RedirectPayload>;
|
||||||
|
function Redirect(payload: RedirectPayload): RedirectAction {
|
||||||
|
return withActionId({
|
||||||
|
payload,
|
||||||
|
type: "redirect",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NotificationPayload = {
|
||||||
|
/**
|
||||||
|
* Matching Dashboard's notification object.
|
||||||
|
*/
|
||||||
|
status?: "info" | "success" | "warning" | "error";
|
||||||
|
title?: string;
|
||||||
|
text?: string;
|
||||||
|
apiMessage?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NotificationAction = ActionWithId<"notification", NotificationPayload>;
|
||||||
|
/**
|
||||||
|
* Shows a notification using Dashboard's notification system.
|
||||||
|
*/
|
||||||
|
function Notification(payload: NotificationPayload): NotificationAction {
|
||||||
|
return withActionId({
|
||||||
|
type: "notification",
|
||||||
|
payload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Actions = RedirectAction | NotificationAction;
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
Redirect,
|
||||||
|
Notification,
|
||||||
|
};
|
165
src/app-bridge/app.ts
Normal file
165
src/app-bridge/app.ts
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
import { SSR } from "./constants";
|
||||||
|
import { Events, EventType, PayloadOfEvent, ThemeType } from "./events";
|
||||||
|
|
||||||
|
export type AppBridgeState = {
|
||||||
|
token?: string;
|
||||||
|
id: string;
|
||||||
|
ready: boolean;
|
||||||
|
domain: string;
|
||||||
|
path: string;
|
||||||
|
theme: ThemeType;
|
||||||
|
};
|
||||||
|
type EventCallback<TPayload extends {} = {}> = (data: TPayload) => void;
|
||||||
|
type SubscribeMap = {
|
||||||
|
[type in EventType]: Record<any, EventCallback<PayloadOfEvent<type>>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function reducer(state: AppBridgeState, event: Events) {
|
||||||
|
switch (event.type) {
|
||||||
|
case EventType.handshake: {
|
||||||
|
const newState = {
|
||||||
|
...state,
|
||||||
|
ready: true,
|
||||||
|
token: event.payload.token,
|
||||||
|
};
|
||||||
|
|
||||||
|
return newState;
|
||||||
|
}
|
||||||
|
case EventType.redirect: {
|
||||||
|
const newState = {
|
||||||
|
...state,
|
||||||
|
path: event.payload.path,
|
||||||
|
};
|
||||||
|
|
||||||
|
return newState;
|
||||||
|
}
|
||||||
|
case EventType.theme: {
|
||||||
|
const newState = {
|
||||||
|
...state,
|
||||||
|
theme: event.payload.theme,
|
||||||
|
};
|
||||||
|
|
||||||
|
return newState;
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const app = (() => {
|
||||||
|
if (SSR) {
|
||||||
|
console.warn(
|
||||||
|
"@saleor/app-bridge detected you're running this app in SSR mode. Make sure to call `createApp` when window object exists."
|
||||||
|
);
|
||||||
|
return null as never;
|
||||||
|
}
|
||||||
|
|
||||||
|
let state: AppBridgeState = {
|
||||||
|
id: "",
|
||||||
|
domain: "",
|
||||||
|
ready: false,
|
||||||
|
path: "/",
|
||||||
|
theme: "light",
|
||||||
|
};
|
||||||
|
let subscribeMap: SubscribeMap = {
|
||||||
|
handshake: {},
|
||||||
|
response: {},
|
||||||
|
redirect: {},
|
||||||
|
theme: {},
|
||||||
|
};
|
||||||
|
let refererOrigin: string;
|
||||||
|
|
||||||
|
try {
|
||||||
|
refererOrigin = new URL(document.referrer).origin;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("document.referrer is empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener(
|
||||||
|
"message",
|
||||||
|
// Generic MessageEvent is not supported by tsdx's TS version
|
||||||
|
({ origin, data }: Omit<MessageEvent, "data"> & { data: Events }) => {
|
||||||
|
// check if event origin matches the document referer
|
||||||
|
if (origin !== refererOrigin) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// compute new state
|
||||||
|
state = reducer(state, data);
|
||||||
|
|
||||||
|
// run callbacks
|
||||||
|
const { type, payload } = data;
|
||||||
|
// We know this is valid object, su supress
|
||||||
|
// eslint-disable-next-line no-prototype-builtins
|
||||||
|
if (EventType.hasOwnProperty(type)) {
|
||||||
|
Object.getOwnPropertySymbols(subscribeMap[type]).forEach((key) =>
|
||||||
|
// @ts-ignore
|
||||||
|
subscribeMap[type][key](payload)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscribes to an Event.
|
||||||
|
*
|
||||||
|
* @param eventType - Event type.
|
||||||
|
* @param cb - Callback that executes when Event is registered. Called with Event payload object.
|
||||||
|
* @returns Unsubscribe function. Call to unregister the callback.
|
||||||
|
*/
|
||||||
|
function subscribe<TEventType extends EventType, TPayload extends PayloadOfEvent<TEventType>>(
|
||||||
|
eventType: TEventType,
|
||||||
|
cb: EventCallback<TPayload>
|
||||||
|
) {
|
||||||
|
const key = Symbol("Callback token") as unknown as string; // https://github.com/Microsoft/TypeScript/issues/24587
|
||||||
|
// @ts-ignore
|
||||||
|
subscribeMap[eventType][key] = cb;
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
delete subscribeMap[eventType][key];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe to all Events of type.
|
||||||
|
*
|
||||||
|
* @param eventType - (optional) Event type. If empty, all callbacks will be unsubscribed.
|
||||||
|
*/
|
||||||
|
function unsubscribeAll(eventType?: EventType) {
|
||||||
|
if (eventType) {
|
||||||
|
subscribeMap[eventType] = {};
|
||||||
|
} else {
|
||||||
|
subscribeMap = {
|
||||||
|
handshake: {},
|
||||||
|
response: {},
|
||||||
|
redirect: {},
|
||||||
|
theme: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets current state.
|
||||||
|
*
|
||||||
|
* @returns State object.
|
||||||
|
*/
|
||||||
|
function getState() {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setState(newState: Partial<AppBridgeState>) {
|
||||||
|
state = {
|
||||||
|
...state,
|
||||||
|
...newState,
|
||||||
|
};
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
subscribe,
|
||||||
|
unsubscribeAll,
|
||||||
|
getState,
|
||||||
|
setState,
|
||||||
|
};
|
||||||
|
})();
|
1
src/app-bridge/constants.ts
Normal file
1
src/app-bridge/constants.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export const SSR = typeof window === "undefined";
|
55
src/app-bridge/events.ts
Normal file
55
src/app-bridge/events.ts
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
import { Values } from "./helpers";
|
||||||
|
|
||||||
|
export type Version = 1;
|
||||||
|
|
||||||
|
export const EventType = {
|
||||||
|
handshake: "handshake",
|
||||||
|
response: "response",
|
||||||
|
redirect: "redirect",
|
||||||
|
theme: "theme",
|
||||||
|
} as const;
|
||||||
|
export type EventType = Values<typeof EventType>;
|
||||||
|
|
||||||
|
type Event<Name extends EventType, Payload extends {}> = {
|
||||||
|
payload: Payload;
|
||||||
|
type: Name;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type HandshakeEvent = Event<
|
||||||
|
"handshake",
|
||||||
|
{
|
||||||
|
token: string;
|
||||||
|
version: Version;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type DispatchResponseEvent = Event<
|
||||||
|
"response",
|
||||||
|
{
|
||||||
|
actionId: string;
|
||||||
|
ok: boolean;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type RedirectEvent = Event<
|
||||||
|
"redirect",
|
||||||
|
{
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type ThemeType = "light" | "dark";
|
||||||
|
export type ThemeEvent = Event<
|
||||||
|
"theme",
|
||||||
|
{
|
||||||
|
theme: ThemeType;
|
||||||
|
}
|
||||||
|
>;
|
||||||
|
|
||||||
|
export type Events = HandshakeEvent | DispatchResponseEvent | RedirectEvent | ThemeEvent;
|
||||||
|
|
||||||
|
export type PayloadOfEvent<
|
||||||
|
TEventType extends EventType,
|
||||||
|
TEvent extends Events = Events
|
||||||
|
// @ts-ignore TODO - why this is not working with this tsconfig? Fixme
|
||||||
|
> = TEvent extends Event<TEventType, any> ? TEvent["payload"] : never;
|
1
src/app-bridge/helpers.ts
Normal file
1
src/app-bridge/helpers.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export type Values<T> = T[keyof T];
|
132
src/app-bridge/index.test.ts
Normal file
132
src/app-bridge/index.test.ts
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
import { fireEvent } from "@testing-library/dom";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
// mock document.referrer
|
||||||
|
const origin = "http://example.com";
|
||||||
|
Object.defineProperty(window.document, "referrer", {
|
||||||
|
value: origin,
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.defineProperty(window, "location", {
|
||||||
|
value: {
|
||||||
|
href: `${origin}?domain=saleor.domain&id=appid`,
|
||||||
|
},
|
||||||
|
writable: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line
|
||||||
|
import { actions, DispatchResponseEvent, createApp } from ".";
|
||||||
|
|
||||||
|
describe("createApp", () => {
|
||||||
|
const domain = "saleor.domain.host";
|
||||||
|
const app = createApp(domain);
|
||||||
|
|
||||||
|
it("correctly sets the domain", () => {
|
||||||
|
expect(app.getState().domain).toEqual(domain);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("authenticates", () => {
|
||||||
|
expect(app.getState().ready).toBe(false);
|
||||||
|
|
||||||
|
const token = "test-token";
|
||||||
|
fireEvent(
|
||||||
|
window,
|
||||||
|
new MessageEvent("message", {
|
||||||
|
data: { type: "handshake", payload: { token } },
|
||||||
|
origin,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(app.getState().ready).toBe(true);
|
||||||
|
expect(app.getState().token).toEqual(token);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("subscribes to an event and returns unsubcribe function", () => {
|
||||||
|
// subscribe
|
||||||
|
const callback = vi.fn();
|
||||||
|
const unsubscribe = app.subscribe("handshake", callback);
|
||||||
|
|
||||||
|
expect(callback).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
const token = "fresh-token";
|
||||||
|
// correct event
|
||||||
|
const payload = {
|
||||||
|
token,
|
||||||
|
version: 1,
|
||||||
|
};
|
||||||
|
fireEvent(
|
||||||
|
window,
|
||||||
|
new MessageEvent("message", {
|
||||||
|
data: { type: "handshake", payload },
|
||||||
|
origin,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// incorrect event type
|
||||||
|
fireEvent(
|
||||||
|
window,
|
||||||
|
new MessageEvent("message", {
|
||||||
|
data: { type: "invalid", payload: { token: "invalid" } },
|
||||||
|
origin,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// incorrect origin
|
||||||
|
fireEvent(
|
||||||
|
window,
|
||||||
|
new MessageEvent("message", {
|
||||||
|
data: { type: "handshake", payload: { token } },
|
||||||
|
origin: "http://wrong.origin.com",
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledTimes(1);
|
||||||
|
expect(callback).toHaveBeenCalledWith(payload);
|
||||||
|
expect(app.getState().token).toEqual(token);
|
||||||
|
expect(app.getState().id).toEqual("appid");
|
||||||
|
|
||||||
|
// unsubscribe
|
||||||
|
unsubscribe();
|
||||||
|
|
||||||
|
fireEvent(
|
||||||
|
window,
|
||||||
|
new MessageEvent("message", {
|
||||||
|
data: { type: "handshake", payload: { token: "123" } },
|
||||||
|
origin,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(callback).toHaveBeenCalledTimes(1);
|
||||||
|
expect(app.getState().token).toEqual("123");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("persists domain", () => {
|
||||||
|
expect(app.getState().domain).toEqual(domain);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("dispatches valid action", () => {
|
||||||
|
const target = "/test";
|
||||||
|
const action = actions.Redirect({ to: target });
|
||||||
|
|
||||||
|
window.addEventListener("message", (event) => {
|
||||||
|
if (event.data.type === action.type) {
|
||||||
|
fireEvent(
|
||||||
|
window,
|
||||||
|
new MessageEvent("message", {
|
||||||
|
data: {
|
||||||
|
type: "response",
|
||||||
|
payload: { ok: true, actionId: action.payload.actionId },
|
||||||
|
} as DispatchResponseEvent,
|
||||||
|
origin,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return expect(app.dispatch(action)).resolves.toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("times out after action response has not been registered", () =>
|
||||||
|
expect(app.dispatch(actions.Redirect({ to: "/test" }))).rejects.toBeInstanceOf(Error));
|
||||||
|
});
|
78
src/app-bridge/index.ts
Normal file
78
src/app-bridge/index.ts
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
import { Actions } from "./actions";
|
||||||
|
import { app } from "./app";
|
||||||
|
import { EventType, ThemeType } from "./events";
|
||||||
|
|
||||||
|
export function createApp(targetDomain?: string) {
|
||||||
|
let domain: string;
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
const id = url.searchParams.get("id") || "";
|
||||||
|
const path = window.location.pathname || "";
|
||||||
|
const theme: ThemeType = url.searchParams.get("theme") === "light" ? "light" : "dark";
|
||||||
|
|
||||||
|
if (targetDomain) {
|
||||||
|
domain = targetDomain;
|
||||||
|
} else {
|
||||||
|
domain = url.searchParams.get("domain") || "";
|
||||||
|
}
|
||||||
|
|
||||||
|
app.setState({ domain, id, path, theme });
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches Action to Saleor Dashboard.
|
||||||
|
*
|
||||||
|
* @param action - Action containing type and payload.
|
||||||
|
* @returns Promise resolved when Action is successfully completed.
|
||||||
|
*/
|
||||||
|
async function dispatch<T extends Actions>(action: T) {
|
||||||
|
return new Promise<void>((resolve, reject) => {
|
||||||
|
if (window.parent) {
|
||||||
|
window.parent.postMessage(
|
||||||
|
{
|
||||||
|
type: action.type,
|
||||||
|
payload: action.payload,
|
||||||
|
},
|
||||||
|
"*"
|
||||||
|
);
|
||||||
|
|
||||||
|
let intervalId: NodeJS.Timer;
|
||||||
|
|
||||||
|
const unsubscribe = app.subscribe(EventType.response, ({ actionId, ok }) => {
|
||||||
|
if (action.payload.actionId === actionId) {
|
||||||
|
unsubscribe();
|
||||||
|
clearInterval(intervalId);
|
||||||
|
|
||||||
|
if (ok) {
|
||||||
|
resolve();
|
||||||
|
} else {
|
||||||
|
reject(
|
||||||
|
new Error(
|
||||||
|
"Error: Action responded with negative status. This indicates the action method was not used properly."
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// If dashboard doesn't respond within 1 second, reject and unsubscribe
|
||||||
|
intervalId = setInterval(() => {
|
||||||
|
unsubscribe();
|
||||||
|
reject(new Error("Error: Action response timed out."));
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
reject(new Error("Error: Parent window does not exist."));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
dispatch,
|
||||||
|
subscribe: app.subscribe,
|
||||||
|
unsubscribeAll: app.unsubscribeAll,
|
||||||
|
getState: app.getState,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export * from "./actions";
|
||||||
|
export * from "./events";
|
||||||
|
export * from "./types";
|
||||||
|
export default createApp;
|
5
src/app-bridge/types.ts
Normal file
5
src/app-bridge/types.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { createApp } from ".";
|
||||||
|
import { AppBridgeState } from "./app";
|
||||||
|
|
||||||
|
export type App = ReturnType<typeof createApp>;
|
||||||
|
export { AppBridgeState };
|
0
src/setup-tests.ts
Normal file
0
src/setup-tests.ts
Normal file
|
@ -13,9 +13,10 @@
|
||||||
/* Language and Environment */
|
/* Language and Environment */
|
||||||
"target": "ES2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
"target": "ES2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
|
||||||
"lib": [
|
"lib": [
|
||||||
|
"dom",
|
||||||
"ES2021"
|
"ES2021"
|
||||||
] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
|
] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
|
||||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
"jsx": "react" /* Specify what JSX code is generated. */,
|
||||||
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
|
||||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||||
|
|
12
vitest.config.ts
Normal file
12
vitest.config.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import react from "@vitejs/plugin-react";
|
||||||
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
|
// https://vitejs.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
test: {
|
||||||
|
environment: "jsdom",
|
||||||
|
setupFiles: "./src/setup-tests.ts",
|
||||||
|
css: false,
|
||||||
|
},
|
||||||
|
});
|
Loading…
Reference in a new issue