diff --git a/.gitignore b/.gitignore index 0b129d4..b7c6cea 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ *.log *.sublime-* borodean-jsonp-*.tgz -dist/jsonp.min.js -dist/jsonp.min.js.map +dist/*.min.js +dist/*.min.js.map node_modules diff --git a/.npmignore b/.npmignore index d7f5a9b..3980c3a 100644 --- a/.npmignore +++ b/.npmignore @@ -3,6 +3,7 @@ .npmignore .travis.yml borodean-jsonp-*.tgz +dist/*-*.*.*.min.* karma.conf.js rollup.config.js -test.js +test diff --git a/README.md b/README.md index d9e2201..ce4bc7c 100644 --- a/README.md +++ b/README.md @@ -12,12 +12,27 @@ Loads data from the server using [JSONP][jsonp]. Example: ```js -jsonp('https://jsfiddle.net/echo/jsonp?foo=bar', function (err, data) { +import jsonp from '@borodean/jsonp'; + +jsonp('https://jsfiddle.net/echo/jsonp?foo=bar', (err, data) => { if (err) throw err; console.log(data); }); ``` +# Promise version + +A version that returns a promise is also available: + +```js +import jsonp from '@borodean/jsonp/promise'; + +jsonp('https://jsfiddle.net/echo/jsonp?foo=bar').then( + data => console.log(data), + err => console.log(err) +); +``` + ## Installation ``` @@ -26,11 +41,18 @@ npm install @borodean/jsonp For a browser global version check the `dist` directory of the installed module or directly download it: -- [Production version][dl] – 268 bytes, minified and gzipped -- [Source map][dl-map] +- [Production version][dl-callback] – 266 bytes, minified and gzipped +- [Source map][dl-callback-map] + +Promise version: + +- [Production version][dl-promise] – 277 bytes, minified and gzipped +- [Source map][dl-promise-map] -[dl]: https://github.com/borodean/jsonp/releases/download/2.0.0/jsonp-2.0.0.min.js -[dl-map]: https://github.com/borodean/jsonp/releases/download/2.0.0/jsonp-2.0.0.min.js.map +[dl-callback]: https://github.com/borodean/jsonp/releases/download/3.0.0/jsonp-3.0.0.min.js +[dl-callback-map]: https://github.com/borodean/jsonp/releases/download/3.0.0/jsonp-3.0.0.min.js.map +[dl-promise]: https://github.com/borodean/jsonp/releases/download/3.0.0/jsonp-promise-3.0.0.min.js +[dl-promise-map]: https://github.com/borodean/jsonp/releases/download/3.0.0/jsonp-promise-3.0.0.min.js.map [jsonp]: http://bob.ippoli.to/archives/2005/12/05/remote-json-jsonp/ [sauce]: https://saucelabs.com/u/borodean-jsonp [sauce-matrix]: https://saucelabs.com/browser-matrix/borodean-jsonp.svg diff --git a/index.js b/callback.js similarity index 75% rename from index.js rename to callback.js index 9ec15e7..faa2fca 100644 --- a/index.js +++ b/callback.js @@ -13,7 +13,7 @@ module.exports = function (url, options, callback) { var script = document.createElement('script'); script.src = parameter ? (url + (~url.indexOf('?') ? '&' : '?') + parameter + '=' + key) : url; // eslint-disable-line no-implicit-coercion - script.onerror = function () { + script.onerror = function () { // eslint-disable-line unicorn/prefer-add-event-listener delete object[key]; callback(new Error()); }; @@ -23,5 +23,5 @@ module.exports = function (url, options, callback) { callback(null, response); }; - document.head.removeChild(document.head.appendChild(script)); + document.head.removeChild(document.head.appendChild(script)); // eslint-disable-line unicorn/prefer-node-append }; diff --git a/karma.conf.js b/karma.conf.js index 8257c06..6cde869 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -36,10 +36,10 @@ module.exports = config => { browserify: { debug: true }, - files: ['test.js'], + files: ['test/*'], frameworks: ['browserify', 'chai', 'mocha', 'sinon'], preprocessors: { - 'test.js': ['browserify'] + 'test/*': ['browserify'] }, reporters: ['dots'], singleRun: true diff --git a/package.json b/package.json index 815d115..95960aa 100644 --- a/package.json +++ b/package.json @@ -1,19 +1,22 @@ { "name": "@borodean/jsonp", - "version": "2.0.0", + "version": "3.0.0", "description": "The smallest possible JSONP implementation", "license": "MIT", "author": "Vadym Borodin http://borodean.com", + "main": "callback.js", "repository": "borodean/jsonp", "scripts": { - "build": "rollup -co dist/jsonp.min.js", - "lint": "xo --env=browser --env=mocha --global expect --global sinon --space", + "build": "rollup -c", + "lint": "xo --env=browser --env=mocha --global expect --global sinon --no-esnext --space", "test": "karma start", "test-local": "karma start --local" }, "devDependencies": { + "@rollup/plugin-commonjs": "^11.0.2", "browserify": "^16.5.0", "chai": "*", + "core-js": "^3.6.4", "karma": "^4.4.1", "karma-browserify": "^7.0.0", "karma-chai": "^0.1.0", @@ -22,12 +25,11 @@ "karma-sinon": "^1.0.5", "lodash": "^4.17.15", "mocha": "^4.1.0", - "rollup": "^0.41.4", - "rollup-plugin-commonjs": "^7.0.0", - "rollup-plugin-filesize": "^1.0.1", - "rollup-plugin-uglify": "^1.0.1", - "sinon": "^1.17.7", + "rollup": "^1.31.0", + "rollup-plugin-filesize": "^6.2.1", + "rollup-plugin-uglify": "^6.0.4", + "sinon": "^7.5.0", "watchify": "^3.11.1", - "xo": "^0.17.1" + "xo": "^0.26.0" } } diff --git a/promise.js b/promise.js new file mode 100644 index 0000000..600c5b1 --- /dev/null +++ b/promise.js @@ -0,0 +1,26 @@ +var count = 0; + +module.exports = function (url, options) { + options = options || {}; + + var object = options.object || window; + var key = options.key || 'j' + count++; + var parameter = 'parameter' in options ? options.parameter : 'callback'; + + var script = document.createElement('script'); + script.src = parameter ? (url + (~url.indexOf('?') ? '&' : '?') + parameter + '=' + key) : url; // eslint-disable-line no-implicit-coercion + + return new Promise(function (resolve, reject) { + script.onerror = function () { // eslint-disable-line unicorn/prefer-add-event-listener + delete object[key]; + reject(new Error()); + }; + + object[key] = function (response) { + delete object[key]; + resolve(response); + }; + + document.head.removeChild(document.head.appendChild(script)); // eslint-disable-line unicorn/prefer-node-append + }); +}; diff --git a/rollup.config.js b/rollup.config.js index 55735de..3919499 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -1,19 +1,40 @@ /* eslint-disable camelcase */ -module.exports = { - entry: 'index.js', - format: 'iife', - moduleName: 'jsonp', +import commonjs from '@rollup/plugin-commonjs'; +import filesize from 'rollup-plugin-filesize'; +import {uglify} from 'rollup-plugin-uglify'; + +import {version} from './package.json'; + +const name = 'jsonp'; + +const createInput = (input, outputBasename) => ({ + input, + output: [ + createOutput(`dist/${outputBasename}.min.js`), + createOutput(`dist/${outputBasename}-${version}.min.js`) + ], plugins: [ - require('rollup-plugin-commonjs')(), - require('rollup-plugin-filesize')(), - require('rollup-plugin-uglify')({ + commonjs(), + filesize(), + uglify({ compress: { collapse_vars: true, unsafe: true }, mangle: true }) - ], - sourceMap: true -}; + ] +}); + +const createOutput = file => ({ + file, + format: 'iife', + name, + sourcemap: true +}); + +export default [ + createInput('callback.js', name), + createInput('promise.js', `${name}-promise`) +]; diff --git a/test.js b/test/callback.js similarity index 59% rename from test.js rename to test/callback.js index d4b9bd6..42c451f 100644 --- a/test.js +++ b/test/callback.js @@ -3,21 +3,29 @@ var _ = require('lodash'); -var jsonp = require('.'); +var jsonp = require('../callback'); -var appendChild = sinon.spy(document.head, 'appendChild'); - -describe('jsonp', function () { +describe('jsonp/callback', function () { this.timeout(20000); + beforeEach(function () { + sinon.spy(document.head, 'appendChild'); + window.foo = {}; + }); + + afterEach(function () { + document.head.appendChild.restore(); + delete window.foo; + }); + it('injects a script', function (done) { jsonp('https://jsfiddle.net/echo/jsonp', done); - expect(appendChild.lastCall.args[0].src).to.equal('https://jsfiddle.net/echo/jsonp?callback=j0'); + expect(document.head.appendChild.lastCall.args[0].src).to.equal('https://jsfiddle.net/echo/jsonp?callback=j0'); }); it('respects query parameters', function (done) { jsonp('https://jsfiddle.net/echo/jsonp?foo=bar', done); - expect(appendChild.lastCall.args[0].src).to.equal('https://jsfiddle.net/echo/jsonp?foo=bar&callback=j1'); + expect(document.head.appendChild.lastCall.args[0].src).to.equal('https://jsfiddle.net/echo/jsonp?foo=bar&callback=j1'); }); it('handles simultaneous requests', function (done) { @@ -64,26 +72,28 @@ describe('jsonp', function () { it('sets a custom callback query parameter', function (done) { jsonp('https://www.reddit.com/api/info.json', {parameter: 'jsonp'}, done); - expect(appendChild.lastCall.args[0].src).to.equal('https://www.reddit.com/api/info.json?jsonp=j6'); + expect(document.head.appendChild.lastCall.args[0].src).to.equal('https://www.reddit.com/api/info.json?jsonp=j6'); }); it('disables the callback query parameter', function (done) { jsonp('https://httpbin.org/status/400', {parameter: ''}, done.bind(this, null)); - expect(appendChild.lastCall.args[0].src).to.equal('https://httpbin.org/status/400'); + expect(document.head.appendChild.lastCall.args[0].src).to.equal('https://httpbin.org/status/400'); }); - it('sets a custom callback name', function (done) { - jsonp('https://jsfiddle.net/echo/jsonp', {key: 'foo'}, done); - expect(window.foo).to.be.a('function'); - expect(appendChild.lastCall.args[0].src).to.equal('https://jsfiddle.net/echo/jsonp?callback=foo'); + it('retrieves data via a custom callback name', function (done) { + jsonp('https://jsfiddle.net/echo/jsonp?foo=bar', {key: 'foo'}, function (err, data) { + expect(err).to.be.null; + expect(data).to.deep.equal({foo: 'bar'}); + expect(document.head.appendChild.lastCall.args[0].src).to.equal('https://jsfiddle.net/echo/jsonp?foo=bar&callback=foo'); + done(); + }); }); - it('sets a custom callback object', function (done) { - window.foo = {}; - jsonp('https://jsfiddle.net/echo/jsonp?callback=foo.bar', {object: window.foo, key: 'bar', parameter: ''}, function () { - delete window.foo; + it('retrieves data via a custom callback object', function (done) { + jsonp('https://jsfiddle.net/echo/jsonp?foo=bar&callback=foo.bar', {object: window.foo, key: 'bar', parameter: ''}, function (err, data) { + expect(err).to.be.null; + expect(data).to.deep.equal({foo: 'bar'}); done(); }); - expect(window.foo.bar).to.be.a('function'); }); }); diff --git a/test/promise.js b/test/promise.js new file mode 100644 index 0000000..d814b28 --- /dev/null +++ b/test/promise.js @@ -0,0 +1,89 @@ +/* eslint-disable import/no-unassigned-import */ +/* eslint-disable no-unused-expressions */ +/* eslint-disable promise/prefer-await-to-then */ + +require('core-js/features/promise'); +var _ = require('lodash'); + +var jsonp = require('../promise'); + +describe('jsonp/promise', function () { + this.timeout(20000); + + beforeEach(function () { + sinon.spy(document.head, 'appendChild'); + window.foo = {}; + }); + + afterEach(function () { + document.head.appendChild.restore(); + delete window.foo; + }); + + it('injects a script', function () { + var promise = jsonp('https://jsfiddle.net/echo/jsonp'); + expect(document.head.appendChild.lastCall.args[0].src).to.equal('https://jsfiddle.net/echo/jsonp?callback=j0'); + return promise; + }); + + it('respects query parameters', function () { + var promise = jsonp('https://jsfiddle.net/echo/jsonp?foo=bar'); + expect(document.head.appendChild.lastCall.args[0].src).to.equal('https://jsfiddle.net/echo/jsonp?foo=bar&callback=j1'); + return promise; + }); + + it('handles simultaneous requests', function () { + return Promise.all([ + jsonp('https://jsfiddle.net/echo/jsonp?foo=bar&delay=1'), + jsonp('https://jsfiddle.net/echo/jsonp?baz=qux') + ]).then(function (data) { + expect(data[0]).to.deep.equal({foo: 'bar'}); + expect(data[1]).to.deep.equal({baz: 'qux'}); + }); + }); + + it('retrieves data and cleans up', function () { + var promise = jsonp('https://jsfiddle.net/echo/jsonp?foo=bar'); + return promise.then(function (data) { + expect(data).to.deep.equal({foo: 'bar'}); + expect(Object.keys(window).some(RegExp.prototype.test.bind(/^j\d+/))).to.be.false; + expect(document.querySelectorAll('script[src*="jsfiddle.net"]')).to.have.lengthOf(0); + }); + }); + + it('fails and cleans up', function () { + var promise = jsonp('https://httpbin.org/status/400'); + return promise.then(expect.fail, function (err) { + expect(err).to.be.an('error'); + expect(Object.keys(window).some(RegExp.prototype.test.bind(/^j\d+/))).to.be.false; + expect(document.querySelectorAll('script[src*="jsfiddle.net"]')).to.have.lengthOf(0); + }); + }); + + it('sets a custom callback query parameter', function () { + var promise = jsonp('https://www.reddit.com/api/info.json', {parameter: 'jsonp'}); + expect(document.head.appendChild.lastCall.args[0].src).to.equal('https://www.reddit.com/api/info.json?jsonp=j6'); + return promise; + }); + + it('disables the callback query parameter', function () { + var promise = jsonp('https://httpbin.org/status/400', {parameter: ''}); + expect(document.head.appendChild.lastCall.args[0].src).to.equal('https://httpbin.org/status/400'); + return promise.then(expect.fail, _.noop); + }); + + it('retrieves data via a custom callback name', function () { + var promise = jsonp('https://jsfiddle.net/echo/jsonp?foo=bar', {key: 'foo'}); + return promise.then(function (data) { + expect(data).to.deep.equal({foo: 'bar'}); + expect(document.head.appendChild.lastCall.args[0].src).to.equal('https://jsfiddle.net/echo/jsonp?foo=bar&callback=foo'); + }); + }); + + it('retrieves data via a custom callback object', function () { + var promise = jsonp('https://jsfiddle.net/echo/jsonp?foo=bar&callback=foo.bar', {object: window.foo, key: 'bar', parameter: ''}); + return promise.then(function (data) { + expect(data).to.deep.equal({foo: 'bar'}); + }); + }); +});