diff --git a/README.md b/README.md index 6875df28..1ab35e41 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,8 @@ otherwise the polyfilled `fetch` will be installed during the first pass of the If set to `false`, the polyfilled `fetch` will replace native `fetch` be there or not. +If all your [browser targets](https://guides.emberjs.com/release/configuring-ember/build-targets/) support native `fetch`, and `preferNative: true`, the polyfill will not be included in the output build. If, for some reason, you still need the polyfill to be included in the bundle, you can set `alwaysIncludePolyfill: true`. + The way you do import remains same. ## Browser Support diff --git a/assets/browser-fetch.js.t b/assets/browser-fetch.js.t index 8d8871c2..0f4a313a 100644 --- a/assets/browser-fetch.js.t +++ b/assets/browser-fetch.js.t @@ -33,6 +33,10 @@ <%= moduleBody %> + if (!self.fetch) { + throw new Error('fetch is not defined - maybe your browser targets are not covering everything you need?'); + } + var pending = 0; function decrement(result) { pending--; diff --git a/index.js b/index.js index b65b0cf2..7497b275 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,6 @@ 'use strict'; +const caniuse = require('caniuse-api'); const path = require('path'); // We use a few different Broccoli plugins to build our trees: // @@ -73,7 +74,11 @@ module.exports = { importTarget = this; } - app._fetchBuildConfig = app.options['ember-fetch'] || { preferNative: false }; + app._fetchBuildConfig = Object.assign({ + preferNative: false, + alwaysIncludePolyfill: false, + browsers: this.project.targets && this.project.targets.browsers + }, app.options['ember-fetch']); importTarget.import('vendor/ember-fetch.js', { exports: { @@ -100,7 +105,10 @@ module.exports = { treeForVendor() { let babelAddon = this.addons.find(addon => addon.name === 'ember-cli-babel'); - let browserTree = this.treeForBrowserFetch(); + const app = this._findApp(); + const options = app._fetchBuildConfig; + + let browserTree = this.treeForBrowserFetch(options); if (babelAddon) { browserTree = debug(babelAddon.transpileTree(browserTree, { 'ember-cli-babel': { @@ -112,8 +120,7 @@ module.exports = { this.ui.writeWarnLine('[ember-fetch] Could not find `ember-cli-babel` addon, opting out of transpilation!') } - const app = this._findApp(); - const preferNative = app._fetchBuildConfig.preferNative; + const preferNative = options.preferNative; return debug(map(browserTree, (content) => `if (typeof FastBoot === 'undefined') { var preferNative = ${preferNative}; @@ -143,32 +150,52 @@ module.exports = { // Returns a tree containing the browser polyfill (from `whatwg-fetch` and `abortcontroller-polyfill`), // wrapped in a shim that stops it from exporting a global and instead turns it into a module // that can be used by the Ember app. - treeForBrowserFetch() { - const abortcontrollerNode = debug(new Rollup(path.dirname(path.dirname(require.resolve('abortcontroller-polyfill'))), { - rollup: { - input: 'src/abortcontroller-polyfill.js', - output: { - file: 'abortcontroller.js', - // abortcontroller is polyfill only, the name is only required by rollup iife - name: 'AbortController', - format: 'iife' - }, - } - }), 'abortcontroller'); - - const fetchNode = debug(new Rollup(path.dirname(path.dirname(require.resolve('whatwg-fetch'))), { - rollup: { - input: 'fetch.js', - output: { - file: 'fetch.js', - name: 'WHATWGFetch', - format: 'iife' + treeForBrowserFetch(options) { + const browsers = options.browsers; + // To skip including the polyfill, `preferNative` needs to be `true` AND `alwaysIncludePolyfill` needs to be `false` (default) + const alwaysIncludePolyfill = !options.preferNative || options.alwaysIncludePolyfill; + const needsFetchPolyfill = alwaysIncludePolyfill || !this._checkSupports('fetch', browsers); + const needsAbortControllerPolyfill = alwaysIncludePolyfill || !this._checkSupports('abortcontroller', browsers); + + const inputNodes = []; + const inputFiles = []; + + if (needsAbortControllerPolyfill) { + const abortcontrollerNode = debug(new Rollup(path.dirname(path.dirname(require.resolve('abortcontroller-polyfill'))), { + rollup: { + input: 'src/abortcontroller-polyfill.js', + output: { + file: 'abortcontroller.js', + // abortcontroller is polyfill only, the name is only required by rollup iife + name: 'AbortController', + format: 'iife' + } } - } - }), 'whatwg-fetch'); + }), 'abortcontroller'); + + inputNodes.push(abortcontrollerNode); + inputFiles.push('abortcontroller.js'); + } - const polyfillNode = debug(concat(new MergeTrees([abortcontrollerNode, fetchNode]), { - inputFiles: ['abortcontroller.js', 'fetch.js'], + if (needsFetchPolyfill) { + const fetchNode = debug(new Rollup(path.dirname(path.dirname(require.resolve('whatwg-fetch'))), { + rollup: { + input: 'fetch.js', + output: { + file: 'fetch.js', + name: 'WHATWGFetch', + format: 'iife' + } + } + }), 'whatwg-fetch'); + + inputNodes.push(fetchNode); + inputFiles.push('fetch.js'); + } + + const polyfillNode = debug(concat(new MergeTrees(inputNodes), { + inputFiles, + allowNone: true, outputFile: 'ember-fetch.js', sourceMapConfig: { enabled: false } }), 'after-concat'); @@ -180,6 +207,15 @@ module.exports = { }), 'browser-fetch'); }, + _checkSupports(featureName, browsers) { + if (!browsers) { + return false; + } + + let browserList = browsers.join(', '); + return caniuse.isSupported(featureName, browserList); + }, + _findApp() { if (typeof this._findHost === 'function') { return this._findHost(); diff --git a/package.json b/package.json index 34d6ca2a..2d35de89 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "broccoli-stew": "^2.0.0", "broccoli-templater": "^2.0.1", "calculate-cache-key-for-tree": "^1.1.0", + "caniuse-api": "^3.0.0", "ember-cli-babel": "^6.8.2", "node-fetch": "^2.3.0", "whatwg-fetch": "^3.0.0" diff --git a/test/browsers-target-test.js b/test/browsers-target-test.js new file mode 100644 index 00000000..a9a8a450 --- /dev/null +++ b/test/browsers-target-test.js @@ -0,0 +1,186 @@ +'use strict'; + +const AddonFactory = require('../'); +const expect = require('chai').expect; +const helpers = require('broccoli-test-helper'); +const co = require('co'); + +describe(`Do not include the polyfill if the browser targets match`, function() { + let output, subject, addon; + + beforeEach(function() { + addon = Object.create(AddonFactory); + Object.assign(addon, { + addons: [], + _fetchBuildConfig: { + preferNative: true, + alwaysIncludePolyfill: false, + browsers: ['last 1 chrome versions'] + }, + ui: { + writeWarnLine() { + } + } + }); + subject = addon.treeForVendor(); + output = helpers.createBuilder(subject); + }); + + afterEach(co.wrap(function* () { + yield output.dispose(); + })); + + it('fetch & AbortController polyfills are not included', co.wrap(function* () { + yield output.build(); + let files = output.read(); + expect(files).to.have.all.keys('ember-fetch.js'); + expect(files['ember-fetch.js']).to.include(`var preferNative = true`); + expect(files['ember-fetch.js']).to.not.include(`fetch.polyfill = true`); + expect(files['ember-fetch.js']).to.not.include(`class AbortController`); + })); + +}); + +describe(`Ignore target browsers if preferNative is false`, function() { + let output, subject, addon; + + beforeEach(function() { + addon = Object.create(AddonFactory); + Object.assign(addon, { + addons: [], + _fetchBuildConfig: { + preferNative: false, + alwaysIncludePolyfill: false, + browsers: ['last 1 chrome versions'] + }, + ui: { + writeWarnLine() { + } + } + }); + subject = addon.treeForVendor(); + output = helpers.createBuilder(subject); + }); + + afterEach(co.wrap(function* () { + yield output.dispose(); + })); + + it('fetch & AbortController polyfills are included', co.wrap(function* () { + yield output.build(); + let files = output.read(); + expect(files).to.have.all.keys('ember-fetch.js'); + expect(files['ember-fetch.js']).to.include(`var preferNative = false`); + expect(files['ember-fetch.js']).to.include(`fetch.polyfill = true`); + expect(files['ember-fetch.js']).to.include(`class AbortController`); + })); + +}); + +describe(`Include the polyfill if the browser targets do not match`, function() { + let output, subject, addon; + + beforeEach(function() { + addon = Object.create(AddonFactory); + Object.assign(addon, { + addons: [], + _fetchBuildConfig: { + preferNative: true, + alwaysIncludePolyfill: false, + browsers: ['ie 11'] + }, + ui: { + writeWarnLine() { + } + } + }); + subject = addon.treeForVendor(); + output = helpers.createBuilder(subject); + }); + + afterEach(co.wrap(function* () { + yield output.dispose(); + })); + + it('fetch & AbortController polyfills are included', co.wrap(function* () { + yield output.build(); + let files = output.read(); + expect(files).to.have.all.keys('ember-fetch.js'); + expect(files['ember-fetch.js']).to.include(`var preferNative = true`); + expect(files['ember-fetch.js']).to.include(`fetch.polyfill = true`); + expect(files['ember-fetch.js']).to.include(`class AbortController`); + })); + +}); + +describe(`Include the abortcontroller polyfill only if the browser targets support fetch only`, function() { + let output, subject, addon; + + beforeEach(function() { + addon = Object.create(AddonFactory); + Object.assign(addon, { + addons: [], + _fetchBuildConfig: { + preferNative: true, + alwaysIncludePolyfill: false, + browsers: ['safari 11'] + }, + ui: { + writeWarnLine() { + } + } + }); + subject = addon.treeForVendor(); + output = helpers.createBuilder(subject); + }); + + afterEach(co.wrap(function* () { + yield output.dispose(); + })); + + it('AbortController polyfill is included', co.wrap(function* () { + yield output.build(); + let files = output.read(); + expect(files).to.have.all.keys('ember-fetch.js'); + expect(files['ember-fetch.js']).to.include(`var preferNative = true`); + expect(files['ember-fetch.js']).to.not.include(`fetch.polyfill = true`); + expect(files['ember-fetch.js']).to.include(`class AbortController`); + })); + +}); + +describe(`Include the polyfill if alwaysIncludePolyfill=true`, function() { + let output, subject, addon; + + beforeEach(function() { + addon = Object.create(AddonFactory); + Object.assign(addon, { + addons: [], + _fetchBuildConfig: { + preferNative: true, + alwaysIncludePolyfill: true, + browsers: ['last 1 chrome versions'] + }, + ui: { + writeWarnLine() { + } + } + }); + subject = addon.treeForVendor(); + output = helpers.createBuilder(subject); + }); + + afterEach(co.wrap(function* () { + yield output.dispose(); + })); + + it('fetch & AbortController polyfills are included', co.wrap(function* () { + yield output.build(); + let files = output.read(); + expect(files).to.have.all.keys('ember-fetch.js'); + expect(files['ember-fetch.js']).to.include(`var preferNative = true`); + expect(files['ember-fetch.js']).to.include(`fetch.polyfill = true`); + expect(files['ember-fetch.js']).to.include(`class AbortController`); + })); + +}); diff --git a/yarn.lock b/yarn.lock index 12c725b8..4d89ad47 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2224,14 +2224,14 @@ browserslist@^3.2.6: caniuse-lite "^1.0.30000844" electron-to-chromium "^1.3.47" -browserslist@^4.1.0: - version "4.3.4" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.3.4.tgz#4477b737db6a1b07077275b24791e680d4300425" - integrity sha512-u5iz+ijIMUlmV8blX82VGFrB9ecnUg5qEt55CMZ/YJEhha+d8qpBfOFuutJ6F/VKRXjZoD33b6uvarpPxcl3RA== +browserslist@^4.0.0, browserslist@^4.1.0: + version "4.3.6" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.3.6.tgz#0f9d9081afc66b36f477c6bdf3813f784f42396a" + integrity sha512-kMGKs4BTzRWviZ8yru18xBpx+CyHG9eqgRbj9XbE3IMgtczf4aiA0Y1YCpVdvUieKGZ03kolSPXqTcscBCb9qw== dependencies: - caniuse-lite "^1.0.30000899" - electron-to-chromium "^1.3.82" - node-releases "^1.0.1" + caniuse-lite "^1.0.30000921" + electron-to-chromium "^1.3.92" + node-releases "^1.1.1" bser@^2.0.0: version "2.0.0" @@ -2385,10 +2385,20 @@ can-symlink@^1.0.0: dependencies: tmp "0.0.28" -caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30000899: - version "1.0.30000907" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000907.tgz#0b9899bde53fb1c30e214fb12402361e02ff5c42" - integrity sha512-No5sQ/OB2Nmka8MNOOM6nJx+Hxt6MQ6h7t7kgJFu9oTuwjykyKRSBP/+i/QAyFHxeHB+ddE0Da1CG5ihx9oehQ== +caniuse-api@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/caniuse-api/-/caniuse-api-3.0.0.tgz#5e4d90e2274961d46291997df599e3ed008ee4c0" + integrity sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw== + dependencies: + browserslist "^4.0.0" + caniuse-lite "^1.0.0" + lodash.memoize "^4.1.2" + lodash.uniq "^4.5.0" + +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000844, caniuse-lite@^1.0.30000921: + version "1.0.30000921" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000921.tgz#7a607c1623444b22351d834e093aedda3c42fbe8" + integrity sha512-Bu09ciy0lMWLgpYC77I0YGuI8eFRBPPzaSOYJK1jTI64txCphYCqnWbxJYjHABYVt/TYX/p3jNjLBR87u1Bfpw== capture-exit@^1.2.0: version "1.2.0" @@ -3113,11 +3123,16 @@ ee-first@1.1.1: resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= -electron-to-chromium@^1.3.47, electron-to-chromium@^1.3.82: +electron-to-chromium@^1.3.47: version "1.3.84" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.84.tgz#2e55df59e818f150a9f61b53471ebf4f0feecc65" integrity sha512-IYhbzJYOopiTaNWMBp7RjbecUBsbnbDneOP86f3qvS0G0xfzwNSvMJpTrvi5/Y1gU7tg2NAgeg8a8rCYvW9Whw== +electron-to-chromium@^1.3.92: + version "1.3.92" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.92.tgz#9027b5abaea400045edd652c0e4838675c814399" + integrity sha512-En051LMzMl3/asMWGZEtU808HOoVWIpmmZx1Ep8N+TT9e7z/X8RcLeBU2kLSNLGQ+5SuKELzMx+MVuTBXk6Q9w== + ember-assign-polyfill@~2.4.0: version "2.4.0" resolved "https://registry.yarnpkg.com/ember-assign-polyfill/-/ember-assign-polyfill-2.4.0.tgz#acb00466f7d674b3e6b030acfe255b3b1f6472e1" @@ -6138,6 +6153,11 @@ lodash.keys@~2.3.0: lodash._shimkeys "~2.3.0" lodash.isobject "~2.3.0" +lodash.memoize@^4.1.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe" + integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4= + lodash.merge@^4.3.1, lodash.merge@^4.6.0: version "4.6.1" resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.1.tgz#adc25d9cb99b9391c59624f379fbba60d7111d54" @@ -6201,7 +6221,7 @@ lodash.templatesettings@~2.3.0: lodash._reinterpolate "~2.3.0" lodash.escape "~2.3.0" -lodash.uniq@^4.2.0: +lodash.uniq@^4.2.0, lodash.uniq@^4.5.0: version "4.5.0" resolved "https://registry.yarnpkg.com/lodash.uniq/-/lodash.uniq-4.5.0.tgz#d0225373aeb652adc1bc82e4945339a842754773" integrity sha1-0CJTc662Uq3BvILklFM5qEJ1R3M= @@ -6790,10 +6810,10 @@ node-pre-gyp@^0.10.0: semver "^5.3.0" tar "^4" -node-releases@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.0.3.tgz#3414ed84595096459c251699bfcb47d88324a9e4" - integrity sha512-ZaZWMsbuDcetpHmYeKWPO6e63pSXLb50M7lJgCbcM2nC/nQC3daNifmtp5a2kp7EWwYfhuvH6zLPWkrF8IiDdw== +node-releases@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.1.tgz#8fff8aea1cfcad1fb4205f805149054fbf73cafd" + integrity sha512-2UXrBr6gvaebo5TNF84C66qyJJ6r0kxBObgZIDX3D3/mt1ADKiHux3NJPWisq0wxvJJdkjECH+9IIKYViKj71Q== dependencies: semver "^5.3.0"