Meteor Night
30 May 2018
Meteor Night
30 May 2018
Different Bundles for Different Folks
Meteor Night
30 May 2018
One Size Need Not Fit All
Meteor Night
30 May 2018
Running with Safety Scissors
Meteor Night
30 May 2018
The Evergreen Dream
"Our litmus test for whether a feature is worth supporting... is that if we start supporting a new language feature... like, say, classes, and you start writing code using classes, you should not have to rewrite that code when native support finally catches up." (10:15)
William Gibson (c. 1993)
@babel/preset-env
for configuration
Not only must you build multiple JavaScript and CSS bundles for different browsers, with different dependency graphs and compilation rules and webpack configurations, but your server must also be able to detect the capabilities of each visiting client, so that it can deliver the appropriate assets at runtime.
Testing a matrix of different browsers and application versions gets cumbersome quickly, so it's no surprise that responsible web developers would rather ship a single, well-tested bundle, and forget about taking advantage of modern features until legacy browsers have disappeared completely.
async
/await
async
/await
just standardized?Yes, and already almost 85% of the world has access to a browser with native async
/await
support!
async
/await
async
/await
was too strict of a requirement?
What if we aimed a bit lower?
In package.js
:
Package.onUse(api => {
api.mainModule("client.js", "client");
api.mainModule("server.js", "server");
});
The current way of dividing client and server logic
In package.js
:
Package.onUse(api => {
api.mainModule("modern.js", "web.browser");
api.mainModule("legacy.js", "legacy");
api.mainModule("server.js", "server");
});
The new way of dividing client logic between the modern and legacy bundles
{
"name": "your-app",
"meteor": {
"mainModule": {
"client": "client/main.js",
"server": "server/main.js"
}
}
}
In
package.json
:
No more imports/
directory! 🎉 🎈 ✨
{
"name": "your-app",
"meteor": {
"mainModule": {
"web.browser": "client/modern.js",
"legacy": "client/legacy.js",
"server": "server/main.js"
}
}
}
In package.json
:
Different entry points for modern/legacy
import { setMinimumBrowserVersions } from "meteor/modern-browsers";
import { setMinimumBrowserVersions } from "meteor/modern-browsers";
setMinimumBrowserVersions({
chrome: 49,
firefox: 45,
edge: 12,
ie: Infinity,
mobileSafari: [9, 2],
opera: 36,
safari: 9,
electron: 1,
}, );
import { setMinimumBrowserVersions } from "meteor/modern-browsers";
setMinimumBrowserVersions({
chrome: 49,
firefox: 45,
edge: 12,
ie: Infinity,
mobileSafari: [9, 2],
opera: 36,
safari: 9,
electron: 1,
}, "classes");
import { setMinimumBrowserVersions } from "meteor/modern-browsers";
setMinimumBrowserVersions({
chrome: 49,
firefox: 45,
edge: 12,
ie: Infinity, // Sorry, IE11.
mobileSafari: [9, 2],
opera: 36,
safari: 9,
electron: 1,
}, "classes");
import { setMinimumBrowserVersions } from "meteor/modern-browsers";
setMinimumBrowserVersions({
chrome: 49,
firefox: 45,
edge: 12,
ie: Infinity, // Sorry, IE11.
mobileSafari: [9, 2], // 9.2.0+
opera: 36,
safari: 9,
electron: 1,
}, "classes");
The minimum modern version for each browser is the maximum of all versions passed to setMinimumBrowserVersions
for that browser.
Meteor.isModern
if (Meteor.isModern) {
// Do something that's only safe in modern browsers
} else {
// Do something that's safe in all browsers
}
Meteor.isModern
if you can: modern
syntax can't be guarded, and both code paths remain in both bundles
babel-compiler
package
// What's wrong here?
let promise = new Promise((resolve, reject) => {
this.savePromise(promise, resolve, reject);
});
promise
variable uninitialized when used, but no TDZ error since
let
compiled to
var
// Silently "succeeds" with var promise = ...
var promise = new Promise((resolve, reject) => {
this.savePromise(promise, resolve, reject);
});
promise
variable uninitialized when used, but no TDZ error since
let
compiled to
var
// Silently "succeeds" with var promise = ...
var promise = new Promise((resolve, reject) => {
this.savePromise(promise, resolve, reject);
});
promise
variable uninitialized when used, but no TDZ error since
let
compiled to
var
// Correct implementation:
let resolve, reject;
let promise = new Promise((res, rej) => {
resolve = res;
reject = rej;
});
this.savePromise(promise, resolve, reject);
Inspired by true events.
require("core-js/modules/es6.object.is");
require("core-js/modules/es6.function.name");
require("core-js/modules/es6.number.is-finite");
require("core-js/modules/es6.number.is-nan");
require("core-js/modules/es7.array.flatten");
require("core-js/modules/es7.array.flat-map");
require("core-js/modules/es7.object.values");
require("core-js/modules/es7.object.entries");
require("core-js/modules/es7.object.get-own-property-descriptors");
require("core-js/modules/es7.string.pad-start");
require("core-js/modules/es7.string.pad-end");
setMinimumBrowserVersions({
chrome: 49,
edge: 12,
ie: 12,
firefox: 45,
mobileSafari: 10,
opera: 38,
safari: 10,
electron: [1, 6],
}, module.id);
ecmascript-runtime-client
:
Much less runtime feature detection!
regenerator-runtime
meteor --production --extra-packages bundle-visualizer
import()
s, CSS, and any other static assets!
If you can stop using an entire npm package on the client, that immediately benefits both modern and legacy users
meteor create --minimal my-minimal-app
fetch()
polyfillfetch()
polyfillPackage.describe({
name: "fetch",
version: "0.1.0",
summary: "Isomorphic modern/legacy/Node polyfill for WHATWG fetch()",
documentation: "README.md"
});
In
package.js
:
fetch()
polyfillPackage.describe({
name: "fetch",
version: "0.1.0",
summary: "Isomorphic modern/legacy/Node polyfill for WHATWG fetch()",
documentation: "README.md"
});
Package.onUse(function(api) {
});
In
package.js
:
fetch()
polyfillPackage.describe({
name: "fetch",
version: "0.1.0",
summary: "Isomorphic modern/legacy/Node polyfill for WHATWG fetch()",
documentation: "README.md"
});
Package.onUse(function(api) {
api.mainModule("modern.js", "web.browser");
api.mainModule("legacy.js", "legacy");
api.mainModule("server.js", "server");
});
In
package.js
:
fetch()
polyfillPackage.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.mainModule("modern.js", "web.browser");
api.mainModule("legacy.js", "legacy");
api.mainModule("server.js", "server");
});
In
package.js
:
fetch()
polyfillPackage.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");
});
In
package.js
:
fetch()
polyfillPackage.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");
api.export("fetch");
});
In
package.js
:
fetch()
polyfill
exports.fetch = global.fetch;
exports.Headers = global.Headers;
exports.Request = global.Request;
exports.Response = global.Response;
In
modern.js
:
No need for any polyfill; just re-export the global fetch() implementation!
fetch()
polyfill
require("whatwg-fetch");
exports.fetch = global.fetch;
exports.Headers = global.Headers;
exports.Request = global.Request;
exports.Response = global.Response;
In
legacy.js
:
Import
whatwg-fetch
, then re-export the global
fetch()
implementation
fetch()
polyfill
const fetch = require("node-fetch");
exports.fetch = fetch;
exports.Headers = fetch.Headers;
exports.Request = fetch.Request;
exports.Response = fetch.Response;
In
server.js
:
Import
node-fetch
, then re-export the global
fetch()
implementation
fetch()
polyfillconst 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");
In
server.js
:
fetch()
polyfillconst 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,
mobileSafari: [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);
In
server.js
:
fetch()
polyfillfetch()
polyfill
meteor add fetch
node_modules
node_modules
imports
directory
npm install
to link the package
into node_modules
node_modules
# Clone the offending package as a git submodule
git submodule add \
git@github.com:visionmedia/superagent.git \
imports/superagent
# Tweak imports/superagent, and commit any changes locally
# Creates a symbolic link at node_modules/superagent
meteor npm install imports/superagent
meteor update --release 1.7.0.1
meteor npm install @babel/runtime@latest
meteor npm install meteor-node-stubs@latest