Skip to content

Commit

Permalink
Fix fetch module definition for Fastboot
Browse files Browse the repository at this point in the history
Define fetch module with a setupFastboot method export to set host and
protocol for every instance.

Rename public/fastboot-fetch.js to public/fetch-fastboot.js to avoid
lower version ember-fetch overwrite this file.

Overrides treeForPublic in index.js to only include public asset if top
level addon to avoid any future rename.
  • Loading branch information
xg-wang committed Dec 3, 2018
1 parent 9a1c3e3 commit 40f4ddb
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 63 deletions.
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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.

Expand Down
6 changes: 3 additions & 3 deletions fastboot/instance-initializers/setup-fetch.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
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');
const request = fastboot.get('request');
// 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 {
Expand Down
21 changes: 17 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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;
},

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
55 changes: 0 additions & 55 deletions public/fastboot-fetch.js

This file was deleted.

75 changes: 75 additions & 0 deletions public/fetch-fastboot.js
Original file line number Diff line number Diff line change
@@ -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`'
);
});
2 changes: 1 addition & 1 deletion test/fastboot-build-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
Expand Down

0 comments on commit 40f4ddb

Please sign in to comment.