From 06ea07d14b4526803a51ed1e0b4631471c88c7e4 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Thu, 10 May 2018 20:16:50 -0400 Subject: [PATCH 1/7] Provide isomorphic implementation of WHATWG fetch() API. --- packages/fetch/.npm/package/.gitignore | 1 + packages/fetch/.npm/package/README | 7 ++++ .../fetch/.npm/package/npm-shrinkwrap.json | 15 +++++++++ packages/fetch/README.md | 25 +++++++++++++++ packages/fetch/fetch-tests.js | 8 +++++ packages/fetch/legacy.js | 6 ++++ packages/fetch/modern.js | 4 +++ packages/fetch/package.js | 32 +++++++++++++++++++ packages/fetch/server.js | 21 ++++++++++++ 9 files changed, 119 insertions(+) create mode 100644 packages/fetch/.npm/package/.gitignore create mode 100644 packages/fetch/.npm/package/README create mode 100644 packages/fetch/.npm/package/npm-shrinkwrap.json create mode 100644 packages/fetch/README.md create mode 100644 packages/fetch/fetch-tests.js create mode 100644 packages/fetch/legacy.js create mode 100644 packages/fetch/modern.js create mode 100644 packages/fetch/package.js create mode 100644 packages/fetch/server.js diff --git a/packages/fetch/.npm/package/.gitignore b/packages/fetch/.npm/package/.gitignore new file mode 100644 index 0000000000..3c3629e647 --- /dev/null +++ b/packages/fetch/.npm/package/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/packages/fetch/.npm/package/README b/packages/fetch/.npm/package/README new file mode 100644 index 0000000000..3d492553a4 --- /dev/null +++ b/packages/fetch/.npm/package/README @@ -0,0 +1,7 @@ +This directory and the files immediately inside it are automatically generated +when you change this package's NPM dependencies. Commit the files in this +directory (npm-shrinkwrap.json, .gitignore, and this README) to source control +so that others run the same versions of sub-dependencies. + +You should NOT check in the node_modules directory that Meteor automatically +creates; if you are using git, the .gitignore file tells git to ignore it. diff --git a/packages/fetch/.npm/package/npm-shrinkwrap.json b/packages/fetch/.npm/package/npm-shrinkwrap.json new file mode 100644 index 0000000000..c9b411ca91 --- /dev/null +++ b/packages/fetch/.npm/package/npm-shrinkwrap.json @@ -0,0 +1,15 @@ +{ + "lockfileVersion": 1, + "dependencies": { + "node-fetch": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.1.2.tgz", + "integrity": "sha1-q4hOjn5X44qUR1POxwb3iNF2i7U=" + }, + "whatwg-fetch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-2.0.4.tgz", + "integrity": "sha512-dcQ1GWpOD/eEQ97k66aiEVpNnapVj90/+R+SXTPYGHpYBBypfKJEQjLrvMZ7YXbKm21gXd4NcuxUTjiv1YtLng==" + } + } +} diff --git a/packages/fetch/README.md b/packages/fetch/README.md new file mode 100644 index 0000000000..85385bb617 --- /dev/null +++ b/packages/fetch/README.md @@ -0,0 +1,25 @@ +# fetch +[Source code of released version](https://github.com/meteor/meteor/tree/master/packages/fetch) | [Source code of development version](https://github.com/meteor/meteor/tree/devel/packages/fetch) +*** + +Isomorphic polyfill for the [WHATWG `fetch()` API](https://fetch.spec.whatwg.org/). + +In [modern browsers](https://github.com/meteor/meteor/tree/release-1.7/packages/modern-browsers), +the native `fetch()` API can be used without a polyfill. In other words, +this package has almost no footprint in modern browsers. This package +[calls `setMinimumBrowserVersions`](./server.js) to enforce minimum modern +browser versions. However, `fetch()` has been supported natively by most +browsers for long enough that these minimum versions are unlikely to make +any difference in the `isModern` test, compared to more recent features +like `async` functions. + +In legacy browsers, the +[`whatwg-fetch`](http://npmjs.org/package/whatwg-fetch) polyfill is +used. Thanks to Meteor's modern/legacy system, this polyfill adds no weight +to the modern JS bundle. + +In Node, the [`node-fetch`](https://www.npmjs.com/package/node-fetch) +polyfill is used. Note: unlike the client polyfills, the Node polyfill +does not define the `fetch` function globally. However, any application or +package that depends on the Meteor `fetch` package can refer to `fetch` as +if it was a global function (or `import { fetch } from "meteor/fetch"`). diff --git a/packages/fetch/fetch-tests.js b/packages/fetch/fetch-tests.js new file mode 100644 index 0000000000..aa74ac9878 --- /dev/null +++ b/packages/fetch/fetch-tests.js @@ -0,0 +1,8 @@ +// Import Tinytest from the tinytest Meteor package. +import { Tinytest } from "meteor/tinytest"; + +// Write your tests here! +// Here is an example. +Tinytest.add('fetch - example', function (test) { + test.equal(typeof fetch, "function"); +}); diff --git a/packages/fetch/legacy.js b/packages/fetch/legacy.js new file mode 100644 index 0000000000..46a352725f --- /dev/null +++ b/packages/fetch/legacy.js @@ -0,0 +1,6 @@ +require("whatwg-fetch"); + +exports.fetch = global.fetch; +exports.Headers = global.Headers; +exports.Request = global.Request; +exports.Response = global.Response; diff --git a/packages/fetch/modern.js b/packages/fetch/modern.js new file mode 100644 index 0000000000..515ae512cb --- /dev/null +++ b/packages/fetch/modern.js @@ -0,0 +1,4 @@ +exports.fetch = global.fetch; +exports.Headers = global.Headers; +exports.Request = global.Request; +exports.Response = global.Response; diff --git a/packages/fetch/package.js b/packages/fetch/package.js new file mode 100644 index 0000000000..37477b66a6 --- /dev/null +++ b/packages/fetch/package.js @@ -0,0 +1,32 @@ +Package.describe({ + name: "fetch", + version: "0.1.0", + summary: "Isomorphic modern/legacy/Node polyfill for WHATWG fetch()", + documentation: "README.md" +}); + +Npm.depends({ + "node-fetch": "2.1.2", + "whatwg-fetch": "2.0.4" +}); + +Package.onUse(function(api) { + api.use("modules"); + api.use("modern-browsers"); + api.use("promise"); + + api.mainModule("modern.js", "web.browser"); + api.mainModule("legacy.js", "legacy"); + api.mainModule("server.js", "server"); + + // The other exports (Headers, Request, Response) can be imported + // explicitly from the "meteor/fetch" package. + api.export("fetch"); +}); + +Package.onTest(function(api) { + api.use("ecmascript"); + api.use("tinytest"); + api.use("fetch"); + api.mainModule("fetch-tests.js"); +}); diff --git a/packages/fetch/server.js b/packages/fetch/server.js new file mode 100644 index 0000000000..3574902554 --- /dev/null +++ b/packages/fetch/server.js @@ -0,0 +1,21 @@ +const fetch = require("node-fetch"); + +exports.fetch = fetch; +exports.Headers = fetch.Headers; +exports.Request = fetch.Request; +exports.Response = fetch.Response; + +const { setMinimumBrowserVersions } = require("meteor/modern-browsers"); + +// https://caniuse.com/#feat=fetch +setMinimumBrowserVersions({ + chrome: 42, + edge: 14, + firefox: 39, + mobile_safari: [10, 3], + opera: 29, + safari: [10, 1], + phantomjs: Infinity, + // https://github.com/Kilian/electron-to-chromium/blob/master/full-versions.js + electron: [0, 25], +}, module.id); From 2be9902a1e9d691f1a4b198496cc83532d0757b9 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Thu, 10 May 2018 20:17:26 -0400 Subject: [PATCH 2/7] Switch dynamic-import from HTTP to fetch. --- packages/dynamic-import/client.js | 36 +++++++++++++++++------------- packages/dynamic-import/package.js | 4 ++-- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/packages/dynamic-import/client.js b/packages/dynamic-import/client.js index 7b71af3f18..5dcfa1c28a 100644 --- a/packages/dynamic-import/client.js +++ b/packages/dynamic-import/client.js @@ -1,6 +1,5 @@ var Module = module.constructor; var cache = require("./cache.js"); -var HTTP = require("meteor/http").HTTP; var meteorInstall = require("meteor/modules").meteorInstall; // Call module.dynamicImport(id) to fetch a module and any/all of its @@ -120,21 +119,26 @@ exports.setSecretKey = function (key) { var fetchURL = require("./common.js").fetchURL; function fetchMissing(missingTree) { - return new Promise(function (resolve, reject) { - // If the hostname of the URL returned by Meteor.absoluteUrl differs - // from location.host, then we'll be making a cross-origin request - // here, but that's fine because the dynamic-import server sets - // appropriate CORS headers to enable fetching dynamic modules from - // any origin. Browsers that check CORS do so by sending an additional - // preflight OPTIONS request, which may add latency to the first - // dynamic import() request, so it's a good idea for ROOT_URL to match - // location.host if possible, though not strictly necessary. - HTTP.call("POST", Meteor.absoluteUrl(fetchURL), { - query: secretKey ? "key=" + secretKey : void 0, - data: missingTree - }, function (error, result) { - error ? reject(error) : resolve(result.data); - }); + // If the hostname of the URL returned by Meteor.absoluteUrl differs + // from location.host, then we'll be making a cross-origin request here, + // but that's fine because the dynamic-import server sets appropriate + // CORS headers to enable fetching dynamic modules from any + // origin. Browsers that check CORS do so by sending an additional + // preflight OPTIONS request, which may add latency to the first dynamic + // import() request, so it's a good idea for ROOT_URL to match + // location.host if possible, though not strictly necessary. + var url = Meteor.absoluteUrl(fetchURL); + + if (secretKey) { + url += "key=" + secretKey; + } + + return fetch(url, { + method: "POST", + body: JSON.stringify(missingTree) + }).then(function (res) { + if (! res.ok) throw res; + return res.json(); }); } diff --git a/packages/dynamic-import/package.js b/packages/dynamic-import/package.js index dd99bbd177..d687530144 100644 --- a/packages/dynamic-import/package.js +++ b/packages/dynamic-import/package.js @@ -1,6 +1,6 @@ Package.describe({ name: "dynamic-import", - version: "0.4.1", + version: "0.5.0", summary: "Runtime support for Meteor 1.5 dynamic import(...) syntax", documentation: "README.md" }); @@ -11,7 +11,7 @@ Package.onUse(function (api) { api.use("modules"); api.use("promise"); - api.use("http"); + api.use("fetch"); api.use("modern-browsers"); api.mainModule("client.js", "client"); From 6e4b7b4d3d2d8c9e2e685bc3565edd509cf8b1e3 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Thu, 10 May 2018 20:17:56 -0400 Subject: [PATCH 3/7] Remove unused http and random dependencies from autoupdate. --- packages/autoupdate/package.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/autoupdate/package.js b/packages/autoupdate/package.js index a6a5918bad..576cb295e6 100644 --- a/packages/autoupdate/package.js +++ b/packages/autoupdate/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "Update the client when new client code is available", - version: '1.4.0' + version: '1.4.1' }); Package.onUse(function (api) { @@ -20,8 +20,6 @@ Package.onUse(function (api) { 'mongo', ], ['client', 'server']); - api.use(['http', 'random'], 'web.cordova'); - api.addFiles('autoupdate_server.js', 'server'); api.addFiles('autoupdate_client.js', 'web.browser'); api.addFiles('autoupdate_cordova.js', 'web.cordova'); From 4363c4d0ec9727e86970f677c37ffe600f5f084c Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Mon, 25 Jun 2018 17:18:57 -0400 Subject: [PATCH 4/7] Removed unused http dependency from ddp-client. --- packages/ddp-client/package.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/ddp-client/package.js b/packages/ddp-client/package.js index 1605d90820..766e1b693d 100644 --- a/packages/ddp-client/package.js +++ b/packages/ddp-client/package.js @@ -1,6 +1,6 @@ Package.describe({ summary: "Meteor's latency-compensated distributed data client", - version: '2.3.2', + version: '2.3.3', documentation: null }); @@ -55,8 +55,6 @@ Package.onTest((api) => { 'check' ]); - api.use('http', 'client'); - api.addFiles('test/stub_stream.js'); api.addFiles('test/livedata_connection_tests.js'); api.addFiles('test/livedata_tests.js'); From a024544b45f5e30386d87b31e118331602221e77 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Mon, 25 Jun 2018 17:27:43 -0400 Subject: [PATCH 5/7] Switch appcache tests from HTTP to fetch(). --- packages/appcache/appcache_tests-client.js | 30 +++++++++++----------- packages/appcache/package.js | 2 +- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/packages/appcache/appcache_tests-client.js b/packages/appcache/appcache_tests-client.js index 741f360485..b492f80419 100644 --- a/packages/appcache/appcache_tests-client.js +++ b/packages/appcache/appcache_tests-client.js @@ -1,28 +1,28 @@ const manifestUrl = '/app.manifest'; -const appcacheTest = (name, cb) => { - Tinytest.addAsync(`appcache - ${name}`, (test, next) => { - HTTP.get(manifestUrl, (err, res) => { - err ? test.fail(err) : cb(test, res); - next(); - }); +function appcacheTest(name, cb) { + Tinytest.addAsync(`appcache - ${name}`, test => { + return fetch(manifestUrl).then( + res => cb(test, res), + err => test.fail(err) + ); }); -}; +} // Verify that the code status of the HTTP response is "OK" appcacheTest('presence', (test, manifest) => - test.equal(manifest.statusCode, 200, 'manifest not served')); + test.equal(manifest.status, 200, 'manifest not served')); // Verify the content-type HTTP header appcacheTest('content type', (test, manifest) => - test.equal(manifest.headers['content-type'], 'text/cache-manifest')); + test.equal(manifest.headers.get('content-type'), 'text/cache-manifest')); // Verify that each section header is only set once. -appcacheTest('sections uniqueness', (test, manifest) => { - const { content } = manifest; +appcacheTest('sections uniqueness', async (test, manifest) => { + const content = await manifest.text(); const mandatorySectionHeaders = ['CACHE:', 'NETWORK:', 'FALLBACK:']; const optionalSectionHeaders = ['SETTINGS']; const allSectionHeaders = [ @@ -46,8 +46,8 @@ appcacheTest('sections uniqueness', (test, manifest) => { // regular expressions. Regular expressions matches malformed URIs but that's // not what we're trying to catch here (the user is free to add its own content // in the manifest -- even malformed). -appcacheTest('sections validity', (test, manifest) => { - const lines = manifest.content.split('\n'); +appcacheTest('sections validity', async (test, manifest) => { + const lines = (await manifest.text()).split('\n'); let i = 0; let currentRegex = null; let line = null; @@ -112,7 +112,7 @@ appcacheTest('sections validity', (test, manifest) => { // are present in the network section of the manifest. The `appcache` package // also automatically add the manifest (`app.manifest`) add the star symbol to // this list and therefore we also check the presence of these two elements. -appcacheTest('network section content', (test, manifest) => { +appcacheTest('network section content', async (test, manifest) => { const shouldBePresentInNetworkSection = [ "/app.manifest", "/online/", @@ -120,7 +120,7 @@ appcacheTest('network section content', (test, manifest) => { "/largedata.json", "*" ]; - const lines = manifest.content.split('\n'); + const lines = (await manifest.text()).split('\n'); const startNetworkSection = lines.indexOf('NETWORK:'); // We search the end of the 'NETWORK:' section by looking at the beginning diff --git a/packages/appcache/package.js b/packages/appcache/package.js index 17aaa0cdd3..100f5e912b 100644 --- a/packages/appcache/package.js +++ b/packages/appcache/package.js @@ -15,7 +15,7 @@ Package.onUse(api => { Package.onTest(api => { api.use('tinytest'); api.use('appcache'); - api.use('http', 'client'); + api.use('fetch'); api.use('webapp', 'server'); api.addFiles('appcache_tests-server.js', 'server'); api.addFiles('appcache_tests-client.js', 'client'); From a20f4e74d82c7b84479016bf255f3c309201fb9a Mon Sep 17 00:00:00 2001 From: James Burgess Date: Wed, 27 Jun 2018 18:37:19 +0400 Subject: [PATCH 6/7] Replace http with fetch in bundle-visualizer (#10031) --- packages/non-core/bundle-visualizer/client.js | 38 +++++++++---------- .../non-core/bundle-visualizer/package.js | 4 +- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/packages/non-core/bundle-visualizer/client.js b/packages/non-core/bundle-visualizer/client.js index 71925a55a6..b96adc5443 100644 --- a/packages/non-core/bundle-visualizer/client.js +++ b/packages/non-core/bundle-visualizer/client.js @@ -1,5 +1,4 @@ import { Meteor } from "meteor/meteor"; -import { HTTP } from "meteor/http"; import { classPrefix, methodNameStats, @@ -13,7 +12,7 @@ Meteor.startup(() => { import("./sunburst.js").then(s => main(s.Sunburst)); }); -function main(builder) { +async function main(builder) { const { container, mask } = frameStage(); document.body.appendChild(mask); @@ -21,26 +20,23 @@ function main(builder) { // Always match the protocol (http or https) and the domain:port of the // current page. - const url = "//" + location.host + methodNameStats; + const url = [ + "//" + + location.host + + methodNameStats + + "?cacheBuster=" + + Math.random().toString(36).slice(2) + ].join(); - HTTP.call("GET", url, { - params: { - cacheBuster: Math.random().toString(36).slice(2) - } - }, (error, { data }) => { - if (error) { - console.error([ - packageName + ": Couldn't load stats for visualization.", - "Are you using standard-minifier-js >= 2.1.0 as the minifier?", - ].join(" ")); - return; - } - - // Load the JSON, which is `d3-hierarchy` digestible. - if (data) { - new builder({ container }).loadJson(data); - } - }); + try { + const data = await fetch(url, { method: "GET" }); + new builder({ container }).loadJson(await data.json()) + } catch (err) { + console.error([ + packageName + ": Couldn't load stats for visualization.", + "Are you using standard-minifier-js >= 2.1.0 as the minifier?", + ].join(" ")) + } } function frameStage() { diff --git a/packages/non-core/bundle-visualizer/package.js b/packages/non-core/bundle-visualizer/package.js index cdf141385b..7ba97d1373 100644 --- a/packages/non-core/bundle-visualizer/package.js +++ b/packages/non-core/bundle-visualizer/package.js @@ -1,5 +1,5 @@ Package.describe({ - version: '1.2.1', + version: '1.2.2', summary: 'Meteor bundle analysis and visualization.', documentation: 'README.md', }); @@ -18,7 +18,7 @@ Package.onUse(function(api) { api.use([ 'ecmascript', 'dynamic-import', - 'http', + 'fetch', 'webapp', ]); api.mainModule('server.js', 'server'); From 9abd2ce4c8aeaf988199bdd4ab3cab24debb5921 Mon Sep 17 00:00:00 2001 From: Ben Newman Date: Thu, 28 Jun 2018 18:47:36 -0400 Subject: [PATCH 7/7] Add another basic fetch() test. --- packages/fetch/fetch-tests.js | 8 -------- packages/fetch/package.js | 3 ++- packages/fetch/tests/asset.json | 5 +++++ packages/fetch/tests/main.js | 17 +++++++++++++++++ 4 files changed, 24 insertions(+), 9 deletions(-) delete mode 100644 packages/fetch/fetch-tests.js create mode 100644 packages/fetch/tests/asset.json create mode 100644 packages/fetch/tests/main.js diff --git a/packages/fetch/fetch-tests.js b/packages/fetch/fetch-tests.js deleted file mode 100644 index aa74ac9878..0000000000 --- a/packages/fetch/fetch-tests.js +++ /dev/null @@ -1,8 +0,0 @@ -// Import Tinytest from the tinytest Meteor package. -import { Tinytest } from "meteor/tinytest"; - -// Write your tests here! -// Here is an example. -Tinytest.add('fetch - example', function (test) { - test.equal(typeof fetch, "function"); -}); diff --git a/packages/fetch/package.js b/packages/fetch/package.js index 37477b66a6..d6abac6032 100644 --- a/packages/fetch/package.js +++ b/packages/fetch/package.js @@ -28,5 +28,6 @@ Package.onTest(function(api) { api.use("ecmascript"); api.use("tinytest"); api.use("fetch"); - api.mainModule("fetch-tests.js"); + api.mainModule("tests/main.js"); + api.addAssets("tests/asset.json", ["client", "server"]); }); diff --git a/packages/fetch/tests/asset.json b/packages/fetch/tests/asset.json new file mode 100644 index 0000000000..01a464e52a --- /dev/null +++ b/packages/fetch/tests/asset.json @@ -0,0 +1,5 @@ +{ + "word": "oyez", + "times": 3, + "where": "SCOTUS" +} diff --git a/packages/fetch/tests/main.js b/packages/fetch/tests/main.js new file mode 100644 index 0000000000..90efecab38 --- /dev/null +++ b/packages/fetch/tests/main.js @@ -0,0 +1,17 @@ +import { Tinytest } from "meteor/tinytest"; + +Tinytest.add("fetch - sanity", function (test) { + test.equal(typeof fetch, "function"); +}); + +Tinytest.addAsync("fetch - asset", function (test) { + return fetch( + Meteor.absoluteUrl("/packages/local-test_fetch/tests/asset.json") + ).then(res => { + if (! res.ok) throw res; + return res.json(); + }).then(json => { + test.equal(json.word, "oyez"); + test.equal(json.times, 3); + }); +});