Skip to content

Commit

Permalink
Merge pull request #185 from tshaddix/webext-support
Browse files Browse the repository at this point in the history
Webext support
  • Loading branch information
tshaddix authored Feb 22, 2019
2 parents 32271d2 + d27cf80 commit 8e429ac
Show file tree
Hide file tree
Showing 9 changed files with 84 additions and 41 deletions.
15 changes: 3 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,7 @@ import {Store} from 'react-chrome-redux';

import App from './components/app/App';

const store = new Store({
portName: 'MY_APP' // communication port name
});
const store = new Store();

// wait for the store to connect to the background page
store.ready().then(() => {
Expand All @@ -67,7 +65,7 @@ import {wrapStore} from 'react-chrome-redux';

const store; // a normal Redux store

wrapStore(store, {portName: 'MY_APP'}); // make sure portName matches
wrapStore(store);
```

That's it! The dispatches called from UI component will find their way to the background page no problem. The new state from your background page will make sure to find its way back to the UI components.
Expand All @@ -86,9 +84,7 @@ import {Store, applyMiddleware} from 'react-chrome-redux';
import thunkMiddleware from 'redux-thunk';

// Proxy store
const store = new Store({
portName: 'MY_APP'
});
const store = new Store();

// Apply middleware to proxy store
const middleware = [thunkMiddleware];
Expand Down Expand Up @@ -234,7 +230,6 @@ import {wrapStore} from 'react-chrome-redux';
const store; // a normal Redux store

wrapStore(store, {
portName: 'MY_APP',
serializer: payload => JSON.stringify(payload, dateReplacer),
deserializer: payload => JSON.parse(payload, dateReviver)
});
Expand All @@ -246,7 +241,6 @@ wrapStore(store, {
import {Store} from 'react-chrome-redux';

const store = new Store({
portName: 'MY_APP',
serializer: payload => JSON.stringify(payload, dateReplacer),
deserializer: payload => JSON.parse(payload, dateReviver)
});
Expand Down Expand Up @@ -306,7 +300,6 @@ import deepDiff from 'react-chrome-redux/strategies/deepDiff/diff';
const store; // a normal Redux store

wrapStore(store, {
portName: 'MY_APP',
diffStrategy: deepDiff
});
```
Expand All @@ -318,7 +311,6 @@ import {Store} from 'react-chrome-redux';
import patchDeepDiff from 'react-chrome-redux/strategies/deepDiff/patch';

const store = new Store({
portName: 'MY_APP',
patchStrategy: patchDeepDiff
});
```
Expand Down Expand Up @@ -350,7 +342,6 @@ const shouldContinue = (oldState, newState, context) => {
const customDeepDiff = makeDiff(shouldContinue);

wrapStore(store, {
portName: 'MY_APP',
diffStrategy: customDeepDiff // Use the custom deep diff
});
```
Expand Down
4 changes: 2 additions & 2 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export class Store<S = any, A extends redux.Action = redux.Action> {
* @param options An object of form {portName, state, extensionId}, where `portName` is a required string and defines the name of the port for state transition changes, `state` is the initial state of this store (default `{}`) `extensionId` is the extension id as defined by chrome when extension is loaded (default `''`)
*/
constructor(options: {
portName: string,
portName?: string,
state?: any,
extensionId?: string,
serializer?: Function,
Expand Down Expand Up @@ -76,7 +76,7 @@ export class Store<S = any, A extends redux.Action = redux.Action> {
export function wrapStore<S>(
store: redux.Store<S>,
configuration: {
portName: string,
portName?: string,
dispatchResponder?(dispatchResult: any, send: (response: any) => void): void,
serializer?: Function,
deserializer?: Function,
Expand Down
6 changes: 5 additions & 1 deletion src/constants/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,8 @@ export const STATE_TYPE = 'chromex.state';

// Message type for state patch events from
// background to Proxy Stores
export const PATCH_STATE_TYPE = 'chromex.patch_state';
export const PATCH_STATE_TYPE = 'chromex.patch_state';

// The default name for the port communication via
// react-chrome-redux
export const DEFAULT_PORT_NAME = "chromex.port_name";
18 changes: 10 additions & 8 deletions src/store/Store.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,12 @@ import assignIn from 'lodash.assignin';
import {
DISPATCH_TYPE,
STATE_TYPE,
PATCH_STATE_TYPE
PATCH_STATE_TYPE,
DEFAULT_PORT_NAME
} from '../constants';
import { withSerializer, withDeserializer, noop } from "../serialization";

import shallowDiff from '../strategies/shallowDiff/patch';
import {getBrowserAPI} from '../util';

const backgroundErrPrefix = '\nLooks like there is an error in the background page. ' +
'You might want to inspect your background page for more details.\n';
Expand All @@ -16,9 +17,9 @@ const backgroundErrPrefix = '\nLooks like there is an error in the background pa
class Store {
/**
* Creates a new Proxy store
* @param {object} options An object of form {portName, state, extensionId, serializer, deserializer, diffStrategy}, where `portName` is a required string and defines the name of the port for state transition changes, `state` is the initial state of this store (default `{}`) `extensionId` is the extension id as defined by chrome when extension is loaded (default `''`), `serializer` is a function to serialize outgoing message payloads (default is passthrough), `deserializer` is a function to deserialize incoming message payloads (default is passthrough), and patchStrategy is one of the included patching strategies (default is shallow diff) or a custom patching function.
* @param {object} options An object of form {portName, state, extensionId, serializer, deserializer, diffStrategy}, where `portName` is a required string and defines the name of the port for state transition changes, `state` is the initial state of this store (default `{}`) `extensionId` is the extension id as defined by browserAPI when extension is loaded (default `''`), `serializer` is a function to serialize outgoing message payloads (default is passthrough), `deserializer` is a function to deserialize incoming message payloads (default is passthrough), and patchStrategy is one of the included patching strategies (default is shallow diff) or a custom patching function.
*/
constructor({portName, state = {}, extensionId = null, serializer = noop, deserializer = noop, patchStrategy = shallowDiff}) {
constructor({portName = DEFAULT_PORT_NAME, state = {}, extensionId = null, serializer = noop, deserializer = noop, patchStrategy = shallowDiff}) {
if (!portName) {
throw new Error('portName is required in options');
}
Expand All @@ -36,12 +37,13 @@ class Store {
this.readyResolved = false;
this.readyPromise = new Promise(resolve => this.readyResolve = resolve);

this.browserAPI = getBrowserAPI();
this.extensionId = extensionId; // keep the extensionId as an instance variable
this.port = chrome.runtime.connect(this.extensionId, {name: portName});
this.port = this.browserAPI.runtime.connect(this.extensionId, {name: portName});
this.safetyHandler = this.safetyHandler.bind(this);
this.safetyMessage = chrome.runtime.onMessage.addListener(this.safetyHandler);
this.safetyMessage = this.browserAPI.runtime.onMessage.addListener(this.safetyHandler);
this.serializedPortListener = withDeserializer(deserializer)((...args) => this.port.onMessage.addListener(...args));
this.serializedMessageSender = withSerializer(serializer)((...args) => chrome.runtime.sendMessage(...args), 1);
this.serializedMessageSender = withSerializer(serializer)((...args) => this.browserAPI.runtime.sendMessage(...args), 1);
this.listeners = [];
this.state = state;
this.patchStrategy = patchStrategy;
Expand Down Expand Up @@ -161,7 +163,7 @@ class Store {
if (message.action === 'storeReady'){

// Remove Saftey Listener
chrome.runtime.onMessage.removeListener(this.safetyHandler);
this.browserAPI.runtime.onMessage.removeListener(this.safetyHandler);

// Resolve if readyPromise has not been resolved.
if(!this.readyResolved) {
Expand Down
14 changes: 14 additions & 0 deletions src/util.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* Looks for a global browser api, first checking the chrome namespace and then
* checking the browser namespace. If no appropriate namespace is present, this
* function will throw an error.
*/
export function getBrowserAPI() {
const api = global.chrome || global.browser;

if (!api) {
throw new Error("Browser API is not present");
}

return api;
}
27 changes: 15 additions & 12 deletions src/wrap-store/wrapStore.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import {
DISPATCH_TYPE,
STATE_TYPE,
PATCH_STATE_TYPE
PATCH_STATE_TYPE,
DEFAULT_PORT_NAME
} from '../constants';
import { withSerializer, withDeserializer, noop } from "../serialization";

import {getBrowserAPI} from '../util';
import shallowDiff from '../strategies/shallowDiff/diff';

/**
Expand Down Expand Up @@ -37,7 +38,7 @@ const promiseResponder = (dispatchResult, send) => {
* @param {Object} options An object of form {portName, dispatchResponder, serializer, deserializer}, where `portName` is a required string and defines the name of the port for state transition changes, `dispatchResponder` is a function that takes the result of a store dispatch and optionally implements custom logic for responding to the original dispatch message,`serializer` is a function to serialize outgoing message payloads (default is passthrough), `deserializer` is a function to deserialize incoming message payloads (default is passthrough), and diffStrategy is one of the included diffing strategies (default is shallow diff) or a custom diffing function.
*/
export default (store, {
portName,
portName = DEFAULT_PORT_NAME,
dispatchResponder,
serializer = noop,
deserializer = noop,
Expand All @@ -61,6 +62,8 @@ export default (store, {
dispatchResponder = promiseResponder;
}

const browserAPI = getBrowserAPI();

/**
* Respond to dispatches from UI components
*/
Expand Down Expand Up @@ -129,42 +132,42 @@ export default (store, {
/**
* Setup action handler
*/
withPayloadDeserializer((...args) => chrome.runtime.onMessage.addListener(...args))(dispatchResponse, shouldDeserialize);
withPayloadDeserializer((...args) => browserAPI.runtime.onMessage.addListener(...args))(dispatchResponse, shouldDeserialize);

/**
* Setup external action handler
*/
if (chrome.runtime.onMessageExternal) {
withPayloadDeserializer((...args) => chrome.runtime.onMessageExternal.addListener(...args))(dispatchResponse, shouldDeserialize);
if (browserAPI.runtime.onMessageExternal) {
withPayloadDeserializer((...args) => browserAPI.runtime.onMessageExternal.addListener(...args))(dispatchResponse, shouldDeserialize);
} else {
console.warn('runtime.onMessageExternal is not supported');
}

/**
* Setup extended connection
*/
chrome.runtime.onConnect.addListener(connectState);
browserAPI.runtime.onConnect.addListener(connectState);

/**
* Setup extended external connection
*/
if (chrome.runtime.onConnectExternal) {
chrome.runtime.onConnectExternal.addListener(connectState);
if (browserAPI.runtime.onConnectExternal) {
browserAPI.runtime.onConnectExternal.addListener(connectState);
} else {
console.warn('runtime.onConnectExternal is not supported');
}

/**
* Safety message to tabs for content scripts
*/
chrome.tabs.query({}, tabs => {
browserAPI.tabs.query({}, tabs => {
for(const tab of tabs){
chrome.tabs.sendMessage(tab.id, {action: 'storeReady'});
browserAPI.tabs.sendMessage(tab.id, {action: 'storeReady'});
}
});

// For non-tab based
// TODO: Find use case for this. Ommiting until then.
// chrome.runtime.sendMessage(null, {action: 'storeReady'});
// browserAPI.runtime.sendMessage(null, {action: 'storeReady'});

};
File renamed without changes.
35 changes: 35 additions & 0 deletions test/util.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@

import should from 'should';

import {getBrowserAPI} from "../src/util";

describe('#getBrowserAPI()', function () {
it('should return the global chrome API if present', function () {
global.chrome = {
isChrome: true
};
global.browser = undefined;

const browserAPI = getBrowserAPI();

should(browserAPI).equals(global.chrome);
});

it('should return the global browser API if chrome is not present', function () {
global.chrome = undefined;
global.browser = {
isBrowser: true
};

const browserAPI = getBrowserAPI();

should(browserAPI).equals(global.browser);
});

it('should throw an error if neither the chrome or browser API is present', function () {
global.chrome = undefined;
global.browser = undefined;

should.throws(() => getBrowserAPI());
});
});
6 changes: 0 additions & 6 deletions test/wrapStore.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -248,12 +248,6 @@ describe('wrapStore', function () {
dispatch: sinon.spy(),
};

it('should throw an error if portName is not present', function () {
should.throws(() => {
wrapStore(store, {});
}, Error);
});

it('should throw an error if serializer is not a function', function () {
should.throws(() => {
wrapStore(store, { portName, serializer: "abc" });
Expand Down

0 comments on commit 8e429ac

Please sign in to comment.