Merge pull request #10029 from meteor/isomorphic-fetch

Provide isomorphic implementation of WHATWG fetch() API.
This commit is contained in:
Ben Newman
2018-06-28 18:58:53 -04:00
committed by GitHub
18 changed files with 193 additions and 63 deletions

View File

@@ -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

View File

@@ -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');

View File

@@ -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');

View File

@@ -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');

View File

@@ -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();
});
}

View File

@@ -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");

View File

@@ -0,0 +1 @@
node_modules

View File

@@ -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.

View File

@@ -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=="
}
}
}

25
packages/fetch/README.md Normal file
View File

@@ -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"`).

6
packages/fetch/legacy.js Normal file
View File

@@ -0,0 +1,6 @@
require("whatwg-fetch");
exports.fetch = global.fetch;
exports.Headers = global.Headers;
exports.Request = global.Request;
exports.Response = global.Response;

4
packages/fetch/modern.js Normal file
View File

@@ -0,0 +1,4 @@
exports.fetch = global.fetch;
exports.Headers = global.Headers;
exports.Request = global.Request;
exports.Response = global.Response;

33
packages/fetch/package.js Normal file
View File

@@ -0,0 +1,33 @@
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("tests/main.js");
api.addAssets("tests/asset.json", ["client", "server"]);
});

21
packages/fetch/server.js Normal file
View File

@@ -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);

View File

@@ -0,0 +1,5 @@
{
"word": "oyez",
"times": 3,
"where": "SCOTUS"
}

View File

@@ -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);
});
});

View File

@@ -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() {

View File

@@ -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');