From 24aba7b02db8fa0fef5c6d65cbf6a9e713f1a17e Mon Sep 17 00:00:00 2001 From: Jan Bobisud Date: Tue, 5 Jul 2022 11:22:36 +0200 Subject: [PATCH] Add `fetch` API globals --- .../fastboot/instance-initializers/fetch.js | 9 ++ .../test/request-details-test.js | 5 +- packages/fastboot/package.json | 4 +- packages/fastboot/src/sandbox.js | 88 +++++++++++++++++++ test-packages/basic-app/app/router.js | 1 + test-packages/basic-app/app/routes/fetch.js | 27 ++++++ .../basic-app/app/templates/fetch.hbs | 1 + .../basic-app/public/absolute-request.json | 3 + .../basic-app/public/absolute-url.json | 3 + .../public/path-relative-request.json | 3 + .../basic-app/public/path-relative-url.json | 3 + .../public/protocol-relative-request.json | 3 + .../public/protocol-relative-url.json | 3 + test-packages/basic-app/test/fetch-test.js | 40 +++++++++ yarn.lock | 30 +++++++ 15 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 packages/ember-cli-fastboot/fastboot/instance-initializers/fetch.js create mode 100644 test-packages/basic-app/app/routes/fetch.js create mode 100644 test-packages/basic-app/app/templates/fetch.hbs create mode 100644 test-packages/basic-app/public/absolute-request.json create mode 100644 test-packages/basic-app/public/absolute-url.json create mode 100644 test-packages/basic-app/public/path-relative-request.json create mode 100644 test-packages/basic-app/public/path-relative-url.json create mode 100644 test-packages/basic-app/public/protocol-relative-request.json create mode 100644 test-packages/basic-app/public/protocol-relative-url.json create mode 100644 test-packages/basic-app/test/fetch-test.js diff --git a/packages/ember-cli-fastboot/fastboot/instance-initializers/fetch.js b/packages/ember-cli-fastboot/fastboot/instance-initializers/fetch.js new file mode 100644 index 000000000..9fbcda4e5 --- /dev/null +++ b/packages/ember-cli-fastboot/fastboot/instance-initializers/fetch.js @@ -0,0 +1,9 @@ +function initialize(instance) { + let { request } = instance.lookup('service:fastboot'); + fetch.__fastbootRequest = request; +} + +export default { + name: 'fastboot:fetch', // `ember-fetch` addon registers as `fetch` + initialize, +}; diff --git a/packages/ember-cli-fastboot/test/request-details-test.js b/packages/ember-cli-fastboot/test/request-details-test.js index 672b397bb..cc0d5e2c9 100644 --- a/packages/ember-cli-fastboot/test/request-details-test.js +++ b/packages/ember-cli-fastboot/test/request-details-test.js @@ -1,5 +1,6 @@ 'use strict'; +const path = require('path'); const chai = require('chai'); const expect = chai.expect; const RSVP = require('rsvp'); @@ -11,8 +12,8 @@ function injectMiddlewareAddon(app) { pkg.devDependencies['body-parser'] = process.env.npm_package_devDependencies_body_parser; pkg.dependencies = pkg.dependencies || {}; - pkg.dependencies['fastboot-express-middleware'] = - process.env.npm_package_dependencies_fastboot_express_middleware; + pkg.dependencies['fastboot'] = `file:${path.resolve(__dirname, '../../fastboot')}` + pkg.dependencies['fastboot-express-middleware'] = `file:${path.resolve(__dirname, '../../fastboot-express-middleware')}` pkg['ember-addon'] = { paths: ['lib/post-middleware'], }; diff --git a/packages/fastboot/package.json b/packages/fastboot/package.json index 1779e03c0..c621d3b41 100644 --- a/packages/fastboot/package.json +++ b/packages/fastboot/package.json @@ -28,10 +28,12 @@ "postversion": "git push origin master --tags" }, "dependencies": { + "abortcontroller-polyfill": "^1.7.3", "chalk": "^4.1.2", "cookie": "^0.4.1", "debug": "^4.3.3", "jsdom": "^19.0.0", + "node-fetch": "^2.6.7", "resolve": "^1.22.0", "simple-dom": "^1.4.0", "source-map-support": "^0.5.21" @@ -79,4 +81,4 @@ "tokenRef": "GITHUB_AUTH" } } -} +} \ No newline at end of file diff --git a/packages/fastboot/src/sandbox.js b/packages/fastboot/src/sandbox.js index f39685df5..e462a0693 100644 --- a/packages/fastboot/src/sandbox.js +++ b/packages/fastboot/src/sandbox.js @@ -4,6 +4,9 @@ const chalk = require('chalk'); const vm = require('vm'); const sourceMapSupport = require('source-map-support'); +const httpRegex = /^https?:\/\//; +const protocolRelativeRegex = /^\/\//; + module.exports = class Sandbox { constructor(globals) { this.globals = globals; @@ -14,6 +17,7 @@ module.exports = class Sandbox { buildSandbox() { let console = this.buildWrappedConsole(); + let fetch = this.buildFetch(); let URL = require('url'); let globals = this.globals; @@ -28,6 +32,7 @@ module.exports = class Sandbox { // Convince jQuery not to assume it's in a browser module: { exports: {} }, }, + fetch, globals ); @@ -53,6 +58,89 @@ module.exports = class Sandbox { return wrappedConsole; } + buildFetch() { + let globals; + + if (globalThis.fetch) { + globals = { + fetch: globalThis.fetch, + Request: globalThis.Request, + Response: globalThis.Response, + Headers: globalThis.Headers, + AbortController: globalThis.AbortController, + }; + } else { + let nodeFetch = require('node-fetch'); + let { + AbortController, + abortableFetch, + } = require('abortcontroller-polyfill/dist/cjs-ponyfill'); + let { fetch, Request } = abortableFetch({ + fetch: nodeFetch, + Request: nodeFetch.Request, + }); + + globals = { + fetch, + Request, + Response: nodeFetch.Response, + Headers: nodeFetch.Headers, + AbortController, + }; + } + + let originalFetch = globals.fetch; + globals.fetch = function __fastbootFetch(input, init) { + input = globals.fetch.__fastbootBuildAbsoluteURL(input); + return originalFetch(input, init); + }; + + globals.fetch.__fastbootBuildAbsoluteURL = function __fastbootBuildAbsoluteURL(input) { + if (input && input.href) { + // WHATWG URL or Node.js Url Object + input = input.href; + } + + if (typeof input !== 'string') { + return input; + } + + if (protocolRelativeRegex.test(input)) { + let request = globals.fetch.__fastbootRequest; + let [protocol] = globals.fetch.__fastbootParseRequest(input, request); + input = `${protocol}//${input}`; + } else if (!httpRegex.test(input)) { + let request = globals.fetch.__fastbootRequest; + let [protocol, host] = globals.fetch.__fastbootParseRequest(input, request); + input = `${protocol}//${host}${input}`; + } + + return input; + }; + + globals.fetch.__fastbootParseRequest = function __fastbootParseRequest(url, request) { + if (!request) { + throw new Error( + `Using fetch with relative URL ${url}, but application instance has not been initialized yet.` + ); + } + + // Old Prember version is not sending protocol + const protocol = request.protocol === 'undefined:' ? 'http:' : request.protocol; + return [protocol, request.host]; + }; + + let OriginalRequest = globals.Request; + globals.Request = class __FastBootRequest extends OriginalRequest { + constructor(input, init) { + input = globals.fetch.__fastbootBuildAbsoluteURL(input); + super(input, init); + } + }; + + return globals; + } + runScript(script) { script.runInContext(this.context); } diff --git a/test-packages/basic-app/app/router.js b/test-packages/basic-app/app/router.js index f1f8fd3ed..19897eebc 100644 --- a/test-packages/basic-app/app/router.js +++ b/test-packages/basic-app/app/router.js @@ -16,4 +16,5 @@ Router.map(function() { this.route('echo-request-headers'); this.route('return-status-code-418'); this.route('metadata'); + this.route('fetch'); }); diff --git a/test-packages/basic-app/app/routes/fetch.js b/test-packages/basic-app/app/routes/fetch.js new file mode 100644 index 000000000..55b25f784 --- /dev/null +++ b/test-packages/basic-app/app/routes/fetch.js @@ -0,0 +1,27 @@ +import Route from '@ember/routing/route'; +import { assert } from '@ember/debug'; + +export default class FetchRoute extends Route { + beforeModel() { + assert('fetch is available', fetch); + assert('Request is available', Request); + assert('Response is available', Response); + assert('Headers is available', Headers); + assert('AbortController is available', AbortController); + } + + async model() { + let responses = await Promise.all([ + fetch('http://localhost:45678/absolute-url.json'), + fetch(new Request('http://localhost:45678/absolute-request.json')), + fetch('//localhost:45678/protocol-relative-url.json'), + fetch(new Request('//localhost:45678/protocol-relative-request.json')), + fetch('/path-relative-url.json'), + fetch(new Request('/path-relative-request.json')), + ]); + + responses = await Promise.all(responses.map((response) => response.json())); + + return responses.map((response) => response.response).join('|'); + } +} diff --git a/test-packages/basic-app/app/templates/fetch.hbs b/test-packages/basic-app/app/templates/fetch.hbs new file mode 100644 index 000000000..ab61809cf --- /dev/null +++ b/test-packages/basic-app/app/templates/fetch.hbs @@ -0,0 +1 @@ +{{@model}} \ No newline at end of file diff --git a/test-packages/basic-app/public/absolute-request.json b/test-packages/basic-app/public/absolute-request.json new file mode 100644 index 000000000..9e22df1aa --- /dev/null +++ b/test-packages/basic-app/public/absolute-request.json @@ -0,0 +1,3 @@ +{ + "response": "absolute-request" +} \ No newline at end of file diff --git a/test-packages/basic-app/public/absolute-url.json b/test-packages/basic-app/public/absolute-url.json new file mode 100644 index 000000000..5be641f21 --- /dev/null +++ b/test-packages/basic-app/public/absolute-url.json @@ -0,0 +1,3 @@ +{ + "response": "absolute-url" +} \ No newline at end of file diff --git a/test-packages/basic-app/public/path-relative-request.json b/test-packages/basic-app/public/path-relative-request.json new file mode 100644 index 000000000..56537c3f6 --- /dev/null +++ b/test-packages/basic-app/public/path-relative-request.json @@ -0,0 +1,3 @@ +{ + "response": "path-relative-request" +} \ No newline at end of file diff --git a/test-packages/basic-app/public/path-relative-url.json b/test-packages/basic-app/public/path-relative-url.json new file mode 100644 index 000000000..bd94d3de6 --- /dev/null +++ b/test-packages/basic-app/public/path-relative-url.json @@ -0,0 +1,3 @@ +{ + "response": "path-relative-url" +} \ No newline at end of file diff --git a/test-packages/basic-app/public/protocol-relative-request.json b/test-packages/basic-app/public/protocol-relative-request.json new file mode 100644 index 000000000..aeb56f25d --- /dev/null +++ b/test-packages/basic-app/public/protocol-relative-request.json @@ -0,0 +1,3 @@ +{ + "response": "protocol-relative-request" +} \ No newline at end of file diff --git a/test-packages/basic-app/public/protocol-relative-url.json b/test-packages/basic-app/public/protocol-relative-url.json new file mode 100644 index 000000000..ad4fd3664 --- /dev/null +++ b/test-packages/basic-app/public/protocol-relative-url.json @@ -0,0 +1,3 @@ +{ + "response": "protocol-relative-url" +} \ No newline at end of file diff --git a/test-packages/basic-app/test/fetch-test.js b/test-packages/basic-app/test/fetch-test.js new file mode 100644 index 000000000..29e8b97ad --- /dev/null +++ b/test-packages/basic-app/test/fetch-test.js @@ -0,0 +1,40 @@ +'use strict'; + +const RSVP = require('rsvp'); +const request = RSVP.denodeify(require('request')); +const expect = require('chai').use(require('chai-string')).expect; +const { startServer, stopServer } = require('../../test-libs'); + +describe('fetch', function () { + this.timeout(120000); + + before(function () { + return startServer(); + }); + + after(function () { + return stopServer(); + }); + + it('uses fetch', async () => { + const response = await request({ + url: 'http://localhost:45678/fetch', + headers: { + Accept: 'text/html', + }, + }); + + expect(response.statusCode).to.equal(200); + expect(response.headers['content-type']).to.equalIgnoreCase('text/html; charset=utf-8'); + expect(response.body).to.contain( + [ + 'absolute-url', + 'absolute-request', + 'protocol-relative-url', + 'protocol-relative-request', + 'path-relative-url', + 'path-relative-request', + ].join('|') + ); + }); +}); diff --git a/yarn.lock b/yarn.lock index 4880e0fc1..78a648e76 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3621,6 +3621,11 @@ abortcontroller-polyfill@^1.4.0: resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.4.0.tgz#0d5eb58e522a461774af8086414f68e1dda7a6c4" integrity sha512-3ZFfCRfDzx3GFjO6RAkYx81lPGpUS20ISxux9gLxuKnqafNcFQo59+IoZqpO2WvQlyc287B62HDnDdNYRmlvWA== +abortcontroller-polyfill@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.3.tgz#1b5b487bd6436b5b764fd52a612509702c3144b5" + integrity sha512-zetDJxd89y3X99Kvo4qFx8GKlt6GsvN3UcRZHwU6iFA/0KiOmhkTVhe8oRoTBiTVPZu09x3vCra47+w8Yz1+2Q== + accepts@~1.3.4, accepts@~1.3.5, accepts@~1.3.7: version "1.3.7" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.7.tgz#531bc726517a3b2b41f850021c6cc15eaab507cd" @@ -13595,6 +13600,13 @@ node-fetch@^2.3.0, node-fetch@^2.6.0: resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.0.tgz#e633456386d4aa55863f676a7ab0daa8fdecb0fd" integrity sha512-8dG4H5ujfvFiqDmVu9fQ5bOHUC15JMjMY/Zumv26oOvvVJjM67KF8koCWIabKQ1GJIa9r2mMZscBq/TbdOcmNA== +node-fetch@^2.6.7: + version "2.6.7" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.7.tgz#24de9fba827e3b4ae44dc8b20256a379160052ad" + integrity sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ== + dependencies: + whatwg-url "^5.0.0" + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -17074,6 +17086,11 @@ tr46@^3.0.0: dependencies: punycode "^2.1.1" +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + tree-sync@^1.2.2: version "1.4.0" resolved "https://registry.yarnpkg.com/tree-sync/-/tree-sync-1.4.0.tgz#314598d13abaf752547d9335b8f95d9a137100d6" @@ -17653,6 +17670,11 @@ wcwidth@^1.0.1: dependencies: defaults "^1.0.3" +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + webidl-conversions@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" @@ -17826,6 +17848,14 @@ whatwg-url@^10.0.0: tr46 "^3.0.0" webidl-conversions "^7.0.0" +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + whatwg-url@^7.0.0: version "7.1.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06"