UNPKG

@tdb/web

Version:

Common condiguration for serving a web-site and testing web-based UI components.

818 lines (676 loc) 26.6 kB
"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(/\/$/, '') || '/'; }