diff --git a/README.md b/README.md index 7dd58056..6875df28 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ export default DS.RESTAdapter.extend(AdapterFetch, { ``` ### Use with Fastboot +#### ajax-service Currently, Fastboot supplies its own server-side ajax functionality, and including `ember-fetch` and the `adapter-fetch` mixin in a Fastboot app will not work without some modifications. To allow the `node-fetch` polyfill that is included with this addon to make your API calls, you must add an initializer to the consuming app's `fastboot` directory that overrides the one Fastboot utilizes to inject its own ajax. Example: @@ -65,6 +66,17 @@ export default { } ``` +#### relative url +`ember-fetch` uses `node-fetch` in Fastboot, which [doesn't allow relative URL](https://github.com/bitinn/node-fetch/tree/v2.3.0#fetchurl-options). + +> `url` should be an absolute url, such as `https://example.com/`. +> A path-relative URL (`/file/under/root`) or protocol-relative URL (`//can-be-http-or-https.com/`) +> will result in a rejected promise. + +However, `ember-fetch` grabs the `protocol` and `host` info from fastboot request after the `instance-initializes`. +This allows you to make a relative URL request unless the app is not initialized, e.g. `initializers` and `app.js`. + +#### top-level addon For addon authors, if the addon supports Fastboot mode, `ember-fetch` should also be listed as a [peer dependency](https://docs.npmjs.com/files/package.json#peerdependencies). This is because Fastboot only invokes top-level addon's `updateFastBootManifest` ([detail](https://github.com/ember-fastboot/ember-cli-fastboot/issues/597)), thus `ember-fetch` has to be a top-level addon installed by the host app. diff --git a/fastboot/instance-initializers/setup-fetch.js b/fastboot/instance-initializers/setup-fetch.js index 4fce0619..0232f95a 100644 --- a/fastboot/instance-initializers/setup-fetch.js +++ b/fastboot/instance-initializers/setup-fetch.js @@ -1,8 +1,8 @@ -import setupFetch from 'fetch/setup'; +import { setupFastboot } from 'fetch'; /** * To allow relative URLs for Fastboot mode, we need the per request information - * from the fastboot service. Then we re-define the `fetch` amd module. + * from the fastboot service. Then we set the protocol and host to fetch module. */ function patchFetchForRelativeURLs(instance) { const fastboot = instance.lookup('service:fastboot'); @@ -10,7 +10,7 @@ function patchFetchForRelativeURLs(instance) { // Prember is not sending protocol const protocol = request.protocol === 'undefined:' ? 'http:' : request.protocol; // host is cp - setupFetch(protocol, request.get('host'))(); + setupFastboot(protocol, request.get('host')); } export default { diff --git a/index.js b/index.js index bc547812..b65b0cf2 100644 --- a/index.js +++ b/index.js @@ -97,7 +97,7 @@ module.exports = { * in a FastBoot build or not. Based on that, we return a tree that contains * the correct version of the polyfill at the `vendor/ember-fetch.js` path. */ - treeForVendor: function() { + treeForVendor() { let babelAddon = this.addons.find(addon => addon.name === 'ember-cli-babel'); let browserTree = this.treeForBrowserFetch(); @@ -121,9 +121,22 @@ module.exports = { }`), 'wrapped'); }, - //add node version of fetch.js into fastboot package.json manifest vendorFiles array - updateFastBootManifest: function(manifest) { - manifest.vendorFiles.push('ember-fetch/fastboot-fetch.js'); + // Only include public/fetch-fastboot.js if top level addon + treeForPublic() { + return !this.parent.parent ? this._super.treeForPublic.apply(this, arguments) : null; + }, + + cacheKeyForTree(treeType) { + if (treeType === 'public') { + return require('calculate-cache-key-for-tree')('public', this, [!this.parent.parent]); + } else { + return this._super.cacheKeyForTree.call(this, treeType); + } + }, + + // Add node version of fetch.js into fastboot package.json manifest vendorFiles array + updateFastBootManifest(manifest) { + manifest.vendorFiles.push('ember-fetch/fetch-fastboot.js'); return manifest; }, diff --git a/package.json b/package.json index 79b11f6b..03665412 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ "broccoli-rollup": "^2.1.1", "broccoli-stew": "^2.0.0", "broccoli-templater": "^2.0.1", + "calculate-cache-key-for-tree": "^1.1.0", "ember-cli-babel": "^6.8.2", "node-fetch": "^2.3.0", "whatwg-fetch": "^3.0.0" diff --git a/public/fastboot-fetch.js b/public/fastboot-fetch.js deleted file mode 100644 index 44b2766f..00000000 --- a/public/fastboot-fetch.js +++ /dev/null @@ -1,55 +0,0 @@ -/* globals define FastBoot */ -define('fetch/setup', ['exports'], function(self) { - var httpRegex = /^https?:\/\//; - var protocolRelativeRegex = /^\/\//; - - var AbortControllerPolyfill = FastBoot.require( - 'abortcontroller-polyfill/dist/cjs-ponyfill' - ); - var nodeFetch = FastBoot.require('node-fetch'); - - self['default'] = function(protocol, host) { - return function() { - define('fetch', ['exports'], function(exports) { - function buildAbsoluteUrl(url, protocol, host) { - if (protocolRelativeRegex.test(url)) { - url = host + url; - } else if (!httpRegex.test(url)) { - if (!host) { - throw new Error( - 'You are using using fetch with a path-relative URL, but host is missing from Fastboot request. Please set the hostWhitelist property in your environment.js.' - ); - } - url = protocol + '//' + host + url; - } - return url; - } - /** - * Setup the exported fetch for a given origin so it can handle: - * - protocol-relative URL (//can-be-http-or-https.com/) - * - path-relative URL (/file/under/root) - * @param {String|Object} input - * @param {Object} [options] - */ - exports['default'] = function fetch(input, options) { - if (typeof input === 'object') { - input.url = buildAbsoluteUrl(input.url, protocol, host); - } else { - input = buildAbsoluteUrl(input, protocol, host); - } - return nodeFetch(input, options); - }; - exports['Request'] = nodeFetch.Request; - exports['Headers'] = nodeFetch.Headers; - exports['Response'] = nodeFetch.Response; - exports['AbortController'] = AbortControllerPolyfill.AbortController; - }); - }; - }; -}); - -define('fetch/ajax', ['exports'], function() { - throw new Error( - 'You included `fetch/ajax` but it was renamed to `ember-fetch/ajax`' - ); -}); diff --git a/public/fetch-fastboot.js b/public/fetch-fastboot.js new file mode 100644 index 00000000..4bd9186b --- /dev/null +++ b/public/fetch-fastboot.js @@ -0,0 +1,75 @@ +/* globals define FastBoot */ +define('fetch', ['exports'], function(exports) { + var httpRegex = /^https?:\/\//; + var protocolRelativeRegex = /^\/\//; + + var AbortControllerPolyfill = FastBoot.require( + 'abortcontroller-polyfill/dist/cjs-ponyfill' + ); + var nodeFetch = FastBoot.require('node-fetch'); + + /** + * Build the absolute url if it's not, can handle: + * - protocol-relative URL (//can-be-http-or-https.com/) + * - path-relative URL (/file/under/root) + * + * @param {string} url + * @param {string} protocol + * @param {string} host + * @returns {string} + */ + function buildAbsoluteUrl(url, protocol, host) { + if (protocolRelativeRegex.test(url)) { + url = host + url; + } else if (!httpRegex.test(url)) { + if (!host) { + throw new Error( + 'You are using using fetch with a path-relative URL, but host is missing from Fastboot request. Please set the hostWhitelist property in your environment.js.' + ); + } + url = protocol + '//' + host + url; + } + return url; + } + + var FastbootHost, FastbootProtocol; + + /** + * Isomorphic `fetch` API for both browser and fastboot + * + * node-fetch doesn't allow relative URLs, we patch it with Fastboot runtime info. + * Before instance-initializers Absolute URL is still not allowed, in this case + * node-fetch will throw error. + * `FastbootProtocol` and `FastbootHost` are re-set for every instance during its + * initializers through calling `setupFastboot`. + * + * @param {String|Object} input + * @param {Object} [options] + */ + exports.default = function fetch(input, options) { + if (typeof input === 'object') { + input.url = buildAbsoluteUrl(input.url, FastbootProtocol, FastbootHost); + } else { + input = buildAbsoluteUrl(input, FastbootProtocol, FastbootHost); + } + return nodeFetch(input, options); + }; + /** + * Assign the local protocol and host being used for building absolute URLs + * @private + */ + exports.setupFastboot = function setupFastboot(protocol, host) { + FastbootProtocol = protocol; + FastbootHost = host; + } + exports.Request = nodeFetch.Request; + exports.Headers = nodeFetch.Headers; + exports.Response = nodeFetch.Response; + exports.AbortController = AbortControllerPolyfill.AbortController; +}); + +define('fetch/ajax', ['exports'], function() { + throw new Error( + 'You included `fetch/ajax` but it was renamed to `ember-fetch/ajax`' + ); +}); diff --git a/test/fastboot-build-test.js b/test/fastboot-build-test.js index f2663323..b0637cbd 100644 --- a/test/fastboot-build-test.js +++ b/test/fastboot-build-test.js @@ -28,7 +28,7 @@ describe('it builds with ember-cli-fastboot', function() { it('builds into dist/ember-fetch/fetch-fastboot.js', function() { return app.runEmberCommand('build').then(function() { expect(app.filePath('dist/index.html')).to.be.a.file(); - expect(app.filePath('dist/ember-fetch/fastboot-fetch.js')).to.be.a.file(); + expect(app.filePath('dist/ember-fetch/fetch-fastboot.js')).to.be.a.file(); expect(app.filePath('dist/assets/dummy-fastboot.js')).to.be.a.file(); }); });