on-demand-entry-handler.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", {
  3. value: true
  4. });
  5. exports.onDemandEntryHandler = onDemandEntryHandler;
  6. exports.getInvalidator = exports.entries = exports.EntryTypes = exports.BUILT = exports.BUILDING = exports.ADDED = void 0;
  7. var _debug = _interopRequireDefault(require("next/dist/compiled/debug"));
  8. var _events = require("events");
  9. var _findPageFile = require("../lib/find-page-file");
  10. var _entries = require("../../build/entries");
  11. var _path = require("path");
  12. var _normalizePathSep = require("../../shared/lib/page-path/normalize-path-sep");
  13. var _normalizePagePath = require("../../shared/lib/page-path/normalize-page-path");
  14. var _ensureLeadingSlash = require("../../shared/lib/page-path/ensure-leading-slash");
  15. var _removePagePathTail = require("../../shared/lib/page-path/remove-page-path-tail");
  16. var _output = require("../../build/output");
  17. var _getRouteFromEntrypoint = _interopRequireDefault(require("../get-route-from-entrypoint"));
  18. var _getPageStaticInfo = require("../../build/analysis/get-page-static-info");
  19. var _utils = require("../../build/utils");
  20. var _utils1 = require("../../shared/lib/utils");
  21. var _constants = require("../../shared/lib/constants");
  22. function _interopRequireDefault(obj) {
  23. return obj && obj.__esModule ? obj : {
  24. default: obj
  25. };
  26. }
  27. const debug = (0, _debug).default("next:on-demand-entry-handler");
  28. /**
  29. * Returns object keys with type inferred from the object key
  30. */ const keys = Object.keys;
  31. const COMPILER_KEYS = keys(_constants.COMPILER_INDEXES);
  32. function treePathToEntrypoint(segmentPath, parentPath) {
  33. const [parallelRouteKey, segment] = segmentPath;
  34. // TODO-APP: modify this path to cover parallelRouteKey convention
  35. const path = (parentPath ? parentPath + "/" : "") + (parallelRouteKey !== "children" && !segment.startsWith("@") ? parallelRouteKey + "/" : "") + (segment === "" ? "page" : segment);
  36. // Last segment
  37. if (segmentPath.length === 2) {
  38. return path;
  39. }
  40. const childSegmentPath = segmentPath.slice(2);
  41. return treePathToEntrypoint(childSegmentPath, path);
  42. }
  43. function convertDynamicParamTypeToSyntax(dynamicParamTypeShort, param) {
  44. switch(dynamicParamTypeShort){
  45. case "c":
  46. return `[...${param}]`;
  47. case "oc":
  48. return `[[...${param}]]`;
  49. case "d":
  50. return `[${param}]`;
  51. default:
  52. throw new Error("Unknown dynamic param type");
  53. }
  54. }
  55. function getEntrypointsFromTree(tree, isFirst, parentPath = []) {
  56. const [segment, parallelRoutes] = tree;
  57. const currentSegment = Array.isArray(segment) ? convertDynamicParamTypeToSyntax(segment[2], segment[0]) : segment;
  58. const currentPath = [
  59. ...parentPath,
  60. currentSegment
  61. ];
  62. if (!isFirst && currentSegment === "") {
  63. // TODO get rid of '' at the start of tree
  64. return [
  65. treePathToEntrypoint(currentPath.slice(1))
  66. ];
  67. }
  68. return Object.keys(parallelRoutes).reduce((paths, key)=>{
  69. const childTree = parallelRoutes[key];
  70. const childPages = getEntrypointsFromTree(childTree, false, [
  71. ...currentPath,
  72. key,
  73. ]);
  74. return [
  75. ...paths,
  76. ...childPages
  77. ];
  78. }, []);
  79. }
  80. const ADDED = Symbol("added");
  81. exports.ADDED = ADDED;
  82. const BUILDING = Symbol("building");
  83. exports.BUILDING = BUILDING;
  84. const BUILT = Symbol("built");
  85. exports.BUILT = BUILT;
  86. var EntryTypes;
  87. exports.EntryTypes = EntryTypes;
  88. (function(EntryTypes) {
  89. EntryTypes[EntryTypes["ENTRY"] = 0] = "ENTRY";
  90. EntryTypes[EntryTypes["CHILD_ENTRY"] = 1] = "CHILD_ENTRY";
  91. })(EntryTypes || (exports.EntryTypes = EntryTypes = {}));
  92. const entries = {};
  93. exports.entries = entries;
  94. let invalidator;
  95. const getInvalidator = ()=>invalidator;
  96. exports.getInvalidator = getInvalidator;
  97. const doneCallbacks = new _events.EventEmitter();
  98. const lastClientAccessPages = [
  99. ""
  100. ];
  101. const lastServerAccessPagesForAppDir = [
  102. ""
  103. ];
  104. // Make sure only one invalidation happens at a time
  105. // Otherwise, webpack hash gets changed and it'll force the client to reload.
  106. class Invalidator {
  107. building = new Set();
  108. rebuildAgain = new Set();
  109. constructor(multiCompiler){
  110. this.multiCompiler = multiCompiler;
  111. }
  112. shouldRebuildAll() {
  113. return this.rebuildAgain.size > 0;
  114. }
  115. invalidate(compilerKeys = COMPILER_KEYS) {
  116. for (const key of compilerKeys){
  117. var ref;
  118. // If there's a current build is processing, we won't abort it by invalidating.
  119. // (If aborted, it'll cause a client side hard reload)
  120. // But let it to invalidate just after the completion.
  121. // So, it can re-build the queued pages at once.
  122. if (this.building.has(key)) {
  123. this.rebuildAgain.add(key);
  124. continue;
  125. }
  126. (ref = this.multiCompiler.compilers[_constants.COMPILER_INDEXES[key]].watching) == null ? void 0 : ref.invalidate();
  127. this.building.add(key);
  128. }
  129. }
  130. startBuilding(compilerKey) {
  131. this.building.add(compilerKey);
  132. }
  133. doneBuilding() {
  134. const rebuild = [];
  135. for (const key of COMPILER_KEYS){
  136. this.building.delete(key);
  137. if (this.rebuildAgain.has(key)) {
  138. rebuild.push(key);
  139. this.rebuildAgain.delete(key);
  140. }
  141. }
  142. this.invalidate(rebuild);
  143. }
  144. }
  145. function disposeInactiveEntries(maxInactiveAge) {
  146. Object.keys(entries).forEach((entryKey)=>{
  147. const entryData = entries[entryKey];
  148. const { lastActiveTime , status , dispose } = entryData;
  149. // TODO-APP: implement disposing of CHILD_ENTRY
  150. if (entryData.type === 1) {
  151. return;
  152. }
  153. if (dispose) // Skip pages already scheduled for disposing
  154. return;
  155. // This means this entry is currently building or just added
  156. // We don't need to dispose those entries.
  157. if (status !== BUILT) return;
  158. // We should not build the last accessed page even we didn't get any pings
  159. // Sometimes, it's possible our XHR ping to wait before completing other requests.
  160. // In that case, we should not dispose the current viewing page
  161. if (lastClientAccessPages.includes(entryKey) || lastServerAccessPagesForAppDir.includes(entryKey)) return;
  162. if (lastActiveTime && Date.now() - lastActiveTime > maxInactiveAge) {
  163. entries[entryKey].dispose = true;
  164. }
  165. });
  166. }
  167. // Normalize both app paths and page paths
  168. function tryToNormalizePagePath(page) {
  169. try {
  170. return (0, _normalizePagePath).normalizePagePath(page);
  171. } catch (err) {
  172. console.error(err);
  173. throw new _utils1.PageNotFoundError(page);
  174. }
  175. }
  176. /**
  177. * Attempts to find a page file path from the given pages absolute directory,
  178. * a page and allowed extensions. If the page can't be found it will throw an
  179. * error. It defaults the `/_error` page to Next.js internal error page.
  180. *
  181. * @param rootDir Absolute path to the project root.
  182. * @param pagesDir Absolute path to the pages folder with trailing `/pages`.
  183. * @param normalizedPagePath The page normalized (it will be denormalized).
  184. * @param pageExtensions Array of page extensions.
  185. */ async function findPagePathData(rootDir, page, extensions, pagesDir, appDir) {
  186. const normalizedPagePath = tryToNormalizePagePath(page);
  187. let pagePath = null;
  188. if ((0, _utils).isMiddlewareFile(normalizedPagePath)) {
  189. pagePath = await (0, _findPageFile).findPageFile(rootDir, normalizedPagePath, extensions, false);
  190. if (!pagePath) {
  191. throw new _utils1.PageNotFoundError(normalizedPagePath);
  192. }
  193. const pageUrl = (0, _ensureLeadingSlash).ensureLeadingSlash((0, _removePagePathTail).removePagePathTail((0, _normalizePathSep).normalizePathSep(pagePath), {
  194. extensions
  195. }));
  196. return {
  197. absolutePagePath: (0, _path).join(rootDir, pagePath),
  198. bundlePath: normalizedPagePath.slice(1),
  199. page: _path.posix.normalize(pageUrl)
  200. };
  201. }
  202. // Check appDir first falling back to pagesDir
  203. if (appDir) {
  204. pagePath = await (0, _findPageFile).findPageFile(appDir, normalizedPagePath, extensions, true);
  205. if (pagePath) {
  206. const pageUrl = (0, _ensureLeadingSlash).ensureLeadingSlash((0, _removePagePathTail).removePagePathTail((0, _normalizePathSep).normalizePathSep(pagePath), {
  207. keepIndex: true,
  208. extensions
  209. }));
  210. return {
  211. absolutePagePath: (0, _path).join(appDir, pagePath),
  212. bundlePath: _path.posix.join("app", (0, _normalizePagePath).normalizePagePath(pageUrl)),
  213. page: _path.posix.normalize(pageUrl)
  214. };
  215. }
  216. }
  217. if (!pagePath && pagesDir) {
  218. pagePath = await (0, _findPageFile).findPageFile(pagesDir, normalizedPagePath, extensions, false);
  219. }
  220. if (pagePath !== null && pagesDir) {
  221. const pageUrl = (0, _ensureLeadingSlash).ensureLeadingSlash((0, _removePagePathTail).removePagePathTail((0, _normalizePathSep).normalizePathSep(pagePath), {
  222. extensions
  223. }));
  224. return {
  225. absolutePagePath: (0, _path).join(pagesDir, pagePath),
  226. bundlePath: _path.posix.join("pages", (0, _normalizePagePath).normalizePagePath(pageUrl)),
  227. page: _path.posix.normalize(pageUrl)
  228. };
  229. }
  230. if (page === "/_error") {
  231. return {
  232. absolutePagePath: require.resolve("next/dist/pages/_error"),
  233. bundlePath: page,
  234. page: (0, _normalizePathSep).normalizePathSep(page)
  235. };
  236. } else {
  237. throw new _utils1.PageNotFoundError(normalizedPagePath);
  238. }
  239. }
  240. function onDemandEntryHandler({ maxInactiveAge , multiCompiler , nextConfig , pagesBufferLength , pagesDir , rootDir , appDir }) {
  241. invalidator = new Invalidator(multiCompiler);
  242. const startBuilding = (compilation)=>{
  243. const compilationName = compilation.name;
  244. invalidator.startBuilding(compilationName);
  245. };
  246. for (const compiler of multiCompiler.compilers){
  247. compiler.hooks.make.tap("NextJsOnDemandEntries", startBuilding);
  248. }
  249. function getPagePathsFromEntrypoints(type, entrypoints, root) {
  250. const pagePaths = [];
  251. for (const entrypoint of entrypoints.values()){
  252. const page = (0, _getRouteFromEntrypoint).default(entrypoint.name, root);
  253. if (page) {
  254. pagePaths.push(`${type}${page}`);
  255. } else if (root && entrypoint.name === "root" || (0, _utils).isMiddlewareFilename(entrypoint.name)) {
  256. pagePaths.push(`${type}/${entrypoint.name}`);
  257. }
  258. }
  259. return pagePaths;
  260. }
  261. multiCompiler.hooks.done.tap("NextJsOnDemandEntries", (multiStats)=>{
  262. if (invalidator.shouldRebuildAll()) {
  263. return invalidator.doneBuilding();
  264. }
  265. const [clientStats, serverStats, edgeServerStats] = multiStats.stats;
  266. const root = !!appDir;
  267. const pagePaths = [
  268. ...getPagePathsFromEntrypoints(_constants.COMPILER_NAMES.client, clientStats.compilation.entrypoints, root),
  269. ...getPagePathsFromEntrypoints(_constants.COMPILER_NAMES.server, serverStats.compilation.entrypoints, root),
  270. ...edgeServerStats ? getPagePathsFromEntrypoints(_constants.COMPILER_NAMES.edgeServer, edgeServerStats.compilation.entrypoints, root) : [],
  271. ];
  272. for (const page of pagePaths){
  273. const entry = entries[page];
  274. if (!entry) {
  275. continue;
  276. }
  277. if (entry.status !== BUILDING) {
  278. continue;
  279. }
  280. entry.status = BUILT;
  281. doneCallbacks.emit(page);
  282. }
  283. invalidator.doneBuilding();
  284. });
  285. const pingIntervalTime = Math.max(1000, Math.min(5000, maxInactiveAge));
  286. setInterval(function() {
  287. disposeInactiveEntries(maxInactiveAge);
  288. }, pingIntervalTime + 1000).unref();
  289. function handleAppDirPing(tree) {
  290. const pages = getEntrypointsFromTree(tree, true);
  291. let toSend = {
  292. invalid: true
  293. };
  294. for (const page of pages){
  295. for (const compilerType of [
  296. _constants.COMPILER_NAMES.client,
  297. _constants.COMPILER_NAMES.server,
  298. _constants.COMPILER_NAMES.edgeServer,
  299. ]){
  300. const pageKey = `${compilerType}/${page}`;
  301. const entryInfo = entries[pageKey];
  302. // If there's no entry, it may have been invalidated and needs to be re-built.
  303. if (!entryInfo) {
  304. continue;
  305. }
  306. // We don't need to maintain active state of anything other than BUILT entries
  307. if (entryInfo.status !== BUILT) continue;
  308. // If there's an entryInfo
  309. if (!lastServerAccessPagesForAppDir.includes(pageKey)) {
  310. lastServerAccessPagesForAppDir.unshift(pageKey);
  311. // Maintain the buffer max length
  312. // TODO: verify that the current pageKey is not at the end of the array as multiple entrypoints can exist
  313. if (lastServerAccessPagesForAppDir.length > pagesBufferLength) {
  314. lastServerAccessPagesForAppDir.pop();
  315. }
  316. }
  317. entryInfo.lastActiveTime = Date.now();
  318. entryInfo.dispose = false;
  319. toSend = {
  320. success: true
  321. };
  322. }
  323. }
  324. return toSend;
  325. }
  326. function handlePing(pg) {
  327. const page = (0, _normalizePathSep).normalizePathSep(pg);
  328. let toSend = {
  329. invalid: true
  330. };
  331. for (const compilerType of [
  332. _constants.COMPILER_NAMES.client,
  333. _constants.COMPILER_NAMES.server,
  334. _constants.COMPILER_NAMES.edgeServer,
  335. ]){
  336. const pageKey = `${compilerType}${page}`;
  337. const entryInfo = entries[pageKey];
  338. // If there's no entry, it may have been invalidated and needs to be re-built.
  339. if (!entryInfo) {
  340. // if (page !== lastEntry) client pings, but there's no entry for page
  341. if (compilerType === _constants.COMPILER_NAMES.client) {
  342. return {
  343. invalid: true
  344. };
  345. }
  346. continue;
  347. }
  348. // 404 is an on demand entry but when a new page is added we have to refresh the page
  349. toSend = page === "/_error" ? {
  350. invalid: true
  351. } : {
  352. success: true
  353. };
  354. // We don't need to maintain active state of anything other than BUILT entries
  355. if (entryInfo.status !== BUILT) continue;
  356. // If there's an entryInfo
  357. if (!lastClientAccessPages.includes(pageKey)) {
  358. lastClientAccessPages.unshift(pageKey);
  359. // Maintain the buffer max length
  360. if (lastClientAccessPages.length > pagesBufferLength) {
  361. lastClientAccessPages.pop();
  362. }
  363. }
  364. entryInfo.lastActiveTime = Date.now();
  365. entryInfo.dispose = false;
  366. }
  367. return toSend;
  368. }
  369. return {
  370. async ensurePage ({ page , clientOnly , appPaths =null }) {
  371. const stalledTime = 60;
  372. const stalledEnsureTimeout = setTimeout(()=>{
  373. debug(`Ensuring ${page} has taken longer than ${stalledTime}s, if this continues to stall this may be a bug`);
  374. }, stalledTime * 1000);
  375. try {
  376. const pagePathData = await findPagePathData(rootDir, page, nextConfig.pageExtensions, pagesDir, appDir);
  377. const isInsideAppDir = !!appDir && pagePathData.absolutePagePath.startsWith(appDir);
  378. const addEntry = (compilerType)=>{
  379. const entryKey = `${compilerType}${pagePathData.page}`;
  380. if (entries[entryKey]) {
  381. entries[entryKey].dispose = false;
  382. entries[entryKey].lastActiveTime = Date.now();
  383. if (entries[entryKey].status === BUILT) {
  384. return {
  385. entryKey,
  386. newEntry: false,
  387. shouldInvalidate: false
  388. };
  389. }
  390. return {
  391. entryKey,
  392. newEntry: false,
  393. shouldInvalidate: true
  394. };
  395. }
  396. entries[entryKey] = {
  397. type: 0,
  398. appPaths,
  399. absolutePagePath: pagePathData.absolutePagePath,
  400. request: pagePathData.absolutePagePath,
  401. bundlePath: pagePathData.bundlePath,
  402. dispose: false,
  403. lastActiveTime: Date.now(),
  404. status: ADDED
  405. };
  406. return {
  407. entryKey: entryKey,
  408. newEntry: true,
  409. shouldInvalidate: true
  410. };
  411. };
  412. const staticInfo = await (0, _getPageStaticInfo).getPageStaticInfo({
  413. pageFilePath: pagePathData.absolutePagePath,
  414. nextConfig,
  415. isDev: true
  416. });
  417. const added = new Map();
  418. const isServerComponent = isInsideAppDir && staticInfo.rsc !== _constants.RSC_MODULE_TYPES.client;
  419. await (0, _entries).runDependingOnPageType({
  420. page: pagePathData.page,
  421. pageRuntime: staticInfo.runtime,
  422. onClient: ()=>{
  423. // Skip adding the client entry for app / Server Components.
  424. if (isServerComponent || isInsideAppDir) {
  425. return;
  426. }
  427. added.set(_constants.COMPILER_NAMES.client, addEntry(_constants.COMPILER_NAMES.client));
  428. },
  429. onServer: ()=>{
  430. added.set(_constants.COMPILER_NAMES.server, addEntry(_constants.COMPILER_NAMES.server));
  431. const edgeServerEntry = `${_constants.COMPILER_NAMES.edgeServer}${pagePathData.page}`;
  432. if (entries[edgeServerEntry]) {
  433. // Runtime switched from edge to server
  434. delete entries[edgeServerEntry];
  435. }
  436. },
  437. onEdgeServer: ()=>{
  438. added.set(_constants.COMPILER_NAMES.edgeServer, addEntry(_constants.COMPILER_NAMES.edgeServer));
  439. const serverEntry = `${_constants.COMPILER_NAMES.server}${pagePathData.page}`;
  440. if (entries[serverEntry]) {
  441. // Runtime switched from server to edge
  442. delete entries[serverEntry];
  443. }
  444. }
  445. });
  446. const addedValues = [
  447. ...added.values()
  448. ];
  449. const entriesThatShouldBeInvalidated = addedValues.filter((entry)=>entry.shouldInvalidate);
  450. const hasNewEntry = addedValues.some((entry)=>entry.newEntry);
  451. if (hasNewEntry) {
  452. (0, _output).reportTrigger(!clientOnly && hasNewEntry ? `${pagePathData.page} (client and server)` : pagePathData.page);
  453. }
  454. if (entriesThatShouldBeInvalidated.length > 0) {
  455. const invalidatePromises = entriesThatShouldBeInvalidated.map(({ entryKey })=>{
  456. return new Promise((resolve, reject)=>{
  457. doneCallbacks.once(entryKey, (err)=>{
  458. if (err) {
  459. return reject(err);
  460. }
  461. resolve();
  462. });
  463. });
  464. });
  465. invalidator.invalidate([
  466. ...added.keys()
  467. ]);
  468. await Promise.all(invalidatePromises);
  469. }
  470. } finally{
  471. clearTimeout(stalledEnsureTimeout);
  472. }
  473. },
  474. onHMR (client) {
  475. client.addEventListener("message", ({ data })=>{
  476. try {
  477. const parsedData = JSON.parse(typeof data !== "string" ? data.toString() : data);
  478. if (parsedData.event === "ping") {
  479. const result = parsedData.appDirRoute ? handleAppDirPing(parsedData.tree) : handlePing(parsedData.page);
  480. client.send(JSON.stringify({
  481. ...result,
  482. [parsedData.appDirRoute ? "action" : "event"]: "pong"
  483. }));
  484. }
  485. } catch (_) {}
  486. });
  487. }
  488. };
  489. }
  490. //# sourceMappingURL=on-demand-entry-handler.js.map