@tdb/web
Version:
Common condiguration for serving a web-site and testing web-based UI components.
818 lines (676 loc) • 26.6 kB
JavaScript
"use strict";
var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault");
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/slicedToArray"));
var _typeof2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/typeof"));
var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs2/regenerator"));
var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/asyncToGenerator"));
var _objectSpread2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/objectSpread"));
var _set = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/set"));
var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/classCallCheck"));
var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/createClass"));
var _defineProperty2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/defineProperty"));
var _url2 = require("url");
var _EventEmitter = _interopRequireDefault(require("../EventEmitter"));
var _shallowEquals = _interopRequireDefault(require("../shallow-equals"));
var _pQueue = _interopRequireDefault(require("../p-queue"));
var _utils = require("../utils");
var _ = require("./");
/* global __NEXT_DATA__ */
var Router =
/*#__PURE__*/
function () {
function Router(_pathname, _query, _as2) {
var _this = this;
var _ref = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {},
initialProps = _ref.initialProps,
pageLoader = _ref.pageLoader,
App = _ref.App,
Component = _ref.Component,
ErrorComponent = _ref.ErrorComponent,
err = _ref.err;
(0, _classCallCheck2.default)(this, Router);
(0, _defineProperty2.default)(this, "onPopState", function (e) {
if (!e.state) {
// We get state as undefined for two reasons.
// 1. With older safari (< 8) and older chrome (< 34)
// 2. When the URL changed with #
//
// In the both cases, we don't need to proceed and change the route.
// (as it's already changed)
// But we can simply replace the state with the new changes.
// Actually, for (1) we don't need to nothing. But it's hard to detect that event.
// So, doing the following for (1) does no harm.
var pathname = _this.pathname,
query = _this.query;
_this.changeState('replaceState', (0, _url2.format)({
pathname: pathname,
query: query
}), (0, _utils.getURL)());
return;
} // If the downstream application returns falsy, return.
// They will then be responsible for handling the event.
if (!_this._beforePopState(e.state)) {
return;
}
var _e$state = e.state,
url = _e$state.url,
as = _e$state.as,
options = _e$state.options;
if (process.env.NODE_ENV !== 'production') {
if (typeof url === 'undefined' || typeof as === 'undefined') {
console.warn('`popstate` event triggered but `event.state` did not have `url` or `as` https://err.sh/zeit/next.js/popstate-state-empty');
}
}
_this.replace(url, as, options);
});
// represents the current component key
this.route = toRoute(_pathname); // set up the component cache (by route keys)
this.components = {}; // We should not keep the cache, if there's an error
// Otherwise, this cause issues when when going back and
// come again to the errored page.
if (Component !== ErrorComponent) {
this.components[this.route] = {
Component: Component,
props: initialProps,
err: err
};
}
this.components['/_app'] = {
Component: App // Backwards compat for Router.router.events
// TODO: Should be remove the following major version as it was never documented
};
this.events = Router.events;
this.pageLoader = pageLoader;
this.prefetchQueue = new _pQueue.default({
concurrency: 2
});
this.ErrorComponent = ErrorComponent;
this.pathname = _pathname;
this.query = _query;
this.asPath = _as2;
this.subscriptions = new _set.default();
this.componentLoadCancel = null;
this._beforePopState = function () {
return true;
};
if (typeof window !== 'undefined') {
// in order for `e.state` to work on the `onpopstate` event
// we have to register the initial route upon initialization
this.changeState('replaceState', (0, _url2.format)({
pathname: _pathname,
query: _query
}), (0, _utils.getURL)());
window.addEventListener('popstate', this.onPopState);
}
}
(0, _createClass2.default)(Router, [{
key: "update",
value: function update(route, Component) {
var data = this.components[route];
if (!data) {
throw new Error("Cannot update unavailable route: ".concat(route));
}
var newData = (0, _objectSpread2.default)({}, data, {
Component: Component
});
this.components[route] = newData; // pages/_app.js updated
if (route === '/_app') {
this.notify(this.components[this.route]);
return;
}
if (route === this.route) {
this.notify(newData);
}
}
}, {
key: "reload",
value: function () {
var _reload = (0, _asyncToGenerator2.default)(
/*#__PURE__*/
_regenerator.default.mark(function _callee(route) {
var pathname, query, url, as, routeInfo, error;
return _regenerator.default.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
delete this.components[route];
this.pageLoader.clearCache(route);
if (!(route !== this.route)) {
_context.next = 4;
break;
}
return _context.abrupt("return");
case 4:
pathname = this.pathname, query = this.query;
url = window.location.href; // This makes sure we only use pathname + query + hash, to mirror `asPath` coming from the server.
as = window.location.pathname + window.location.search + window.location.hash;
Router.events.emit('routeChangeStart', url);
_context.next = 10;
return this.getRouteInfo(route, pathname, query, as);
case 10:
routeInfo = _context.sent;
error = routeInfo.error;
if (!(error && error.cancelled)) {
_context.next = 14;
break;
}
return _context.abrupt("return");
case 14:
this.notify(routeInfo);
if (!error) {
_context.next = 18;
break;
}
Router.events.emit('routeChangeError', error, url);
throw error;
case 18:
Router.events.emit('routeChangeComplete', url);
case 19:
case "end":
return _context.stop();
}
}
}, _callee, this);
}));
return function reload(_x) {
return _reload.apply(this, arguments);
};
}()
}, {
key: "back",
value: function back() {
window.history.back();
}
}, {
key: "push",
value: function push(url) {
var as = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : url;
var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
return this.change('pushState', url, as, options);
}
}, {
key: "replace",
value: function replace(url) {
var as = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : url;
var options = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {};
return this.change('replaceState', url, as, options);
}
}, {
key: "change",
value: function () {
var _change = (0, _asyncToGenerator2.default)(
/*#__PURE__*/
_regenerator.default.mark(function _callee2(method, _url, _as, options) {
var url, as, _parse, asPathname, asQuery, _parse2, pathname, query, route, _options$shallow, shallow, routeInfo, _routeInfo, error, hash;
return _regenerator.default.wrap(function _callee2$(_context2) {
while (1) {
switch (_context2.prev = _context2.next) {
case 0:
// If url and as provided as an object representation,
// we'll format them into the string version here.
url = (0, _typeof2.default)(_url) === 'object' ? (0, _url2.format)(_url) : _url;
as = (0, _typeof2.default)(_as) === 'object' ? (0, _url2.format)(_as) : _as; // Add the ending slash to the paths. So, we can serve the
// "<page>/index.html" directly for the SSR page.
if (__NEXT_DATA__.nextExport) {
as = (0, _._rewriteUrlForNextExport)(as);
}
this.abortComponentLoad(as); // If the url change is only related to a hash change
// We should not proceed. We should only change the state.
if (!this.onlyAHashChange(as)) {
_context2.next = 10;
break;
}
Router.events.emit('hashChangeStart', as);
this.changeState(method, url, as);
this.scrollToHash(as);
Router.events.emit('hashChangeComplete', as);
return _context2.abrupt("return", true);
case 10:
_parse = (0, _url2.parse)(as, true), asPathname = _parse.pathname, asQuery = _parse.query;
_parse2 = (0, _url2.parse)(url, true), pathname = _parse2.pathname, query = _parse2.query; // If asked to change the current URL we should reload the current page
// (not location.reload() but reload getInitialProps and other Next.js stuffs)
// We also need to set the method = replaceState always
// as this should not go into the history (That's how browsers work)
if (!this.urlIsNew(asPathname, asQuery)) {
method = 'replaceState';
}
route = toRoute(pathname);
_options$shallow = options.shallow, shallow = _options$shallow === void 0 ? false : _options$shallow;
routeInfo = null;
Router.events.emit('routeChangeStart', as); // If shallow === false and other conditions met, we reuse the
// existing routeInfo for this route.
// Because of this, getInitialProps would not run.
if (!(shallow && this.isShallowRoutingPossible(route))) {
_context2.next = 21;
break;
}
routeInfo = this.components[route];
_context2.next = 24;
break;
case 21:
_context2.next = 23;
return this.getRouteInfo(route, pathname, query, as);
case 23:
routeInfo = _context2.sent;
case 24:
_routeInfo = routeInfo, error = _routeInfo.error;
if (!(error && error.cancelled)) {
_context2.next = 27;
break;
}
return _context2.abrupt("return", false);
case 27:
Router.events.emit('beforeHistoryChange', as);
this.changeState(method, url, as, options);
hash = window.location.hash.substring(1);
this.set(route, pathname, query, as, (0, _objectSpread2.default)({}, routeInfo, {
hash: hash
}));
if (!error) {
_context2.next = 34;
break;
}
Router.events.emit('routeChangeError', error, as);
throw error;
case 34:
Router.events.emit('routeChangeComplete', as);
return _context2.abrupt("return", true);
case 36:
case "end":
return _context2.stop();
}
}
}, _callee2, this);
}));
return function change(_x2, _x3, _x4, _x5) {
return _change.apply(this, arguments);
};
}()
}, {
key: "changeState",
value: function changeState(method, url, as) {
var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};
if (process.env.NODE_ENV !== 'production') {
if (typeof window.history === 'undefined') {
console.error("Warning: window.history is not available.");
return;
}
if (typeof window.history[method] === 'undefined') {
console.error("Warning: window.history.".concat(method, " is not available"));
return;
}
}
if (method !== 'pushState' || (0, _utils.getURL)() !== as) {
window.history[method]({
url: url,
as: as,
options: options
}, null, as);
}
}
}, {
key: "getRouteInfo",
value: function () {
var _getRouteInfo = (0, _asyncToGenerator2.default)(
/*#__PURE__*/
_regenerator.default.mark(function _callee3(route, pathname, query, as) {
var routeInfo, _routeInfo2, Component, ctx, _Component, _ctx;
return _regenerator.default.wrap(function _callee3$(_context3) {
while (1) {
switch (_context3.prev = _context3.next) {
case 0:
routeInfo = null;
_context3.prev = 1;
routeInfo = this.components[route];
if (routeInfo) {
_context3.next = 8;
break;
}
_context3.next = 6;
return this.fetchComponent(route, as);
case 6:
_context3.t0 = _context3.sent;
routeInfo = {
Component: _context3.t0
};
case 8:
_routeInfo2 = routeInfo, Component = _routeInfo2.Component;
if (!(typeof Component !== 'function')) {
_context3.next = 11;
break;
}
throw new Error("The default export is not a React Component in page: \"".concat(pathname, "\""));
case 11:
ctx = {
pathname: pathname,
query: query,
asPath: as
};
_context3.next = 14;
return this.getInitialProps(Component, ctx);
case 14:
routeInfo.props = _context3.sent;
this.components[route] = routeInfo;
_context3.next = 40;
break;
case 18:
_context3.prev = 18;
_context3.t1 = _context3["catch"](1);
if (!(_context3.t1.code === 'PAGE_LOAD_ERROR')) {
_context3.next = 24;
break;
}
// If we can't load the page it could be one of following reasons
// 1. Page doesn't exists
// 2. Page does exist in a different zone
// 3. Internal error while loading the page
// So, doing a hard reload is the proper way to deal with this.
window.location.href = as; // Changing the URL doesn't block executing the current code path.
// So, we need to mark it as a cancelled error and stop the routing logic.
_context3.t1.cancelled = true;
return _context3.abrupt("return", {
error: _context3.t1
});
case 24:
if (!_context3.t1.cancelled) {
_context3.next = 26;
break;
}
return _context3.abrupt("return", {
error: _context3.t1
});
case 26:
_Component = this.ErrorComponent;
routeInfo = {
Component: _Component,
err: _context3.t1
};
_ctx = {
err: _context3.t1,
pathname: pathname,
query: query
};
_context3.prev = 29;
_context3.next = 32;
return this.getInitialProps(_Component, _ctx);
case 32:
routeInfo.props = _context3.sent;
_context3.next = 39;
break;
case 35:
_context3.prev = 35;
_context3.t2 = _context3["catch"](29);
console.error('Error in error page `getInitialProps`: ', _context3.t2);
routeInfo.props = {};
case 39:
routeInfo.error = _context3.t1;
case 40:
return _context3.abrupt("return", routeInfo);
case 41:
case "end":
return _context3.stop();
}
}
}, _callee3, this, [[1, 18], [29, 35]]);
}));
return function getRouteInfo(_x6, _x7, _x8, _x9) {
return _getRouteInfo.apply(this, arguments);
};
}()
}, {
key: "set",
value: function set(route, pathname, query, as, data) {
this.route = route;
this.pathname = pathname;
this.query = query;
this.asPath = as;
this.notify(data);
}
}, {
key: "beforePopState",
value: function beforePopState(cb) {
this._beforePopState = cb;
}
}, {
key: "onlyAHashChange",
value: function onlyAHashChange(as) {
if (!this.asPath) return false;
var _this$asPath$split = this.asPath.split('#'),
_this$asPath$split2 = (0, _slicedToArray2.default)(_this$asPath$split, 2),
oldUrlNoHash = _this$asPath$split2[0],
oldHash = _this$asPath$split2[1];
var _as$split = as.split('#'),
_as$split2 = (0, _slicedToArray2.default)(_as$split, 2),
newUrlNoHash = _as$split2[0],
newHash = _as$split2[1]; // Makes sure we scroll to the provided hash if the url/hash are the same
if (newHash && oldUrlNoHash === newUrlNoHash && oldHash === newHash) {
return true;
} // If the urls are change, there's more than a hash change
if (oldUrlNoHash !== newUrlNoHash) {
return false;
} // If the hash has changed, then it's a hash only change.
// This check is necessary to handle both the enter and
// leave hash === '' cases. The identity case falls through
// and is treated as a next reload.
return oldHash !== newHash;
}
}, {
key: "scrollToHash",
value: function scrollToHash(as) {
var _as$split3 = as.split('#'),
_as$split4 = (0, _slicedToArray2.default)(_as$split3, 2),
hash = _as$split4[1]; // Scroll to top if the hash is just `#` with no value
if (hash === '') {
window.scrollTo(0, 0);
return;
} // First we check if the element by id is found
var idEl = document.getElementById(hash);
if (idEl) {
idEl.scrollIntoView();
return;
} // If there's no element with the id, we check the `name` property
// To mirror browsers
var nameEl = document.getElementsByName(hash)[0];
if (nameEl) {
nameEl.scrollIntoView();
}
}
}, {
key: "urlIsNew",
value: function urlIsNew(pathname, query) {
return this.pathname !== pathname || !(0, _shallowEquals.default)(query, this.query);
}
}, {
key: "isShallowRoutingPossible",
value: function isShallowRoutingPossible(route) {
return (// If there's cached routeInfo for the route.
Boolean(this.components[route]) && // If the route is already rendered on the screen.
this.route === route
);
}
}, {
key: "prefetch",
value: function () {
var _prefetch = (0, _asyncToGenerator2.default)(
/*#__PURE__*/
_regenerator.default.mark(function _callee4(url) {
var _this2 = this;
var _parse3, pathname, route;
return _regenerator.default.wrap(function _callee4$(_context4) {
while (1) {
switch (_context4.prev = _context4.next) {
case 0:
if (!(process.env.NODE_ENV === 'development')) {
_context4.next = 2;
break;
}
return _context4.abrupt("return");
case 2:
_parse3 = (0, _url2.parse)(url), pathname = _parse3.pathname;
route = toRoute(pathname);
return _context4.abrupt("return", this.prefetchQueue.add(function () {
return _this2.fetchRoute(route);
}));
case 5:
case "end":
return _context4.stop();
}
}
}, _callee4, this);
}));
return function prefetch(_x10) {
return _prefetch.apply(this, arguments);
};
}()
}, {
key: "fetchComponent",
value: function () {
var _fetchComponent = (0, _asyncToGenerator2.default)(
/*#__PURE__*/
_regenerator.default.mark(function _callee5(route, as) {
var cancelled, cancel, Component, error;
return _regenerator.default.wrap(function _callee5$(_context5) {
while (1) {
switch (_context5.prev = _context5.next) {
case 0:
cancelled = false;
cancel = this.componentLoadCancel = function () {
cancelled = true;
};
_context5.next = 4;
return this.fetchRoute(route);
case 4:
Component = _context5.sent;
if (!cancelled) {
_context5.next = 9;
break;
}
error = new Error("Abort fetching component for route: \"".concat(route, "\""));
error.cancelled = true;
throw error;
case 9:
if (cancel === this.componentLoadCancel) {
this.componentLoadCancel = null;
}
return _context5.abrupt("return", Component);
case 11:
case "end":
return _context5.stop();
}
}
}, _callee5, this);
}));
return function fetchComponent(_x11, _x12) {
return _fetchComponent.apply(this, arguments);
};
}()
}, {
key: "getInitialProps",
value: function () {
var _getInitialProps = (0, _asyncToGenerator2.default)(
/*#__PURE__*/
_regenerator.default.mark(function _callee6(Component, ctx) {
var cancelled, cancel, App, props, err;
return _regenerator.default.wrap(function _callee6$(_context6) {
while (1) {
switch (_context6.prev = _context6.next) {
case 0:
cancelled = false;
cancel = function cancel() {
cancelled = true;
};
this.componentLoadCancel = cancel;
App = this.components['/_app'].Component;
_context6.next = 6;
return (0, _utils.loadGetInitialProps)(App, {
Component: Component,
router: this,
ctx: ctx
});
case 6:
props = _context6.sent;
if (cancel === this.componentLoadCancel) {
this.componentLoadCancel = null;
}
if (!cancelled) {
_context6.next = 12;
break;
}
err = new Error('Loading initial props cancelled');
err.cancelled = true;
throw err;
case 12:
return _context6.abrupt("return", props);
case 13:
case "end":
return _context6.stop();
}
}
}, _callee6, this);
}));
return function getInitialProps(_x13, _x14) {
return _getInitialProps.apply(this, arguments);
};
}()
}, {
key: "fetchRoute",
value: function () {
var _fetchRoute = (0, _asyncToGenerator2.default)(
/*#__PURE__*/
_regenerator.default.mark(function _callee7(route) {
return _regenerator.default.wrap(function _callee7$(_context7) {
while (1) {
switch (_context7.prev = _context7.next) {
case 0:
return _context7.abrupt("return", this.pageLoader.loadPage(route));
case 1:
case "end":
return _context7.stop();
}
}
}, _callee7, this);
}));
return function fetchRoute(_x15) {
return _fetchRoute.apply(this, arguments);
};
}()
}, {
key: "abortComponentLoad",
value: function abortComponentLoad(as) {
if (this.componentLoadCancel) {
Router.events.emit('routeChangeError', new Error('Route Cancelled'), as);
this.componentLoadCancel();
this.componentLoadCancel = null;
}
}
}, {
key: "notify",
value: function notify(data) {
var App = this.components['/_app'].Component;
this.subscriptions.forEach(function (fn) {
return fn((0, _objectSpread2.default)({}, data, {
App: App
}));
});
}
}, {
key: "subscribe",
value: function subscribe(fn) {
var _this3 = this;
this.subscriptions.add(fn);
return function () {
return _this3.subscriptions.delete(fn);
};
}
}]);
return Router;
}();
exports.default = Router;
(0, _defineProperty2.default)(Router, "events", new _EventEmitter.default());
function toRoute(path) {
return path.replace(/\/$/, '') || '/';
}