UNPKG

@tdb/web

Version:

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

601 lines (491 loc) 20.7 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime-corejs2/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = onDemandEntryHandler; exports.normalizePage = normalizePage; var _classCallCheck2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/classCallCheck")); var _createClass2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/createClass")); var _stringify = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/json/stringify")); var _isArray = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/array/is-array")); var _keys = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/object/keys")); var _regenerator = _interopRequireDefault(require("@babel/runtime-corejs2/regenerator")); var _asyncToGenerator2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/asyncToGenerator")); var _now = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/date/now")); var _slicedToArray2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/slicedToArray")); var _getIterator2 = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/get-iterator")); var _toConsumableArray2 = _interopRequireDefault(require("@babel/runtime-corejs2/helpers/toConsumableArray")); var _promise = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/promise")); var _symbol = _interopRequireDefault(require("@babel/runtime-corejs2/core-js/symbol")); var _DynamicEntryPlugin = _interopRequireDefault(require("webpack/lib/DynamicEntryPlugin")); var _events = require("events"); var _path = require("path"); var _url = require("url"); var _fs = _interopRequireDefault(require("fs")); var _promisify = _interopRequireDefault(require("../lib/promisify")); var _glob = _interopRequireDefault(require("glob")); var _require = require("./require"); var _utils = require("../build/webpack/utils"); var _constants = require("../lib/constants"); var ADDED = (0, _symbol.default)('added'); var BUILDING = (0, _symbol.default)('building'); var BUILT = (0, _symbol.default)('built'); var glob = (0, _promisify.default)(_glob.default); var access = (0, _promisify.default)(_fs.default.access); // Based on https://github.com/webpack/webpack/blob/master/lib/DynamicEntryPlugin.js#L29-L37 function addEntry(compilation, context, name, entry) { return new _promise.default(function (resolve, reject) { var dep = _DynamicEntryPlugin.default.createDependency(entry, name); compilation.addEntry(context, dep, name, function (err) { if (err) return reject(err); resolve(); }); }); } function onDemandEntryHandler(devMiddleware, multiCompiler, _ref) { var buildId = _ref.buildId, dir = _ref.dir, dev = _ref.dev, reload = _ref.reload, pageExtensions = _ref.pageExtensions, _ref$maxInactiveAge = _ref.maxInactiveAge, maxInactiveAge = _ref$maxInactiveAge === void 0 ? 1000 * 60 : _ref$maxInactiveAge, _ref$pagesBufferLengt = _ref.pagesBufferLength, pagesBufferLength = _ref$pagesBufferLengt === void 0 ? 2 : _ref$pagesBufferLengt; var compilers = multiCompiler.compilers; var invalidator = new Invalidator(devMiddleware, multiCompiler); var entries = {}; var lastAccessPages = ['']; var doneCallbacks = new _events.EventEmitter(); var reloading = false; var stopped = false; var reloadCallbacks = new _events.EventEmitter(); var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { var _loop = function _loop() { var compiler = _step.value; compiler.hooks.make.tapPromise('NextJsOnDemandEntries', function (compilation) { invalidator.startBuilding(); var allEntries = (0, _keys.default)(entries).map( /*#__PURE__*/ function () { var _ref2 = (0, _asyncToGenerator2.default)( /*#__PURE__*/ _regenerator.default.mark(function _callee2(page) { var _entries$page, name, entry, files, _iteratorNormalCompletion3, _didIteratorError3, _iteratorError3, _iterator3, _step3, file; return _regenerator.default.wrap(function _callee2$(_context2) { while (1) { switch (_context2.prev = _context2.next) { case 0: _entries$page = entries[page], name = _entries$page.name, entry = _entries$page.entry; files = (0, _isArray.default)(entry) ? entry : [entry]; // Is just one item. But it's passed as an array. _iteratorNormalCompletion3 = true; _didIteratorError3 = false; _iteratorError3 = undefined; _context2.prev = 5; _iterator3 = (0, _getIterator2.default)(files); case 7: if (_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done) { _context2.next = 22; break; } file = _step3.value; _context2.prev = 9; _context2.next = 12; return access((0, _path.join)(dir, file), (_fs.default.constants || _fs.default).W_OK); case 12: _context2.next = 19; break; case 14: _context2.prev = 14; _context2.t0 = _context2["catch"](9); console.warn('Page was removed', page); delete entries[page]; return _context2.abrupt("return"); case 19: _iteratorNormalCompletion3 = true; _context2.next = 7; break; case 22: _context2.next = 28; break; case 24: _context2.prev = 24; _context2.t1 = _context2["catch"](5); _didIteratorError3 = true; _iteratorError3 = _context2.t1; case 28: _context2.prev = 28; _context2.prev = 29; if (!_iteratorNormalCompletion3 && _iterator3.return != null) { _iterator3.return(); } case 31: _context2.prev = 31; if (!_didIteratorError3) { _context2.next = 34; break; } throw _iteratorError3; case 34: return _context2.finish(31); case 35: return _context2.finish(28); case 36: entries[page].status = BUILDING; return _context2.abrupt("return", addEntry(compilation, compiler.context, name, entry)); case 38: case "end": return _context2.stop(); } } }, _callee2, this, [[5, 24, 28, 36], [9, 14], [29,, 31, 35]]); })); return function (_x2) { return _ref2.apply(this, arguments); }; }()); return _promise.default.all(allEntries); }); }; for (var _iterator = (0, _getIterator2.default)(compilers), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { _loop(); } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return != null) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } multiCompiler.hooks.done.tap('NextJsOnDemandEntries', function (multiStats) { var clientStats = multiStats.stats[0]; var compilation = clientStats.compilation; var hardFailedPages = compilation.errors.filter(function (e) { // Make sure to only pick errors which marked with missing modules var hasNoModuleFoundError = /ENOENT/.test(e.message) || /Module not found/.test(e.message); if (!hasNoModuleFoundError) return false; // The page itself is missing. So this is a failed page. if (_constants.IS_BUNDLED_PAGE_REGEX.test(e.module.name)) return true; // No dependencies means this is a top level page. // So this is a failed page. return e.module.dependencies.length === 0; }).map(function (e) { return e.module.chunks; }).reduce(function (a, b) { return (0, _toConsumableArray2.default)(a).concat((0, _toConsumableArray2.default)(b)); }, []).map(function (c) { var pageName = _constants.ROUTE_NAME_REGEX.exec(c.name)[1]; return normalizePage("/".concat(pageName)); }); // compilation.entrypoints is a Map object, so iterating over it 0 is the key and 1 is the value var _iteratorNormalCompletion2 = true; var _didIteratorError2 = false; var _iteratorError2 = undefined; try { for (var _iterator2 = (0, _getIterator2.default)(compilation.entrypoints.entries()), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { var _step2$value = (0, _slicedToArray2.default)(_step2.value, 2), entrypoint = _step2$value[1]; var result = _constants.ROUTE_NAME_REGEX.exec(entrypoint.name); if (!result) { continue; } var pagePath = result[1]; if (!pagePath) { continue; } var page = normalizePage('/' + pagePath); var entry = entries[page]; if (!entry) { continue; } if (entry.status !== BUILDING) { continue; } entry.status = BUILT; entry.lastActiveTime = (0, _now.default)(); doneCallbacks.emit(page); } } catch (err) { _didIteratorError2 = true; _iteratorError2 = err; } finally { try { if (!_iteratorNormalCompletion2 && _iterator2.return != null) { _iterator2.return(); } } finally { if (_didIteratorError2) { throw _iteratorError2; } } } invalidator.doneBuilding(); if (hardFailedPages.length > 0 && !reloading) { console.log("> Reloading webpack due to inconsistant state of pages(s): ".concat(hardFailedPages.join(', '))); reloading = true; reload().then(function () { console.log('> Webpack reloaded.'); reloadCallbacks.emit('done'); stop(); }).catch(function (err) { console.error("> Webpack reloading failed: ".concat(err.message)); console.error(err.stack); process.exit(1); }); } }); var disposeHandler = setInterval(function () { if (stopped) return; disposeInactiveEntries(devMiddleware, entries, lastAccessPages, maxInactiveAge); }, 5000); disposeHandler.unref(); function stop() { clearInterval(disposeHandler); stopped = true; doneCallbacks = null; reloadCallbacks = null; } return { waitUntilReloaded: function waitUntilReloaded() { if (!reloading) return _promise.default.resolve(true); return new _promise.default(function (resolve) { reloadCallbacks.once('done', function () { resolve(); }); }); }, ensurePage: function () { var _ensurePage = (0, _asyncToGenerator2.default)( /*#__PURE__*/ _regenerator.default.mark(function _callee(page) { var normalizedPagePath, extensions, paths, relativePathToPage, pathname, _createEntry, name, files; return _regenerator.default.wrap(function _callee$(_context) { while (1) { switch (_context.prev = _context.next) { case 0: _context.next = 2; return this.waitUntilReloaded(); case 2: page = normalizePage(page); _context.prev = 3; normalizedPagePath = (0, _require.normalizePagePath)(page); _context.next = 11; break; case 7: _context.prev = 7; _context.t0 = _context["catch"](3); console.error(_context.t0); throw (0, _require.pageNotFoundError)(normalizedPagePath); case 11: extensions = pageExtensions.join('|'); _context.next = 14; return glob("pages/{".concat(normalizedPagePath, "/index,").concat(normalizedPagePath, "}.+(").concat(extensions, ")"), { cwd: dir }); case 14: paths = _context.sent; if (!(paths.length === 0)) { _context.next = 17; break; } throw (0, _require.pageNotFoundError)(normalizedPagePath); case 17: relativePathToPage = paths[0]; pathname = (0, _path.join)(dir, relativePathToPage); _createEntry = (0, _utils.createEntry)(relativePathToPage, { buildId: buildId, pageExtensions: extensions }), name = _createEntry.name, files = _createEntry.files; _context.next = 22; return new _promise.default(function (resolve, reject) { var entryInfo = entries[page]; if (entryInfo) { if (entryInfo.status === BUILT) { resolve(); return; } if (entryInfo.status === BUILDING) { doneCallbacks.once(page, handleCallback); return; } } console.log("> Building page: ".concat(page)); entries[page] = { name: name, entry: files, pathname: pathname, status: ADDED }; doneCallbacks.once(page, handleCallback); invalidator.invalidate(); function handleCallback(err) { if (err) return reject(err); resolve(); } }); case 22: case "end": return _context.stop(); } } }, _callee, this, [[3, 7]]); })); return function ensurePage(_x) { return _ensurePage.apply(this, arguments); }; }(), middleware: function middleware() { var _this = this; return function (req, res, next) { if (stopped) { // If this handler is stopped, we need to reload the user's browser. // So the user could connect to the actually running handler. res.statusCode = 302; res.setHeader('Location', req.url); res.end('302'); } else if (reloading) { // Webpack config is reloading. So, we need to wait until it's done and // reload user's browser. // So the user could connect to the new handler and webpack setup. _this.waitUntilReloaded().then(function () { res.statusCode = 302; res.setHeader('Location', req.url); res.end('302'); }); } else { if (!/^\/_next\/on-demand-entries-ping/.test(req.url)) return next(); var _parse = (0, _url.parse)(req.url, true), query = _parse.query; var page = normalizePage(query.page); var entryInfo = entries[page]; // If there's no entry. // Then it seems like an weird issue. if (!entryInfo) { var message = "Client pings, but there's no entry for page: ".concat(page); console.error(message); sendJson(res, { invalid: true }); return; } sendJson(res, { success: true }); // We don't need to maintain active state of anything other than BUILT entries if (entryInfo.status !== BUILT) return; // If there's an entryInfo if (!lastAccessPages.includes(page)) { lastAccessPages.unshift(page); // Maintain the buffer max length if (lastAccessPages.length > pagesBufferLength) lastAccessPages.pop(); } entryInfo.lastActiveTime = (0, _now.default)(); } }; } }; } function disposeInactiveEntries(devMiddleware, entries, lastAccessPages, maxInactiveAge) { var disposingPages = []; (0, _keys.default)(entries).forEach(function (page) { var _entries$page2 = entries[page], lastActiveTime = _entries$page2.lastActiveTime, status = _entries$page2.status; // This means this entry is currently building or just added // We don't need to dispose those entries. if (status !== BUILT) return; // We should not build the last accessed page even we didn't get any pings // Sometimes, it's possible our XHR ping to wait before completing other requests. // In that case, we should not dispose the current viewing page if (lastAccessPages.includes(page)) return; if ((0, _now.default)() - lastActiveTime > maxInactiveAge) { disposingPages.push(page); } }); if (disposingPages.length > 0) { disposingPages.forEach(function (page) { delete entries[page]; }); console.log("> Disposing inactive page(s): ".concat(disposingPages.join(', '))); devMiddleware.invalidate(); } } // /index and / is the same. So, we need to identify both pages as the same. // This also applies to sub pages as well. function normalizePage(page) { var unixPagePath = page.replace(/\\/g, '/'); if (unixPagePath === '/index' || unixPagePath === '/') { return '/'; } return unixPagePath.replace(/\/index$/, ''); } function sendJson(res, payload) { res.setHeader('Content-Type', 'application/json'); res.status = 200; res.end((0, _stringify.default)(payload)); } // Make sure only one invalidation happens at a time // Otherwise, webpack hash gets changed and it'll force the client to reload. var Invalidator = /*#__PURE__*/ function () { function Invalidator(devMiddleware, multiCompiler) { (0, _classCallCheck2.default)(this, Invalidator); this.multiCompiler = multiCompiler; this.devMiddleware = devMiddleware; // contains an array of types of compilers currently building this.building = false; this.rebuildAgain = false; } (0, _createClass2.default)(Invalidator, [{ key: "invalidate", value: function invalidate() { // If there's a current build is processing, we won't abort it by invalidating. // (If aborted, it'll cause a client side hard reload) // But let it to invalidate just after the completion. // So, it can re-build the queued pages at once. if (this.building) { this.rebuildAgain = true; return; } this.building = true; // Work around a bug in webpack, calling `invalidate` on Watching.js // doesn't trigger the invalid call used to keep track of the `.done` hook on multiCompiler var _iteratorNormalCompletion4 = true; var _didIteratorError4 = false; var _iteratorError4 = undefined; try { for (var _iterator4 = (0, _getIterator2.default)(this.multiCompiler.compilers), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { var compiler = _step4.value; compiler.hooks.invalid.call(); } } catch (err) { _didIteratorError4 = true; _iteratorError4 = err; } finally { try { if (!_iteratorNormalCompletion4 && _iterator4.return != null) { _iterator4.return(); } } finally { if (_didIteratorError4) { throw _iteratorError4; } } } this.devMiddleware.invalidate(); } }, { key: "startBuilding", value: function startBuilding() { this.building = true; } }, { key: "doneBuilding", value: function doneBuilding() { this.building = false; if (this.rebuildAgain) { this.rebuildAgain = false; this.invalidate(); } } }]); return Invalidator; }();