diff --git a/.babelrc b/.babelrc
index b550b51e..ba6ee236 100644
--- a/.babelrc
+++ b/.babelrc
@@ -1,4 +1,12 @@
{
- "presets": [ "es2015-rollup" ],
- "plugins": ["transform-class-properties"]
+ "presets": [ "es2015" ],
+ "plugins": [
+ "transform-es2015-modules-umd",
+ [
+ "rename-umd-globals",
+ {
+ "postmate": "Postmate"
+ }
+ ]
+ ]
}
diff --git a/Gulpfile.js b/Gulpfile.js
index 049eea7c..ecb21281 100644
--- a/Gulpfile.js
+++ b/Gulpfile.js
@@ -1,18 +1,16 @@
-const babel = require('rollup-plugin-babel');
+const babel = require('gulp-babel');
+const uglify = require('gulp-uglify');
+const rename = require('gulp-rename');
const connect = require('connect');
const eslint = require('gulp-eslint');
const fs = require('fs');
const gulp = require('gulp');
const header = require('gulp-header');
const http = require('http');
-const minify = require('uglify-js').minify;
const mochaPhantomJS = require('gulp-mocha-phantomjs');
const path = require('path');
-const rollup = require('rollup-stream');
const serveStatic = require('serve-static');
-const source = require('vinyl-source-stream');
-const uglify = require('rollup-plugin-uglify');
var parentServer; // eslint-disable-line no-var
var childServer; // eslint-disable-line no-var
@@ -27,23 +25,15 @@ const banner = ['/**',
''].join('\n');
gulp.task('do-build', () =>
- rollup({
- entry: './src/postmate.js',
- format: 'umd',
- moduleName: 'Postmate',
- plugins: [
- babel({
- exclude: 'node_modules/**',
- }),
- uglify({}, minify),
- ],
- })
- .pipe(source('postmate.min.js'))
+ gulp.src('./src/postmate.js')
+ .pipe(babel())
+ .pipe(uglify())
.pipe(header(banner, { pkg }))
+ .pipe(rename('postmate.min.js'))
.pipe(gulp.dest('./build'))
);
-gulp.task('update-readme', () => {
+gulp.task('update-readme', ['do-build'], () => {
const readme = path.join(__dirname, 'README.md');
const data = fs.readFileSync(readme, 'utf-8');
const distSize = fs.statSync(path.join(__dirname, 'build', 'postmate.min.js')).size;
diff --git a/README.md b/README.md
index d03ee1ca..0b44fed2 100644
--- a/README.md
+++ b/README.md
@@ -35,7 +35,7 @@ You can download the compiled javascript directly [here](/build/postmate.min.js)
* Child emits events that the parent can listen to.
* Parent can `call` functions within a `child`
* *Zero* dependencies. Provide your own polyfill or abstraction for the `Promise` API if needed.
-* Lightweight, weighing in at ~ `5.2kb`.
+* Lightweight, weighing in at ~ `4.1kb`.
## Installing
Postmate can be installed via NPM or Bower.
diff --git a/build/postmate.min.js b/build/postmate.min.js
index 04262ecc..82d81998 100644
--- a/build/postmate.min.js
+++ b/build/postmate.min.js
@@ -4,4 +4,4 @@
* @link https://github.com/dollarshaveclub/postmate
* @author Jacob Kelley
* @license MIT */
-!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):e.Postmate=t()}(this,function(){"use strict";function e(){return++l}function t(){var e;c.debug&&(e=console).log.apply(e,arguments)}function n(e){var t=document.createElement("a");return t.href=e,t.origin||t.protocol+"//"+t.hostname}function i(e,t){return e.origin===t&&("object"===r(e.data)&&("postmate"in e.data&&(e.data.type===d&&!!{"handshake-reply":1,call:1,emit:1,reply:1,request:1}[e.data.postmate])))}function a(e,t){var n="function"==typeof e[t]?e[t]():e[t];return c.Promise.resolve(n)}var r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol?"symbol":typeof e},o=function(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")},s=function(){function e(e,t){for(var n=0;n0&&void 0!==arguments[0]?arguments[0]:{};if(o(e,s)){var i=e.data||{},c=i.methodName,u=i.uid,f=i.args;if("call"===e.data.postmate&&(r(a+": Received "+c+"() call"),c in t)){var p=t[c].apply(t,n(f));m.resolve(p).then(function(e){r(a+": Sending "+c+"() reply"),d.postMessage({postmate:"reply",type:l,uid:u,returnValue:e},s)})}}};return i.addEventListener("message",c,!1),r(a+": Awaiting calls..."),function(){i.removeEventListener("message",c,!1)}}Object.defineProperty(e,"__esModule",{value:!0});var s="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(e){return typeof e}:function(e){return e&&"function"==typeof Symbol&&e.constructor===Symbol&&e!==Symbol.prototype?"symbol":typeof e},l="application/x-postmate-v1+json",c=0,u=!1,m=function(){try{return window?window.Promise:m}catch(e){return null}}();e.connectParent=function(e){var n=e.url,t=e.container,c=e.methods,u=void 0===c?{}:c,f=window,p=document.createElement("iframe");(t||document.body).appendChild(p);var v=p.contentWindow||p.contentDocument.parentWindow,h=a(n);return new m(function(e,t){var a=function n(a){if(!o(a,h))return!1;if("handshake-reply"===a.data.postmate){var l=function(){r("Parent: Received handshake reply from Child"),f.removeEventListener("message",n,!1),r("Parent: Saving Child origin",a.origin);var t={localName:"Parent",local:f,remote:v,remoteOrigin:a.origin},o=d(t,u),s=i(t,a.data.methodNames);return s.frame=p,s.destroy=function(){o(),p.parentNode.removeChild(p)},{v:e(s)}}();if("object"===("undefined"==typeof l?"undefined":s(l)))return l.v}return r("Parent: Invalid handshake reply"),t("Failed handshake")};f.addEventListener("message",a,!1);var c=function(){r("Parent: Sending handshake"),v.postMessage({postmate:"handshake",type:l,methodNames:Object.keys(u)},h)};p.attachEvent?p.attachEvent("onload",c):p.onload=c,r("Parent: Loading frame"),p.src=n})},e.connectChild=function(e){var n=e.methods,t=void 0===n?{}:n,a=window,o=a.parent;return new m(function(e,n){var s=function s(c){if(c.data&&"handshake"===c.data.postmate){r("Child: Received handshake from Parent"),a.removeEventListener("message",s,!1),r("Child: Sending handshake reply to Parent"),c.source.postMessage({postmate:"handshake-reply",type:l,methodNames:Object.keys(t)},c.origin),r("Child: Saving Parent origin",c.origin);var u={localName:"Child",local:a,remote:o,remoteOrigin:c.origin};return d(u,t),e(i(u,c.data.methodNames))}return n("Child: Handshake Reply Failed")};a.addEventListener("message",s,!1)})},e.setDebug=function(e){return u=e},e.setPromise=function(e){return m=e}});
\ No newline at end of file
diff --git a/package.json b/package.json
index e2155807..8589659a 100644
--- a/package.json
+++ b/package.json
@@ -7,29 +7,28 @@
"build"
],
"devDependencies": {
- "babel-plugin-transform-class-properties": "^6.11.5",
- "babel-preset-es2015-rollup": "^1.1.1",
+ "babel-plugin-rename-umd-globals": "0.0.4",
+ "babel-plugin-transform-es2015-modules-umd": "^6.12.0",
+ "babel-preset-es2015": "^6.16.0",
"chai": "^3.5.0",
"connect": "^3.4.1",
"eslint-config-airbnb": "^9.0.1",
"eslint-plugin-import": "^1.12.0",
"gulp": "^3.9.1",
+ "gulp-babel": "^6.1.2",
"gulp-eslint": "^3.0.1",
"gulp-header": "^1.8.7",
"gulp-mocha-phantomjs": "^0.11.0",
+ "gulp-rename": "^1.2.2",
+ "gulp-uglify": "^2.0.0",
"mocha": "^3.0.2",
"mocha-phantomjs": "^4.1.0",
- "rollup-plugin-babel": "^2.6.1",
- "rollup-plugin-uglify": "^1.0.1",
- "rollup-stream": "^1.11.0",
"rsvp": "^3.2.1",
- "serve-static": "^1.11.1",
- "uglify-js": "^2.7.0",
- "vinyl-source-stream": "^1.1.0"
+ "serve-static": "^1.11.1"
},
"scripts": {
"test": "gulp test",
- "build": "gulp build && gulp update-readme",
+ "build": "gulp build update-readme",
"build-watch": "gulp build-watch",
"lint": "gulp lint",
"postpublish": "git tag $npm_package_version && git push origin --tags"
@@ -52,5 +51,6 @@
"bugs": {
"url": "https://github.com/dollarshaveclub/postmate/issues"
},
- "homepage": "https://github.com/dollarshaveclub/postmate"
+ "homepage": "https://github.com/dollarshaveclub/postmate",
+ "dependencies": {}
}
diff --git a/src/postmate.js b/src/postmate.js
index ddeca031..6cb8825a 100644
--- a/src/postmate.js
+++ b/src/postmate.js
@@ -11,6 +11,17 @@ const MESSAGE_TYPE = 'application/x-postmate-v1+json';
*/
let _messageId = 0;
+let debug = false;
+
+// Internet Explorer craps itself
+let Promise = (() => {
+ try {
+ return window ? window.Promise : Promise;
+ } catch(e) {
+ return null;
+ }
+})();
+
/**
* Increments and returns a message ID
* @return {Number} A unique ID for a message
@@ -24,7 +35,7 @@ function messageId() {
* @param {Object} ...args Rest Arguments
*/
function log(...args) {
- if (!Postmate.debug) return;
+ if (!debug) return;
console.log(...args); // eslint-disable-line no-console
}
@@ -53,295 +64,194 @@ function sanitize(message, allowedOrigin) {
if (!{
'handshake-reply': 1,
call: 1,
- emit: 1,
- reply: 1,
- request: 1
+ reply: 1
}[message.data.postmate]) return false;
return true;
}
-/**
- * Takes a model, and searches for a value by the property
- * @param {Object} model The dictionary to search against
- * @param {String} property A path within a dictionary (i.e. 'window.location.href')
- * @param {Object} data Additional information from the get request that is
- * passed to functions in the child model
- * @return {Promise}
- */
-function resolveValue(model, property) {
- const unwrappedContext = typeof model[property] === 'function'
- ? model[property]() : model[property];
- return Postmate.Promise.resolve(unwrappedContext);
-}
+function createCallSender(info, methodNames) {
+ const { localName, local, remote, remoteOrigin } = info;
+
+ log(`${localName}: Creating call sender`);
+
+ const createMethodProxy = methodName => {
+ return (...args) => {
+ log(`${localName}: Sending ${methodName}() call`);
+ return new Promise(resolve => {
+ // Extract data from response and kill listeners
+ const uid = messageId();
+ const transact = message => {
+ if (!sanitize(message, remoteOrigin)) return;
+ if (message.data.uid === uid && message.data.postmate === 'reply') {
+ log(`${localName}: Received ${methodName}() reply`);
+ local.removeEventListener('message', transact, false);
+ resolve(message.data.returnValue);
+ }
+ };
-/**
- * Composes an API to be used by the parent
- * @param {Object} info Information on the consumer
- */
-class ParentAPI {
-
- constructor(info) {
- this.parent = info.parent;
- this.frame = info.frame;
- this.child = info.child;
- this.childOrigin = info.childOrigin;
-
- this.events = {};
-
- log('Parent: Registering API');
- log('Parent: Awaiting messages...');
-
- this.listener = e => {
- const { data, name } = (((e || {}).data || {}).value || {});
- if (e.data.postmate === 'emit') {
- log(`Parent: Received event emission: ${name}`);
- if (name in this.events) {
- this.events[name].call(this, data);
- }
- }
+ local.addEventListener('message', transact, false);
+ remote.postMessage({
+ postmate: 'call',
+ type: MESSAGE_TYPE,
+ uid,
+ methodName,
+ args
+ }, remoteOrigin);
+ });
};
+ };
- this.parent.addEventListener('message', this.listener, false);
- log('Parent: Awaiting event emissions from Child');
- }
+ return methodNames.reduce((api, methodName) => {
+ api[methodName] = createMethodProxy(methodName);
+ return api;
+ }, {});
+}
+function connectCallReceiver(info, methods) {
+ const { localName, local, remote, remoteOrigin } = info;
- get(property) {
- return new Postmate.Promise(resolve => {
- // Extract data from response and kill listeners
- const uid = messageId();
- const transact = e => {
- if (e.data.uid === uid && e.data.postmate === 'reply') {
- this.parent.removeEventListener('message', transact, false);
- resolve(e.data.value);
- }
- };
+ log(`${localName}: Connecting call receiver`);
- // Prepare for response from Child...
- this.parent.addEventListener('message', transact, false);
+ const listener = (message = {}) => {
+ if (!sanitize(message, remoteOrigin)) return;
+ const { methodName, uid, args } = message.data || {};
- // Then ask child for information
- this.child.postMessage({
- postmate: 'request',
- type: MESSAGE_TYPE,
- property,
- uid,
- }, this.childOrigin);
- });
- }
+ if (message.data.postmate === 'call') {
+ log(`${localName}: Received ${methodName}() call`);
+ if (methodName in methods) {
+ var methodReturnValue = methods[methodName](...args);
+ Promise.resolve(methodReturnValue).then(messageReplyValue => {
+ log(`${localName}: Sending ${methodName}() reply`);
- call(property, data) {
- // Send information to the child
- this.child.postMessage({
- postmate: 'call',
- type: MESSAGE_TYPE,
- property,
- data,
- }, this.childOrigin);
- }
+ remote.postMessage({
+ postmate: 'reply',
+ type: MESSAGE_TYPE,
+ uid,
+ returnValue: messageReplyValue,
+ }, remoteOrigin);
+ });
+ }
+ }
+ };
- on(eventName, callback) {
- this.events[eventName] = callback;
- }
+ local.addEventListener('message', listener, false);
- destroy() {
- log('Parent: Destroying Postmate instance');
- window.removeEventListener('message', this.listener, false);
- this.frame.parentNode.removeChild(this.frame);
- }
+ log(`${localName}: Awaiting calls...`);
+
+ return () => {
+ local.removeEventListener('message', listener, false);
+ };
}
/**
- * Composes an API to be used by the child
- * @param {Object} info Information on the consumer
+ * The entry point of the Parent.
+ * @type {Function}
*/
-class ChildAPI {
-
- constructor(info) {
- this.model = info.model;
- this.parent = info.parent;
- this.parentOrigin = info.parentOrigin;
- this.child = info.child;
-
- log('Child: Registering API');
- log('Child: Awaiting messages...');
-
- this.child.addEventListener('message', e => {
- if (!sanitize(e, this.parentOrigin)) return;
- log('Child: Received request', e.data);
-
- const { property, uid, data } = e.data;
-
- if (e.data.postmate === 'call') {
- if (property in this.model && typeof this.model[property] === 'function') {
- this.model[property].call(this, data);
- }
- return;
+export const connectParent = ({ url, container, methods = {} }) => {
+ const parent = window;
+ const frame = document.createElement('iframe');
+ (container || document.body).appendChild(frame);
+ const child = frame.contentWindow || frame.contentDocument.parentWindow;
+
+ const childOrigin = resolveOrigin(url);
+ return new Promise((resolve, reject) => {
+ const reply = e => {
+ if (!sanitize(e, childOrigin)) return false;
+ if (e.data.postmate === 'handshake-reply') {
+ log('Parent: Received handshake reply from Child');
+ parent.removeEventListener('message', reply, false);
+
+ log('Parent: Saving Child origin', e.origin);
+
+ const info = {
+ localName: 'Parent',
+ local: parent,
+ remote: child,
+ remoteOrigin: e.origin
+ };
+
+ const disconnectReceiver = connectCallReceiver(info, methods);
+ const api = createCallSender(info, e.data.methodNames);
+
+ api.frame = frame;
+
+ api.destroy = function() {
+ disconnectReceiver();
+ frame.parentNode.removeChild(frame);
+ };
+
+ return resolve(api);
}
- // Reply to Parent
- resolveValue(this.model, property)
- .then(value => e.source.postMessage({
- property,
- postmate: 'reply',
- type: MESSAGE_TYPE,
- uid,
- value,
- }, e.origin));
- });
- }
-
- emit(name, data) {
- log(`Child: Emitting Event "${name}"`, data);
- this.parent.postMessage({
- postmate: 'emit',
- type: MESSAGE_TYPE,
- value: {
- name,
- data,
- },
- }, this.parentOrigin);
- }
-}
+ // Might need to remove since parent might be receiving different messages
+ // from different hosts
+ log('Parent: Invalid handshake reply');
+ return reject('Failed handshake');
+ };
-/**
- * The entry point of the Parent.
- * @type {Class}
- */
-class Postmate {
+ parent.addEventListener('message', reply, false);
- static debug = false;
+ const loaded = () => {
+ log('Parent: Sending handshake');
+ child.postMessage({
+ postmate: 'handshake',
+ type: MESSAGE_TYPE,
+ methodNames: Object.keys(methods)
+ }, childOrigin);
+ };
- // Internet Explorer craps itself
- static Promise = (() => {
- try {
- return window ? window.Promise : Promise;
- } catch(e) {
- return null;
+ if (frame.attachEvent){
+ frame.attachEvent("onload", loaded);
+ } else {
+ frame.onload = loaded;
}
- })();
-
- /**
- * Sets options related to the Parent
- * @param {Object} userOptions The element to inject the frame into, and the url
- * @return {Promise}
- */
- constructor(userOptions) {
- const { container, url, model } = userOptions;
-
- this.parent = window;
- this.frame = document.createElement('iframe');
- (container || document.body).appendChild(this.frame);
- this.child = this.frame.contentWindow || this.frame.contentDocument.parentWindow;
- this.model = model || {};
-
- return this.sendHandshake(url);
- }
-
- /**
- * Begins the handshake strategy
- * @param {String} url The URL to send a handshake request to
- * @return {Promise} Promise that resolves when the handshake is complete
- */
- sendHandshake(url) {
- const childOrigin = resolveOrigin(url);
- return new Postmate.Promise((resolve, reject) => {
- const reply = e => {
- if (!sanitize(e, childOrigin)) return false;
- if (e.data.postmate === 'handshake-reply') {
- log('Parent: Received handshake reply from Child');
- this.parent.removeEventListener('message', reply, false);
- this.childOrigin = e.origin;
- log('Parent: Saving Child origin', this.childOrigin);
- return resolve(new ParentAPI(this));
- }
-
- // Might need to remove since parent might be receiving different messages
- // from different hosts
- log('Parent: Invalid handshake reply');
- return reject('Failed handshake');
- };
-
- this.parent.addEventListener('message', reply, false);
-
-
- const loaded = () => {
- log('Parent: Sending handshake');
- this.child.postMessage({
- postmate: 'handshake',
- type: MESSAGE_TYPE,
- model: this.model,
- }, childOrigin);
- };
-
- if (this.frame.attachEvent){
- this.frame.attachEvent("onload", loaded);
- } else {
- this.frame.onload = loaded;
- }
- log('Parent: Loading frame');
- this.frame.src = url;
- });
- }
-}
+ log('Parent: Loading frame');
+ frame.src = url;
+ });
+};
/**
* The entry point of the Child
- * @type {Class}
+ * @type {Function}
*/
-Postmate.Model = class Model {
-
- /**
- * Initializes the child, model, parent, and responds to the Parents handshake
- * @param {Object} model Hash of values, functions, or promises
- * @return {Promise} The Promise that resolves when the handshake has been received
- */
- constructor(model) {
- this.child = window;
- this.model = model;
- this.parent = this.child.parent;
- return this.sendHandshakeReply();
- }
+export const connectChild = ({ methods = {} }) => {
+ const child = window;
+ const parent = child.parent;
+
+ return new Promise((resolve, reject) => {
+ const shake = message => {
+ if (message.data && message.data.postmate === 'handshake') {
+ log('Child: Received handshake from Parent');
+ child.removeEventListener('message', shake, false);
+
+ log('Child: Sending handshake reply to Parent');
+ message.source.postMessage({
+ postmate: 'handshake-reply',
+ type: MESSAGE_TYPE,
+ methodNames: Object.keys(methods)
+ }, message.origin);
- /**
- * Responds to a handshake initiated by the Parent
- * @return {Promise} Resolves an object that exposes an API for the Child
- */
- sendHandshakeReply() {
- return new Postmate.Promise((resolve, reject) => {
- const shake = e => {
- if (e.data.postmate === 'handshake') {
- log('Child: Received handshake from Parent');
- this.child.removeEventListener('message', shake, false);
- log('Child: Sending handshake reply to Parent');
- e.source.postMessage({
- postmate: 'handshake-reply',
- type: MESSAGE_TYPE,
- }, e.origin);
- this.parentOrigin = e.origin;
-
- // Extend model with the one provided by the parent
- const defaults = e.data.model;
- if (defaults) {
- const keys = Object.keys(defaults);
- for (let i = 0; i < keys.length; i++) {
- if (defaults.hasOwnProperty(keys[i])) {
- this.model[keys[i]] = defaults[keys[i]];
- }
- }
- log('Child: Inherited and extended model from Parent');
- }
+ log('Child: Saving Parent origin', message.origin);
- log('Child: Saving Parent origin', this.parentOrigin);
- return resolve(new ChildAPI(this));
- }
- return reject('Handshake Reply Failed');
- };
- this.child.addEventListener('message', shake, false);
- });
- }
+ const info = {
+ localName: 'Child',
+ local: child,
+ remote: parent,
+ remoteOrigin: message.origin
+ };
+
+ connectCallReceiver(info, methods);
+
+ return resolve(createCallSender(info, message.data.methodNames));
+ }
+ return reject('Child: Handshake Reply Failed');
+ };
+
+ child.addEventListener('message', shake, false);
+ });
};
-// Export
-export default Postmate;
+export const setDebug = value => debug = value;
+
+export const setPromise = value => Promise = value;
diff --git a/test/fixtures/child.html b/test/fixtures/child.html
index b20e9530..f32392dd 100644
--- a/test/fixtures/child.html
+++ b/test/fixtures/child.html
@@ -16,38 +16,35 @@
-
+