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 @@ - + diff --git a/test/runner.html b/test/runner.html index 896ee4e8..718c481f 100644 --- a/test/runner.html +++ b/test/runner.html @@ -12,7 +12,7 @@ - +