@teleporthq/teleport-plugin-next-data-source 0.43.57 → 0.43.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (147) hide show
  1. package/__tests__/_helpers/pagination-widget-fixture.ts +230 -0
  2. package/__tests__/ecommerce-product-allow-backorders.test.ts +156 -0
  3. package/__tests__/ecommerce-product-discounts.test.ts +196 -0
  4. package/__tests__/ecommerce-product-model.test.ts +179 -0
  5. package/__tests__/infinite-scroll.test.ts +143 -0
  6. package/__tests__/numbered-pagination.test.ts +113 -0
  7. package/__tests__/page-reset-on-filter-sort.test.ts +182 -0
  8. package/__tests__/page-url-sync.test.ts +208 -0
  9. package/__tests__/product-price-sort.ts +313 -0
  10. package/__tests__/search-url-sync.test.ts +7 -3
  11. package/dist/cjs/array-mapper-pagination.d.ts +9 -0
  12. package/dist/cjs/array-mapper-pagination.d.ts.map +1 -1
  13. package/dist/cjs/array-mapper-pagination.js.map +1 -1
  14. package/dist/cjs/count-effect.d.ts +42 -0
  15. package/dist/cjs/count-effect.d.ts.map +1 -0
  16. package/dist/cjs/count-effect.js +81 -0
  17. package/dist/cjs/count-effect.js.map +1 -0
  18. package/dist/cjs/fetchers/postgresql.d.ts.map +1 -1
  19. package/dist/cjs/fetchers/postgresql.js +2 -1
  20. package/dist/cjs/fetchers/postgresql.js.map +1 -1
  21. package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
  22. package/dist/cjs/fetchers/teleport.js +3 -2
  23. package/dist/cjs/fetchers/teleport.js.map +1 -1
  24. package/dist/cjs/infinite-scroll.d.ts +65 -0
  25. package/dist/cjs/infinite-scroll.d.ts.map +1 -0
  26. package/dist/cjs/infinite-scroll.js +190 -0
  27. package/dist/cjs/infinite-scroll.js.map +1 -0
  28. package/dist/cjs/numbered-pagination.d.ts +42 -0
  29. package/dist/cjs/numbered-pagination.d.ts.map +1 -0
  30. package/dist/cjs/numbered-pagination.js +167 -0
  31. package/dist/cjs/numbered-pagination.js.map +1 -0
  32. package/dist/cjs/page-reset.d.ts +42 -0
  33. package/dist/cjs/page-reset.d.ts.map +1 -0
  34. package/dist/cjs/page-reset.js +116 -0
  35. package/dist/cjs/page-reset.js.map +1 -0
  36. package/dist/cjs/page-url-sync.d.ts +42 -0
  37. package/dist/cjs/page-url-sync.d.ts.map +1 -0
  38. package/dist/cjs/page-url-sync.js +180 -0
  39. package/dist/cjs/page-url-sync.js.map +1 -0
  40. package/dist/cjs/pagination-controls.d.ts +55 -0
  41. package/dist/cjs/pagination-controls.d.ts.map +1 -0
  42. package/dist/cjs/pagination-controls.js +159 -0
  43. package/dist/cjs/pagination-controls.js.map +1 -0
  44. package/dist/cjs/pagination-plugin.d.ts.map +1 -1
  45. package/dist/cjs/pagination-plugin.js +506 -160
  46. package/dist/cjs/pagination-plugin.js.map +1 -1
  47. package/dist/cjs/product-price-sort.d.ts +116 -0
  48. package/dist/cjs/product-price-sort.d.ts.map +1 -0
  49. package/dist/cjs/product-price-sort.js +169 -0
  50. package/dist/cjs/product-price-sort.js.map +1 -0
  51. package/dist/cjs/transformations/ecommerce-product.d.ts +12 -0
  52. package/dist/cjs/transformations/ecommerce-product.d.ts.map +1 -1
  53. package/dist/cjs/transformations/ecommerce-product.js +2 -1
  54. package/dist/cjs/transformations/ecommerce-product.js.map +1 -1
  55. package/dist/cjs/transformations/index.d.ts +2 -1
  56. package/dist/cjs/transformations/index.d.ts.map +1 -1
  57. package/dist/cjs/transformations/index.js +17 -1
  58. package/dist/cjs/transformations/index.js.map +1 -1
  59. package/dist/cjs/transformations/shared-utils.d.ts.map +1 -1
  60. package/dist/cjs/transformations/shared-utils.js +1 -1
  61. package/dist/cjs/transformations/shared-utils.js.map +1 -1
  62. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  63. package/dist/esm/array-mapper-pagination.d.ts +9 -0
  64. package/dist/esm/array-mapper-pagination.d.ts.map +1 -1
  65. package/dist/esm/array-mapper-pagination.js.map +1 -1
  66. package/dist/esm/count-effect.d.ts +42 -0
  67. package/dist/esm/count-effect.d.ts.map +1 -0
  68. package/dist/esm/count-effect.js +53 -0
  69. package/dist/esm/count-effect.js.map +1 -0
  70. package/dist/esm/fetchers/postgresql.d.ts.map +1 -1
  71. package/dist/esm/fetchers/postgresql.js +2 -1
  72. package/dist/esm/fetchers/postgresql.js.map +1 -1
  73. package/dist/esm/fetchers/teleport.d.ts.map +1 -1
  74. package/dist/esm/fetchers/teleport.js +3 -2
  75. package/dist/esm/fetchers/teleport.js.map +1 -1
  76. package/dist/esm/infinite-scroll.d.ts +65 -0
  77. package/dist/esm/infinite-scroll.d.ts.map +1 -0
  78. package/dist/esm/infinite-scroll.js +159 -0
  79. package/dist/esm/infinite-scroll.js.map +1 -0
  80. package/dist/esm/numbered-pagination.d.ts +42 -0
  81. package/dist/esm/numbered-pagination.d.ts.map +1 -0
  82. package/dist/esm/numbered-pagination.js +138 -0
  83. package/dist/esm/numbered-pagination.js.map +1 -0
  84. package/dist/esm/page-reset.d.ts +42 -0
  85. package/dist/esm/page-reset.d.ts.map +1 -0
  86. package/dist/esm/page-reset.js +87 -0
  87. package/dist/esm/page-reset.js.map +1 -0
  88. package/dist/esm/page-url-sync.d.ts +42 -0
  89. package/dist/esm/page-url-sync.d.ts.map +1 -0
  90. package/dist/esm/page-url-sync.js +152 -0
  91. package/dist/esm/page-url-sync.js.map +1 -0
  92. package/dist/esm/pagination-controls.d.ts +55 -0
  93. package/dist/esm/pagination-controls.d.ts.map +1 -0
  94. package/dist/esm/pagination-controls.js +126 -0
  95. package/dist/esm/pagination-controls.js.map +1 -0
  96. package/dist/esm/pagination-plugin.d.ts.map +1 -1
  97. package/dist/esm/pagination-plugin.js +507 -161
  98. package/dist/esm/pagination-plugin.js.map +1 -1
  99. package/dist/esm/product-price-sort.d.ts +116 -0
  100. package/dist/esm/product-price-sort.d.ts.map +1 -0
  101. package/dist/esm/product-price-sort.js +160 -0
  102. package/dist/esm/product-price-sort.js.map +1 -0
  103. package/dist/esm/transformations/ecommerce-product.d.ts +12 -0
  104. package/dist/esm/transformations/ecommerce-product.d.ts.map +1 -1
  105. package/dist/esm/transformations/ecommerce-product.js +3 -2
  106. package/dist/esm/transformations/ecommerce-product.js.map +1 -1
  107. package/dist/esm/transformations/index.d.ts +2 -1
  108. package/dist/esm/transformations/index.d.ts.map +1 -1
  109. package/dist/esm/transformations/index.js +17 -1
  110. package/dist/esm/transformations/index.js.map +1 -1
  111. package/dist/esm/transformations/shared-utils.d.ts.map +1 -1
  112. package/dist/esm/transformations/shared-utils.js +1 -1
  113. package/dist/esm/transformations/shared-utils.js.map +1 -1
  114. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  115. package/package.json +5 -5
  116. package/src/array-mapper-pagination.ts +10 -0
  117. package/src/count-effect.ts +137 -0
  118. package/src/fetchers/postgresql.ts +52 -13
  119. package/src/fetchers/teleport.ts +53 -13
  120. package/src/infinite-scroll.ts +401 -0
  121. package/src/numbered-pagination.ts +292 -0
  122. package/src/page-reset.ts +162 -0
  123. package/src/page-url-sync.ts +241 -0
  124. package/src/pagination-controls.ts +176 -0
  125. package/src/pagination-plugin.ts +755 -346
  126. package/src/product-price-sort.ts +204 -0
  127. package/src/transformations/ecommerce-product.ts +141 -10
  128. package/src/transformations/index.ts +19 -1
  129. package/src/transformations/shared-utils.ts +14 -0
  130. package/dist/cjs/array-mapper-registry.d.ts +0 -85
  131. package/dist/cjs/array-mapper-registry.d.ts.map +0 -1
  132. package/dist/cjs/array-mapper-registry.js +0 -296
  133. package/dist/cjs/array-mapper-registry.js.map +0 -1
  134. package/dist/cjs/pagination-with-count.d.ts +0 -6
  135. package/dist/cjs/pagination-with-count.d.ts.map +0 -1
  136. package/dist/cjs/pagination-with-count.js +0 -63
  137. package/dist/cjs/pagination-with-count.js.map +0 -1
  138. package/dist/esm/array-mapper-registry.d.ts +0 -85
  139. package/dist/esm/array-mapper-registry.d.ts.map +0 -1
  140. package/dist/esm/array-mapper-registry.js +0 -292
  141. package/dist/esm/array-mapper-registry.js.map +0 -1
  142. package/dist/esm/pagination-with-count.d.ts +0 -6
  143. package/dist/esm/pagination-with-count.d.ts.map +0 -1
  144. package/dist/esm/pagination-with-count.js +0 -34
  145. package/dist/esm/pagination-with-count.js.map +0 -1
  146. package/src/array-mapper-registry.ts +0 -416
  147. package/src/pagination-with-count.ts +0 -89
@@ -34,11 +34,20 @@ var __generator = (this && this.__generator) || function (thisArg, body) {
34
34
  if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
35
35
  }
36
36
  };
37
+ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
38
+ if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
39
+ if (ar || !(i in from)) {
40
+ if (!ar) ar = Array.prototype.slice.call(from, 0, i);
41
+ ar[i] = from[i];
42
+ }
43
+ }
44
+ return to.concat(ar || Array.prototype.slice.call(from));
45
+ };
37
46
  import { ChunkType, FileType, } from '@teleporthq/teleport-types';
38
47
  import * as types from '@babel/types';
39
48
  import { parseExpression } from '@babel/parser';
40
49
  import { ASTStatementOrder, StringUtils } from '@teleporthq/teleport-shared';
41
- import { ASTUtils, URLSearchParamSync } from '@teleporthq/teleport-plugin-common';
50
+ import { ASTUtils, URLQueryWriter, URLSearchParamSync } from '@teleporthq/teleport-plugin-common';
42
51
  import { generateSafeFileName } from './utils';
43
52
  import { generateDataSourceFetcherWithCore } from './data-source-fetchers';
44
53
  import { buildProductTransformOptions, } from './transformations';
@@ -47,6 +56,12 @@ import { appendFiltersParam, pushStateIdsAsDeps, pushPropIdsAsDeps } from './fil
47
56
  import { applyLoadingStateToDataProvider, buildLoadingStateDeclarations, getLoadingStateVars, } from './loading-state';
48
57
  import { mergeServerCacheOptions, resolveCacheFromRepeaterContent } from './cache/config';
49
58
  import { buildCacheHydrationEffect, buildCacheStoreThen, buildCachedFetchBody, buildCachedParamsDeclarations, registerCacheClientImports, } from './cache/ast';
59
+ import { convertControlToButton, dropInactivePaginationControls, findPaginationControl, setJSXExpressionAttribute, } from './pagination-controls';
60
+ import { buildCountFetchEffect } from './count-effect';
61
+ import { buildPageTokensDeclaration, wireNumberedPagination, } from './numbered-pagination';
62
+ import { buildAccumulatingResponseHandler, buildInfiniteScrollDeclarations, buildSentinelElement, buildSentinelObserverEffect, getInfiniteScrollVars, } from './infinite-scroll';
63
+ import { buildFilterSortPageResetEffect, buildPageResetRefDeclaration, hasNoPageResetDeps, } from './page-reset';
64
+ import { buildPageUrlRefDeclaration, buildPageUrlSyncEffect } from './page-url-sync';
50
65
  // tslint:disable-next-line:no-any
51
66
  function parseSearchDefaultValue(raw) {
52
67
  if (!raw || typeof raw !== 'object') {
@@ -400,6 +415,17 @@ function buildStateRegistry(uidlNode) {
400
415
  content.searchUrlParamKey.trim().length > 0
401
416
  ? content.searchUrlParamKey.trim()
402
417
  : undefined;
418
+ // An accumulating list has no discrete pages, so it can neither jump
419
+ // between them nor put one in the URL. Normalising here — rather than at
420
+ // each of the ~6 emit sites — keeps every downstream branch able to trust
421
+ // `paginationMode` / `pageUrlParamKey` on their own.
422
+ var infiniteScroll = !!content.infiniteScroll;
423
+ var pageUrlParamKey = !infiniteScroll &&
424
+ typeof content.pageUrlParamKey === 'string' &&
425
+ content.pageUrlParamKey.trim().length > 0
426
+ ? content.pageUrlParamKey.trim()
427
+ : undefined;
428
+ var paginationMode = !infiniteScroll && content.paginationMode === 'numbered' ? 'numbered' : 'buttons';
403
429
  var usage = {
404
430
  index: index++,
405
431
  dataSourceIdentifier: parentDataSource.identifier,
@@ -423,6 +449,10 @@ function buildStateRegistry(uidlNode) {
423
449
  filterPropIds: filterPropIds,
424
450
  filterUrlSearchParamKeys: filterUrlSearchParamKeys,
425
451
  cache: resolveCacheFromRepeaterContent(content.cache, parentDataSource.resourceDef),
452
+ paginationMode: paginationMode,
453
+ infiniteScroll: infiniteScroll,
454
+ infiniteScrollLoadMore: infiniteScroll && !!content.infiniteScrollLoadMore,
455
+ pageUrlParamKey: pageUrlParamKey,
426
456
  category: 'plain',
427
457
  };
428
458
  // Determine category
@@ -514,6 +544,16 @@ function usageHasSearchUrlSync(usage) {
514
544
  return (!!usage.searchUrlParamKey &&
515
545
  (usage.category === 'paginated+search' || usage.category === 'search-only'));
516
546
  }
547
+ // The page ⇄ URL counterpart of `usageHasSearchUrlSync`: the usage must carry a
548
+ // `pageUrlParamKey` AND own a page state (only the paginated categories declare
549
+ // `ds_N_page` / `ds_N_state.page`). `infiniteScroll` usages are already excluded
550
+ // by the registry, which clears their key.
551
+ function usageHasPageUrlSync(usage) {
552
+ return (!!usage.pageUrlParamKey &&
553
+ usage.paginated &&
554
+ !usage.infiniteScroll &&
555
+ (usage.category === 'paginated+search' || usage.category === 'paginated-only'));
556
+ }
517
557
  // Emits the two URL-sync effects for a search input bound to a URL param
518
558
  // (`searchUrlParamKey`), reusing the shared `URLSearchParamSync` builders so the
519
559
  // search input behaves exactly like the `selectedCategory` / `sortBy` dropdowns:
@@ -553,10 +593,157 @@ function pushSearchUrlSyncEffects(effectStatements, usage, vars) {
553
593
  // debounce effect propagates the change into the debounced value + fetch.
554
594
  effectStatements.push(URLSearchParamSync.buildUrlReadBackEffect(usage.searchUrlParamKey, vars.setSearchQueryVar));
555
595
  }
596
+ // How one usage reads and writes its current page.
597
+ //
598
+ // A paginated+search list keeps the page inside a combined
599
+ // `{ page, debouncedQuery }` object so a search reset can replace both at once,
600
+ // while a paginated-only list has a plain `ds_N_page`. Every control that moves
601
+ // the page goes through this pair rather than re-deriving the distinction.
602
+ function getPageAccessors(usage, vars) {
603
+ var isCombinedState = usage.category === 'paginated+search';
604
+ return {
605
+ currentPageExpr: function () {
606
+ return isCombinedState
607
+ ? types.memberExpression(types.identifier(vars.combinedStateVar), types.identifier('page'))
608
+ : types.identifier(vars.pageStateVar);
609
+ },
610
+ // Functional updates that bail when the page is unchanged: clicking the
611
+ // page you are already on must not schedule a render, and with it a refetch.
612
+ buildSetPageStatement: function (pageExpr) {
613
+ return isCombinedState
614
+ ? types.callExpression(types.identifier(vars.setCombinedStateVar), [
615
+ types.arrowFunctionExpression([types.identifier('state')], types.conditionalExpression(types.binaryExpression('===', types.memberExpression(types.identifier('state'), types.identifier('page')), types.cloneNode(pageExpr, true)), types.identifier('state'), types.objectExpression([
616
+ types.spreadElement(types.identifier('state')),
617
+ types.objectProperty(types.identifier('page'), types.cloneNode(pageExpr, true)),
618
+ ]))),
619
+ ])
620
+ : types.callExpression(types.identifier(vars.setPageStateVar), [
621
+ types.arrowFunctionExpression([types.identifier('page')], types.conditionalExpression(types.binaryExpression('===', types.identifier('page'), types.cloneNode(pageExpr, true)), types.identifier('page'), types.cloneNode(pageExpr, true))),
622
+ ]);
623
+ },
624
+ };
625
+ }
626
+ /**
627
+ * Advance one page, unless the count already says this is the last one. Shared
628
+ * by the scroll sentinel and the Load More button.
629
+ *
630
+ * ⛔ The next page is derived from the updater's OWN argument, never from the
631
+ * page in scope. The sentinel's effect is re-armed on `hasMore` / `maxPages`,
632
+ * and neither changes on an append while the total is unknown — so a captured
633
+ * `ds_N_page` would still read 1 on the second intersection and the list would
634
+ * stop growing after one page, on exactly the sources that cannot be counted.
635
+ */
636
+ function buildAdvancePageStatement(usage, vars) {
637
+ var isCombinedState = usage.category === 'paginated+search';
638
+ var paramName = isCombinedState ? 'state' : 'page';
639
+ var pageOfParam = function () {
640
+ return isCombinedState
641
+ ? types.memberExpression(types.identifier(paramName), types.identifier('page'))
642
+ : types.identifier(paramName);
643
+ };
644
+ // maxPages > 0 && <param page> >= maxPages
645
+ var atLastPage = types.logicalExpression('&&', types.binaryExpression('>', types.identifier(vars.maxPagesStateVar), types.numericLiteral(0)), types.binaryExpression('>=', pageOfParam(), types.identifier(vars.maxPagesStateVar)));
646
+ var nextPage = types.binaryExpression('+', pageOfParam(), types.numericLiteral(1));
647
+ return types.expressionStatement(types.callExpression(types.identifier(isCombinedState ? vars.setCombinedStateVar : vars.setPageStateVar), [
648
+ types.arrowFunctionExpression([types.identifier(paramName)], types.conditionalExpression(atLastPage, types.identifier(paramName), isCombinedState
649
+ ? types.objectExpression([
650
+ types.spreadElement(types.identifier(paramName)),
651
+ types.objectProperty(types.identifier('page'), nextPage),
652
+ ])
653
+ : nextPage)),
654
+ ]));
655
+ }
656
+ // The accumulator, the end-of-data flag and (in auto mode) the sentinel ref,
657
+ // declared only for a mapper that appends.
658
+ function pushInfiniteScrollDeclarations(stateDeclarations, effectStatements, usage, vars) {
659
+ if (!usage.infiniteScroll || !usage.paginated) {
660
+ return;
661
+ }
662
+ var infiniteVars = getInfiniteScrollVars(usage.index);
663
+ var currentPageExpr = getPageAccessors(usage, vars).currentPageExpr;
664
+ var usesSentinel = !usage.infiniteScrollLoadMore;
665
+ stateDeclarations.push.apply(stateDeclarations, buildInfiniteScrollDeclarations(infiniteVars, vars.maxPagesStateVar, currentPageExpr(), usesSentinel));
666
+ if (usesSentinel) {
667
+ effectStatements.push(buildSentinelObserverEffect(infiniteVars, vars.maxPagesStateVar, function () {
668
+ return buildAdvancePageStatement(usage, vars);
669
+ }));
670
+ }
671
+ }
672
+ // The `useMemo` holding the visible page numbers, declared only for a mapper in
673
+ // numbered mode.
674
+ function pushPageTokensDeclaration(stateDeclarations, usage, vars) {
675
+ if (usage.paginationMode !== 'numbered' || !usage.paginated) {
676
+ return;
677
+ }
678
+ var currentPageExpr = getPageAccessors(usage, vars).currentPageExpr;
679
+ stateDeclarations.push(buildPageTokensDeclaration(usage.index, vars.maxPagesStateVar, currentPageExpr()));
680
+ }
681
+ // Emits the page ⇄ `?<key>=` sync for a mapper bound to `pageUrlParamKey` — the
682
+ // pagination counterpart of `pushSearchUrlSyncEffects`, but a SINGLE effect
683
+ // that owns both directions. `page-url-sync.ts` explains at length why the
684
+ // usual read-back / write-back pair cannot be used here.
685
+ //
686
+ // ⛔ MUST be pushed AFTER the filter/sort reset effect. Both can fire in one
687
+ // flush on a navigation that changes filter and page together; effects run in
688
+ // emission order, so this one running LAST is what makes the URL win when it
689
+ // carries a page, while a plain user filter change (whose URL has not moved
690
+ // yet) is left with the reset's page 1 and writes that back.
691
+ function pushPageUrlSyncEffects(stateDeclarations, effectStatements, usage, vars) {
692
+ if (!usageHasPageUrlSync(usage)) {
693
+ return;
694
+ }
695
+ var refVar = "ds_".concat(usage.index, "_pageUrlRef");
696
+ var _a = getPageAccessors(usage, vars), currentPageExpr = _a.currentPageExpr, buildSetPageStatement = _a.buildSetPageStatement;
697
+ stateDeclarations.push(buildPageUrlRefDeclaration(refVar));
698
+ effectStatements.push(buildPageUrlSyncEffect({
699
+ paramKey: usage.pageUrlParamKey,
700
+ refVar: refVar,
701
+ currentPageExpr: currentPageExpr,
702
+ buildSetPageStatement: buildSetPageStatement,
703
+ }));
704
+ }
705
+ // The name of the ref holding this usage's last-seen filter/sort signature.
706
+ function getPageResetRefVar(usage) {
707
+ return "ds_".concat(usage.index, "_filterResetRef");
708
+ }
709
+ // Everything that can narrow this list: filter destinations (state-, prop- and
710
+ // URL-bound) plus the state a dynamic sort reads. Sort does not change WHICH
711
+ // rows match, but it does change which row is on which page, so holding the
712
+ // visitor on page 5 across a sort change shows them an arbitrary window of a
713
+ // list they just reordered.
714
+ function collectPageResetDeps(usage) {
715
+ var _a, _b;
716
+ return {
717
+ stateIds: __spreadArray(__spreadArray([], usage.filterStateIds, true), ((_b = (_a = usage.dynamicSort) === null || _a === void 0 ? void 0 : _a.depStateIds) !== null && _b !== void 0 ? _b : []), true),
718
+ propIds: usage.filterPropIds,
719
+ urlSearchParamKeys: usage.filterUrlSearchParamKeys,
720
+ };
721
+ }
722
+ // Emits the "a filter or sort changed ⇒ go back to page 1" effect for one
723
+ // paginated usage. No-op for a list nothing can filter or reorder, so a mapper
724
+ // without filters generates byte-for-byte what it always did.
725
+ function pushFilterSortPageResetEffect(stateDeclarations, effectStatements, usage, vars) {
726
+ if (!usage.paginated) {
727
+ return;
728
+ }
729
+ var deps = collectPageResetDeps(usage);
730
+ if (hasNoPageResetDeps(deps)) {
731
+ return;
732
+ }
733
+ var refVar = getPageResetRefVar(usage);
734
+ stateDeclarations.push(buildPageResetRefDeclaration(refVar));
735
+ // The setter bails when the page is already 1 (see `getPageAccessors`), so a
736
+ // visitor who was on page 1 all along and merely ticked a category does not
737
+ // get an extra render — and with it an extra fetch.
738
+ var buildSetPageStatement = getPageAccessors(usage, vars).buildSetPageStatement;
739
+ effectStatements.push(buildFilterSortPageResetEffect(refVar, deps, function () {
740
+ return types.expressionStatement(buildSetPageStatement(types.numericLiteral(1)));
741
+ }));
742
+ }
556
743
  // ==================== MAIN PLUGIN ====================
557
744
  export var createNextArrayMapperPaginationPlugin = function () {
558
745
  var paginationPlugin = function (structure) { return __awaiter(void 0, void 0, void 0, function () {
559
- var uidl, chunks, dependencies, options, componentChunk, variableDeclaration, declarator, arrowFunction, blockStatement, registry, getStaticPropsChunk, isPage, stateDeclarations, effectStatements, needsUseRouter, hasRouterDecl, returnIndex, insertIndex, dataProviders, dataProvidersWithRepeaters, usageIndexByDataSourceId, loadingStateDeclarations, cacheDeclarations, sharedOptions, cachedScopes, dataProvidersWithoutRepeaters, searchInputs, searchEnabledUsages, paginationNodes, paginatedUsages;
746
+ var uidl, chunks, dependencies, options, componentChunk, variableDeclaration, declarator, arrowFunction, blockStatement, registry, getStaticPropsChunk, isPage, stateDeclarations, effectStatements, needsUseRouter, hasRouterDecl, returnIndex, insertIndex, dataProviders, dataProvidersWithRepeaters, usageIndexByDataSourceId, loadingStateDeclarations, usagesWithLoadingState, usageByProviderNode, cacheDeclarations, sharedOptions, cachedScopes, dataProvidersWithoutRepeaters, searchInputs, searchEnabledUsages;
560
747
  var _a, _b;
561
748
  return __generator(this, function (_c) {
562
749
  uidl = structure.uidl, chunks = structure.chunks, dependencies = structure.dependencies, options = structure.options;
@@ -688,11 +875,19 @@ export var createNextArrayMapperPaginationPlugin = function () {
688
875
  types.variableDeclaration('const', [
689
876
  types.variableDeclarator(types.identifier('timer'), types.callExpression(types.identifier('setTimeout'), [
690
877
  types.arrowFunctionExpression([], types.blockStatement([
878
+ // ⛔ Functional, and a no-op when the query has not
879
+ // actually moved. This effect fires on ANY change to
880
+ // the input state — including the URL read-back
881
+ // echoing back a value the visitor never typed (a
882
+ // browser Forward to `?searchKeyword=x&page=3`) — and
883
+ // replacing the whole object would silently drop the
884
+ // page it had just adopted, as well as remount the
885
+ // provider on a `key` that contains the query.
691
886
  types.expressionStatement(types.callExpression(types.identifier(vars.setCombinedStateVar), [
692
- types.objectExpression([
887
+ types.arrowFunctionExpression([types.identifier('state')], types.conditionalExpression(types.binaryExpression('===', types.memberExpression(types.identifier('state'), types.identifier('debouncedQuery')), types.identifier(vars.searchQueryVar)), types.identifier('state'), types.objectExpression([
693
888
  types.objectProperty(types.identifier('page'), types.numericLiteral(1)),
694
889
  types.objectProperty(types.identifier('debouncedQuery'), types.identifier(vars.searchQueryVar)),
695
- ]),
890
+ ]))),
696
891
  ])),
697
892
  ])),
698
893
  types.numericLiteral(usage.searchDebounce),
@@ -725,42 +920,6 @@ export var createNextArrayMapperPaginationPlugin = function () {
725
920
  }
726
921
  // Add filters to count fetch params if present
727
922
  appendFiltersParam(urlParams, usage.filters, buildFilterDestinationExpression);
728
- // Build the count fetch effect body
729
- var countFetchEffectBody = [];
730
- // Refresh the count on mount (no skip-on-mount guard). Pages seed maxPages
731
- // from the build-time `getStaticProps` count, but that snapshot goes stale:
732
- // rows added AFTER the build (e.g. the normalized product_variants table,
733
- // which is seeded/grows past one page during store generation) are not
734
- // reflected, so a frozen maxPages of 0/1 leaves "Next" permanently disabled
735
- // on data-heavy tables. Re-fetching the live count on mount corrects
736
- // maxPages so pagination tracks the real row count. The effect still runs
737
- // when the search query or a filter changes (see deps below).
738
- // Add the fetch call
739
- countFetchEffectBody.push(types.expressionStatement(types.callExpression(types.memberExpression(types.callExpression(types.memberExpression(types.callExpression(types.identifier('fetch'), [
740
- types.templateLiteral([
741
- types.templateElement({
742
- raw: "/api/".concat(fileName, "-count?"),
743
- cooked: "/api/".concat(fileName, "-count?"),
744
- }),
745
- types.templateElement({ raw: '', cooked: '' }),
746
- ], [
747
- types.newExpression(types.identifier('URLSearchParams'), [
748
- types.objectExpression(urlParams),
749
- ]),
750
- ]),
751
- ]), types.identifier('then')), [
752
- types.arrowFunctionExpression([types.identifier('res')], types.callExpression(types.memberExpression(types.identifier('res'), types.identifier('json')), [])),
753
- ]), types.identifier('then')), [
754
- types.arrowFunctionExpression([types.identifier('data')], types.blockStatement([
755
- types.ifStatement(types.logicalExpression('&&', types.identifier('data'), types.binaryExpression('in', types.stringLiteral('count'), types.identifier('data'))), types.blockStatement([
756
- types.expressionStatement(types.callExpression(types.identifier(vars.setMaxPagesStateVar), [
757
- types.conditionalExpression(types.binaryExpression('===', types.memberExpression(types.identifier('data'), types.identifier('count')), types.numericLiteral(0)), types.numericLiteral(0), types.callExpression(types.memberExpression(types.identifier('Math'), types.identifier('ceil')), [
758
- types.binaryExpression('/', types.memberExpression(types.identifier('data'), types.identifier('count')), types.numericLiteral(usage.perPage)),
759
- ])),
760
- ])),
761
- ])),
762
- ])),
763
- ])));
764
923
  var countEffectDeps = [
765
924
  types.memberExpression(types.identifier(vars.combinedStateVar), types.identifier('debouncedQuery')),
766
925
  ];
@@ -775,12 +934,24 @@ export var createNextArrayMapperPaginationPlugin = function () {
775
934
  pushPropIdsAsDeps(countEffectDeps, countEffectSeen, usage.filterPropIds);
776
935
  // Same goes for URL-driven filters (already documented above).
777
936
  pushUrlSearchParamMemoDeps(countEffectDeps, usage);
778
- effectStatements.push(types.expressionStatement(types.callExpression(types.identifier('useEffect'), [
779
- types.arrowFunctionExpression([], types.blockStatement(countFetchEffectBody)),
780
- types.arrayExpression(countEffectDeps),
781
- ])));
937
+ effectStatements.push(buildCountFetchEffect({
938
+ fileName: fileName,
939
+ perPage: usage.perPage,
940
+ setMaxPagesVar: vars.setMaxPagesStateVar,
941
+ urlParams: urlParams,
942
+ deps: countEffectDeps,
943
+ }));
782
944
  // Two-way URL sync for the search input when bound to a URL param.
783
945
  pushSearchUrlSyncEffects(effectStatements, usage, vars);
946
+ // Visible page numbers, when this mapper draws a numbered strip.
947
+ pushPageTokensDeclaration(stateDeclarations, usage, vars);
948
+ // Accumulation state, when it appends instead of paging.
949
+ pushInfiniteScrollDeclarations(stateDeclarations, effectStatements, usage, vars);
950
+ // A filter or sort change invalidates the page the visitor is on.
951
+ pushFilterSortPageResetEffect(stateDeclarations, effectStatements, usage, vars);
952
+ // Ordering matters — see `pushPageUrlSyncEffects`: the read-back runs
953
+ // after the reset so a URL that names a page wins over it.
954
+ pushPageUrlSyncEffects(stateDeclarations, effectStatements, usage, vars);
784
955
  }
785
956
  else if (usage.category === 'paginated-only') {
786
957
  // Simple page state
@@ -799,42 +970,52 @@ export var createNextArrayMapperPaginationPlugin = function () {
799
970
  types.identifier(vars.setPageStateVar),
800
971
  ]), types.callExpression(types.identifier('useState'), [types.numericLiteral(1)])),
801
972
  ]));
802
- // For components (not pages), add a useEffect to fetch count on mount
803
- // Pages get count from getStaticProps, but components need to fetch it client-side
804
- if (!isPage) {
973
+ // The live row count behind `ds_N_maxPages`.
974
+ //
975
+ // Components have never had a build-time count to fall back on, so they
976
+ // have always fetched one here. Pages seed theirs from `getStaticProps`,
977
+ // which only knows the STATIC filters — good enough to enable/disable a
978
+ // "Next" button, but not to label a numbered strip or to know when an
979
+ // infinite list has reached its end, so those two modes fetch a live
980
+ // count as well.
981
+ var needsLiveCount = !isPage || usage.paginationMode === 'numbered' || usage.infiniteScroll;
982
+ if (needsLiveCount) {
805
983
  var fileName = generateSafeFileName(usage.resourceDefinition.dataSourceType, usage.resourceDefinition.tableName, usage.resourceDefinition.dataSourceId);
806
- effectStatements.push(types.expressionStatement(types.callExpression(types.identifier('useEffect'), [
807
- types.arrowFunctionExpression([], types.blockStatement([
808
- types.expressionStatement(types.callExpression(types.memberExpression(types.callExpression(types.memberExpression(types.callExpression(types.identifier('fetch'), [
809
- types.stringLiteral("/api/".concat(fileName, "-count")),
810
- ]), types.identifier('then')), [
811
- types.arrowFunctionExpression([types.identifier('res')], types.callExpression(types.memberExpression(types.identifier('res'), types.identifier('json')), [])),
812
- ]), types.identifier('then')), [
813
- types.arrowFunctionExpression([types.identifier('data')], types.blockStatement([
814
- types.ifStatement(types.logicalExpression('&&', types.identifier('data'), types.binaryExpression('in', types.stringLiteral('count'), types.identifier('data'))), types.blockStatement([
815
- types.expressionStatement(types.callExpression(types.identifier(vars.setMaxPagesStateVar), [
816
- types.conditionalExpression(types.binaryExpression('===', types.memberExpression(types.identifier('data'), types.identifier('count')), types.numericLiteral(0)), types.numericLiteral(0), types.callExpression(types.memberExpression(types.identifier('Math'), types.identifier('ceil')), [
817
- types.binaryExpression('/', types.memberExpression(types.identifier('data'), types.identifier('count')), types.numericLiteral(usage.perPage)),
818
- ])),
819
- ])),
820
- ])),
821
- ])),
822
- ])),
823
- ])),
824
- // Default to mount-only; refresh when ANY filter destination
825
- // changes — state-bound (e.g. `selectedCategory`) and
826
- // URL-driven — so the pagination control reflects the current
827
- // filtered count instead of the unfiltered mount-time total.
828
- types.arrayExpression((function () {
829
- var deps = [];
830
- var depsSeen = new Set();
831
- pushStateIdsAsDeps(deps, depsSeen, usage.filterStateIds);
832
- pushPropIdsAsDeps(deps, depsSeen, usage.filterPropIds);
833
- pushUrlSearchParamMemoDeps(deps, usage);
834
- return deps;
835
- })()),
836
- ])));
984
+ // ⛔ This list has no search input, so no `query` param — but it may
985
+ // well have filters, and omitting them (as this call site did until
986
+ // the shared builder replaced it) counts the WHOLE table: "Next"
987
+ // stayed enabled far past the end of a filtered result set.
988
+ var countUrlParams = [];
989
+ if (usage.queryColumns.length > 0) {
990
+ countUrlParams.push(types.objectProperty(types.identifier('queryColumns'), types.callExpression(types.memberExpression(types.identifier('JSON'), types.identifier('stringify')), [types.arrayExpression(usage.queryColumns.map(function (c) { return types.stringLiteral(c); }))])));
991
+ }
992
+ appendFiltersParam(countUrlParams, usage.filters, buildFilterDestinationExpression);
993
+ // Default to mount-only; refresh when ANY filter destination
994
+ // changes — state-bound (e.g. `selectedCategory`) and URL-driven —
995
+ // so the pagination control reflects the current filtered count
996
+ // instead of the unfiltered mount-time total.
997
+ var countDeps = [];
998
+ var countDepsSeen = new Set();
999
+ pushStateIdsAsDeps(countDeps, countDepsSeen, usage.filterStateIds);
1000
+ pushPropIdsAsDeps(countDeps, countDepsSeen, usage.filterPropIds);
1001
+ pushUrlSearchParamMemoDeps(countDeps, usage);
1002
+ effectStatements.push(buildCountFetchEffect({
1003
+ fileName: fileName,
1004
+ perPage: usage.perPage,
1005
+ setMaxPagesVar: vars.setMaxPagesStateVar,
1006
+ urlParams: countUrlParams,
1007
+ deps: countDeps,
1008
+ }));
837
1009
  }
1010
+ // Visible page numbers, when this mapper draws a numbered strip.
1011
+ pushPageTokensDeclaration(stateDeclarations, usage, vars);
1012
+ // Accumulation state, when it appends instead of paging.
1013
+ pushInfiniteScrollDeclarations(stateDeclarations, effectStatements, usage, vars);
1014
+ // A filter or sort change invalidates the page the visitor is on.
1015
+ pushFilterSortPageResetEffect(stateDeclarations, effectStatements, usage, vars);
1016
+ // Ordering matters — see `pushPageUrlSyncEffects`: the read-back runs
1017
+ // after the reset so a URL that names a page wins over it.
1018
+ pushPageUrlSyncEffects(stateDeclarations, effectStatements, usage, vars);
838
1019
  }
839
1020
  else if (usage.category === 'search-only') {
840
1021
  // Search-only state
@@ -880,7 +1061,7 @@ export var createNextArrayMapperPaginationPlugin = function () {
880
1061
  });
881
1062
  // Insert state declarations at the beginning
882
1063
  stateDeclarations.reverse().forEach(function (s) { return blockStatement.body.unshift(s); });
883
- needsUseRouter = registry.usages.some(function (u) { return hasUrlSearchParamFilters(u) || usageHasSearchUrlSync(u); });
1064
+ needsUseRouter = registry.usages.some(function (u) { return hasUrlSearchParamFilters(u) || usageHasSearchUrlSync(u) || usageHasPageUrlSync(u); });
884
1065
  if (needsUseRouter) {
885
1066
  if (!dependencies.useRouter) {
886
1067
  // Match the shape the sibling Next.js plugins use (i18n locale mapper,
@@ -910,6 +1091,13 @@ export var createNextArrayMapperPaginationPlugin = function () {
910
1091
  ]));
911
1092
  }
912
1093
  }
1094
+ // Both the search write-back and the page ⇄ URL effect write the URL through
1095
+ // the shared `URLQueryWriter`, so the ref + helper they close over must be in
1096
+ // the body. Idempotent with `createNextUrlSearchParamsPlugin`, which needs the
1097
+ // same pair for the category/sort dropdowns and may have run first.
1098
+ if (registry.usages.some(function (u) { return usageHasSearchUrlSync(u) || usageHasPageUrlSync(u); })) {
1099
+ URLQueryWriter.ensureQuerySyncDeclarations(blockStatement.body);
1100
+ }
913
1101
  returnIndex = blockStatement.body.findIndex(function (s) { return s.type === 'ReturnStatement'; });
914
1102
  insertIndex = returnIndex !== -1 ? returnIndex : blockStatement.body.length;
915
1103
  effectStatements.reverse().forEach(function (e) { return blockStatement.body.splice(insertIndex, 0, e); });
@@ -920,6 +1108,8 @@ export var createNextArrayMapperPaginationPlugin = function () {
920
1108
  });
921
1109
  usageIndexByDataSourceId = new Map();
922
1110
  loadingStateDeclarations = [];
1111
+ usagesWithLoadingState = new Set();
1112
+ usageByProviderNode = new Map();
923
1113
  cacheDeclarations = [];
924
1114
  dataProvidersWithRepeaters.forEach(function (dp) {
925
1115
  var _a, _b;
@@ -936,6 +1126,7 @@ export var createNextArrayMapperPaginationPlugin = function () {
936
1126
  }
937
1127
  var usage = usages[currentIndex];
938
1128
  usageIndexByDataSourceId.set(dataSourceIdentifier, currentIndex + 1);
1129
+ usageByProviderNode.set(dp, usage);
939
1130
  var vars = getStateVarsForUsage(usage);
940
1131
  var fileName = generateSafeFileName(usage.resourceDefinition.dataSourceType, usage.resourceDefinition.tableName, usage.resourceDefinition.dataSourceId);
941
1132
  // Update DataProvider based on category
@@ -958,6 +1149,15 @@ export var createNextArrayMapperPaginationPlugin = function () {
958
1149
  var loadingVars = getLoadingStateVars(usage.index);
959
1150
  if (applyLoadingStateToDataProvider(dp, loadingVars)) {
960
1151
  loadingStateDeclarations.push.apply(loadingStateDeclarations, buildLoadingStateDeclarations(loadingVars));
1152
+ // Recorded because the loading state is only declared for a provider
1153
+ // that HAS a loading slot; a Load More button wired later may only
1154
+ // reference `ds_N_isFetching` if it actually exists.
1155
+ usagesWithLoadingState.add(usage.index);
1156
+ }
1157
+ // An auto-loading list needs something at the end of the rows for its
1158
+ // IntersectionObserver to watch.
1159
+ if (usage.infiniteScroll && usage.paginated && !usage.infiniteScrollLoadMore) {
1160
+ insertSentinelAfterProvider(blockStatement, dp, buildSentinelElement(getInfiniteScrollVars(usage.index)));
961
1161
  }
962
1162
  // Create API route for all categories (including 'plain' for components)
963
1163
  ensureAPIRouteExists(options.extractedResources, usage, options.dataSources, buildProductTransformOptions(options));
@@ -1009,16 +1209,23 @@ export var createNextArrayMapperPaginationPlugin = function () {
1009
1209
  var vars = getStateVarsForUsage(usage);
1010
1210
  wireSearchInput(input.node, vars);
1011
1211
  });
1012
- paginationNodes = findAllPaginationNodesInJSX(blockStatement);
1013
- paginatedUsages = registry.usages.filter(function (u) { return u.paginated; });
1014
- // Match by order - pagination node 0 -> paginatedUsages[0], etc.
1015
- paginationNodes.forEach(function (paginationNode, idx) {
1016
- if (idx >= paginatedUsages.length) {
1212
+ // STEP 5: Wire pagination widgets
1213
+ //
1214
+ // Each widget belongs to the mapper whose `DataProvider` it follows in the
1215
+ // document — the builder emits it as a sibling of (or inside) that provider.
1216
+ //
1217
+ // ⛔ Not a positional zip of "widgets" against "paginated usages": the two
1218
+ // lists no longer have to be the same length, because an infinite-scroll
1219
+ // mapper renders no widget. Counting one for it would shift every later
1220
+ // mapper onto the wrong usage and wire one list's buttons to another list's
1221
+ // state. Anchoring on the provider cannot drift for the same reason the
1222
+ // provider loop above cannot.
1223
+ findPaginationNodesWithOwners(blockStatement, usageByProviderNode).forEach(function (_a) {
1224
+ var node = _a.node, usage = _a.usage;
1225
+ if (!usage) {
1017
1226
  return;
1018
1227
  }
1019
- var usage = paginatedUsages[idx];
1020
- var vars = getStateVarsForUsage(usage);
1021
- wirePaginationButtons(paginationNode.node, usage, vars);
1228
+ wirePaginationWidget(node, usage, getStateVarsForUsage(usage), usagesWithLoadingState.has(usage.index));
1022
1229
  });
1023
1230
  // STEP 6: Update getStaticProps if this is a page
1024
1231
  if (isPage) {
@@ -1030,6 +1237,57 @@ export var createNextArrayMapperPaginationPlugin = function () {
1030
1237
  return paginationPlugin;
1031
1238
  };
1032
1239
  // ==================== HELPER FUNCTIONS ====================
1240
+ /**
1241
+ * Inserts the scroll sentinel immediately after a `DataProvider`, as its next
1242
+ * sibling, so it sits below the rendered rows.
1243
+ *
1244
+ * It has to be a sibling: the provider's own child is a render-prop function,
1245
+ * and anything placed inside it would be re-created on every fetch — the
1246
+ * observer would then be watching an element that is no longer in the document.
1247
+ *
1248
+ * Returns false when the provider has no element parent to splice into (a
1249
+ * provider returned directly from the component), leaving the list on its
1250
+ * count-driven `hasMore` rather than injecting a stray node.
1251
+ */
1252
+ function insertSentinelAfterProvider(blockStatement, provider, sentinel) {
1253
+ var inserted = false;
1254
+ var traverse = function (node) {
1255
+ if (!node || inserted) {
1256
+ return;
1257
+ }
1258
+ if (Array.isArray(node.children)) {
1259
+ var at = node.children.indexOf(provider);
1260
+ if (at !== -1) {
1261
+ node.children.splice(at + 1, 0, sentinel);
1262
+ inserted = true;
1263
+ return;
1264
+ }
1265
+ node.children.forEach(traverse);
1266
+ }
1267
+ if (node.body) {
1268
+ if (Array.isArray(node.body)) {
1269
+ node.body.forEach(traverse);
1270
+ }
1271
+ else {
1272
+ traverse(node.body);
1273
+ }
1274
+ }
1275
+ if (node.consequent) {
1276
+ traverse(node.consequent);
1277
+ }
1278
+ if (node.alternate) {
1279
+ traverse(node.alternate);
1280
+ }
1281
+ if (node.expression) {
1282
+ traverse(node.expression);
1283
+ }
1284
+ if (node.argument) {
1285
+ traverse(node.argument);
1286
+ }
1287
+ };
1288
+ traverse(blockStatement);
1289
+ return inserted;
1290
+ }
1033
1291
  function findAllDataProvidersInJSX(blockStatement) {
1034
1292
  var results = [];
1035
1293
  var traverse = function (node) {
@@ -1149,18 +1407,32 @@ function findAllSearchInputsInJSX(blockStatement) {
1149
1407
  traverse(blockStatement);
1150
1408
  return results;
1151
1409
  }
1152
- function findAllPaginationNodesInJSX(blockStatement) {
1410
+ /**
1411
+ * Every pagination widget in the JSX, paired with the mapper it belongs to.
1412
+ *
1413
+ * Ownership follows document order: a widget belongs to the most recent
1414
+ * `DataProvider` seen before it, which is exactly how the builder emits it —
1415
+ * as a sibling that follows its provider, or nested inside that provider's
1416
+ * subtree. A widget that precedes every provider (nothing sensible to wire it
1417
+ * to) comes back with no owner and is left alone.
1418
+ */
1419
+ function findPaginationNodesWithOwners(blockStatement, usageByProviderNode) {
1153
1420
  var results = [];
1421
+ var currentUsage;
1154
1422
  var traverse = function (node) {
1155
1423
  var _a, _b, _c, _d, _e;
1156
1424
  if (!node) {
1157
1425
  return;
1158
1426
  }
1159
1427
  if (node.type === 'JSXElement') {
1428
+ var owner = usageByProviderNode.get(node);
1429
+ if (owner) {
1430
+ currentUsage = owner;
1431
+ }
1160
1432
  var classAttr = (_b = (_a = node.openingElement) === null || _a === void 0 ? void 0 : _a.attributes) === null || _b === void 0 ? void 0 : _b.find(function (attr) { return attr.type === 'JSXAttribute' && attr.name.name === 'className'; });
1161
1433
  var className = ((_c = classAttr === null || classAttr === void 0 ? void 0 : classAttr.value) === null || _c === void 0 ? void 0 : _c.value) || ((_e = (_d = classAttr === null || classAttr === void 0 ? void 0 : classAttr.value) === null || _d === void 0 ? void 0 : _d.expression) === null || _e === void 0 ? void 0 : _e.value) || '';
1162
1434
  if (className.includes('cms-pagination-node')) {
1163
- results.push({ node: node, className: className });
1435
+ results.push({ node: node, usage: currentUsage });
1164
1436
  }
1165
1437
  }
1166
1438
  if (node.children && Array.isArray(node.children)) {
@@ -1223,7 +1495,6 @@ dp, usage, paramsProps, memoDeps, cacheDeclarations) {
1223
1495
  return hoisted.cachedIdentifier;
1224
1496
  }
1225
1497
  function updateDataProviderForPaginatedSearch(dp, usage, vars, fileName, cacheDeclarations) {
1226
- var _a;
1227
1498
  var attrs = dp.openingElement.attributes;
1228
1499
  // Remove existing params, key, initialData, fetchData, persistDataDuringLoading
1229
1500
  dp.openingElement.attributes = attrs.filter(function (attr) {
@@ -1259,7 +1530,12 @@ function updateDataProviderForPaginatedSearch(dp, usage, vars, fileName, cacheDe
1259
1530
  // the first client fetch (its internal guard only skips when initialData is
1260
1531
  // defined on mount). Without that skip, toggling sort correctly triggers a
1261
1532
  // refetch via the useMemo params dependency chain.
1262
- if (!usage.dynamicSort) {
1533
+ //
1534
+ // An appending list skips it for a second reason: `initialData` makes
1535
+ // DataProvider skip its first fetch, so page 1 would render from the prefetch
1536
+ // and never enter the accumulator — appending page 2 would then REPLACE the
1537
+ // visible rows with page 2 alone.
1538
+ if (!usage.dynamicSort && !usage.infiniteScroll) {
1263
1539
  var initialDataCondition = wrapInitialDataWithUrlFilterGuard(types.logicalExpression('&&', types.binaryExpression('===', types.memberExpression(types.identifier(vars.combinedStateVar), types.identifier('page')), types.numericLiteral(1)), types.unaryExpression('!', types.memberExpression(types.identifier(vars.combinedStateVar), types.identifier('debouncedQuery')), true)), usage);
1264
1540
  dp.openingElement.attributes.push(types.jsxAttribute(types.jsxIdentifier('initialData'), types.jsxExpressionContainer(types.conditionalExpression(initialDataCondition, types.optionalMemberExpression(types.identifier('props'), types.identifier(vars.propsPrefix), false, true), cachedInitialData ? types.identifier(cachedInitialData) : types.identifier('undefined')))));
1265
1541
  }
@@ -1268,24 +1544,37 @@ function updateDataProviderForPaginatedSearch(dp, usage, vars, fileName, cacheDe
1268
1544
  // "skip-first-fetch-when-we-have-initialData" guard, which would prevent the
1269
1545
  // new sort params from reaching the fetcher. Leaving sort out of the key
1270
1546
  // lets the useMemo params identity change alone drive refetch.
1271
- dp.openingElement.attributes.push(types.jsxAttribute(types.jsxIdentifier('key'), types.jsxExpressionContainer(types.templateLiteral([
1272
- types.templateElement({
1273
- raw: "".concat(usage.dataSourceIdentifier, "-"),
1274
- cooked: "".concat(usage.dataSourceIdentifier, "-"),
1275
- }),
1276
- types.templateElement({ raw: '-', cooked: '-' }),
1277
- types.templateElement({ raw: '', cooked: '' }),
1278
- ], [
1279
- types.memberExpression(types.identifier(vars.combinedStateVar), types.identifier('page')),
1280
- types.memberExpression(types.identifier(vars.combinedStateVar), types.identifier('debouncedQuery')),
1281
- ]))));
1547
+ //
1548
+ // ⛔ The page is left out too when the list APPENDS: remounting on every
1549
+ // appended page would unmount the accumulated rows and refetch from scratch.
1550
+ // The query stays in the key, so a new search still starts a clean list.
1551
+ dp.openingElement.attributes.push(types.jsxAttribute(types.jsxIdentifier('key'), types.jsxExpressionContainer(usage.infiniteScroll
1552
+ ? types.templateLiteral([
1553
+ types.templateElement({
1554
+ raw: "".concat(usage.dataSourceIdentifier, "-"),
1555
+ cooked: "".concat(usage.dataSourceIdentifier, "-"),
1556
+ }),
1557
+ types.templateElement({ raw: '', cooked: '' }),
1558
+ ], [
1559
+ types.memberExpression(types.identifier(vars.combinedStateVar), types.identifier('debouncedQuery')),
1560
+ ])
1561
+ : types.templateLiteral([
1562
+ types.templateElement({
1563
+ raw: "".concat(usage.dataSourceIdentifier, "-"),
1564
+ cooked: "".concat(usage.dataSourceIdentifier, "-"),
1565
+ }),
1566
+ types.templateElement({ raw: '-', cooked: '-' }),
1567
+ types.templateElement({ raw: '', cooked: '' }),
1568
+ ], [
1569
+ types.memberExpression(types.identifier(vars.combinedStateVar), types.identifier('page')),
1570
+ types.memberExpression(types.identifier(vars.combinedStateVar), types.identifier('debouncedQuery')),
1571
+ ]))));
1282
1572
  // Add fetchData
1283
- dp.openingElement.attributes.push(createFetchDataAttribute(fileName, ((_a = usage.cache) === null || _a === void 0 ? void 0 : _a.client) ? usage.cache : undefined));
1573
+ dp.openingElement.attributes.push(buildFetchDataForUsage(fileName, usage));
1284
1574
  // Add persistDataDuringLoading
1285
1575
  dp.openingElement.attributes.push(types.jsxAttribute(types.jsxIdentifier('persistDataDuringLoading'), types.jsxExpressionContainer(types.booleanLiteral(true))));
1286
1576
  }
1287
1577
  function updateDataProviderForPaginationOnly(dp, usage, vars, fileName, cacheDeclarations) {
1288
- var _a;
1289
1578
  var attrs = dp.openingElement.attributes;
1290
1579
  dp.openingElement.attributes = attrs.filter(function (attr) {
1291
1580
  var _a;
@@ -1312,20 +1601,26 @@ function updateDataProviderForPaginationOnly(dp, usage, vars, fileName, cacheDec
1312
1601
  // Add params
1313
1602
  var cachedInitialData = pushParamsAttribute(dp, usage, paramsProps, memoDeps, cacheDeclarations);
1314
1603
  // Add initialData. See paginated+search updater for why we skip prefetch
1315
- // reuse when a dynamic state-bound sort is active.
1316
- if (!usage.dynamicSort) {
1604
+ // reuse when a dynamic state-bound sort is active, and why an appending list
1605
+ // skips it too.
1606
+ if (!usage.dynamicSort && !usage.infiniteScroll) {
1317
1607
  dp.openingElement.attributes.push(types.jsxAttribute(types.jsxIdentifier('initialData'), types.jsxExpressionContainer(types.conditionalExpression(wrapInitialDataWithUrlFilterGuard(types.binaryExpression('===', types.identifier(vars.pageStateVar), types.numericLiteral(1)), usage), types.optionalMemberExpression(types.identifier('props'), types.identifier(vars.propsPrefix), false, true), cachedInitialData ? types.identifier(cachedInitialData) : types.identifier('undefined')))));
1318
1608
  }
1319
- // Add key — sort is intentionally NOT included; see paginated+search updater.
1320
- dp.openingElement.attributes.push(types.jsxAttribute(types.jsxIdentifier('key'), types.jsxExpressionContainer(types.templateLiteral([
1321
- types.templateElement({
1322
- raw: "".concat(usage.dataSourceIdentifier, "-page-"),
1323
- cooked: "".concat(usage.dataSourceIdentifier, "-page-"),
1324
- }),
1325
- types.templateElement({ raw: '', cooked: '' }),
1326
- ], [types.identifier(vars.pageStateVar)]))));
1609
+ // Add key — sort is intentionally NOT included; see paginated+search updater,
1610
+ // which also explains why an appending list keys on nothing at all (there is
1611
+ // no query here to vary it by; a filter change restarts the accumulator
1612
+ // through its signature instead).
1613
+ if (!usage.infiniteScroll) {
1614
+ dp.openingElement.attributes.push(types.jsxAttribute(types.jsxIdentifier('key'), types.jsxExpressionContainer(types.templateLiteral([
1615
+ types.templateElement({
1616
+ raw: "".concat(usage.dataSourceIdentifier, "-page-"),
1617
+ cooked: "".concat(usage.dataSourceIdentifier, "-page-"),
1618
+ }),
1619
+ types.templateElement({ raw: '', cooked: '' }),
1620
+ ], [types.identifier(vars.pageStateVar)]))));
1621
+ }
1327
1622
  // Add fetchData
1328
- dp.openingElement.attributes.push(createFetchDataAttribute(fileName, ((_a = usage.cache) === null || _a === void 0 ? void 0 : _a.client) ? usage.cache : undefined));
1623
+ dp.openingElement.attributes.push(buildFetchDataForUsage(fileName, usage));
1329
1624
  dp.openingElement.attributes.push(types.jsxAttribute(types.jsxIdentifier('persistDataDuringLoading'), types.jsxExpressionContainer(types.booleanLiteral(true))));
1330
1625
  }
1331
1626
  function updateDataProviderForSearchOnly(dp, usage, vars, fileName, cacheDeclarations) {
@@ -1458,7 +1753,10 @@ function stabilizeDataProviderWithoutRepeater(dp) {
1458
1753
  // Replace the params attribute
1459
1754
  attrs[paramsAttrIndex] = types.jsxAttribute(types.jsxIdentifier('params'), types.jsxExpressionContainer(memoizedParams));
1460
1755
  }
1461
- function createFetchDataAttribute(fileName, cache) {
1756
+ function createFetchDataAttribute(fileName, cache,
1757
+ // When set, the resolved value is every page fetched so far rather than just
1758
+ // the page that was requested — see `infinite-scroll.ts`.
1759
+ accumulate) {
1462
1760
  var fetchCall = types.callExpression(types.identifier('fetch'), [
1463
1761
  types.templateLiteral([
1464
1762
  types.templateElement({
@@ -1479,7 +1777,16 @@ function createFetchDataAttribute(fileName, cache) {
1479
1777
  // The uncached tail is left exactly as it has always been emitted, so a
1480
1778
  // project that does not cache produces byte-identical output.
1481
1779
  var uncachedTail = types.arrowFunctionExpression([types.identifier('response')], types.optionalMemberExpression(types.identifier('response'), types.identifier('data'), false, true));
1482
- var networkChain = types.callExpression(types.memberExpression(parsedJson, types.identifier('then')), [cache ? buildCacheStoreThen(cache) : uncachedTail]);
1780
+ // ⛔ Accumulation and the client cache are mutually exclusive: a cache hit
1781
+ // short-circuits the fetch and hands back one page's rows, which would drop
1782
+ // every page already accumulated. The caller suppresses the client cache for
1783
+ // an appending list, so this branch never sees both.
1784
+ var responseTail = accumulate
1785
+ ? buildAccumulatingResponseHandler(getInfiniteScrollVars(accumulate.usage.index), accumulate.usage.perPage)
1786
+ : cache
1787
+ ? buildCacheStoreThen(cache)
1788
+ : uncachedTail;
1789
+ var networkChain = types.callExpression(types.memberExpression(parsedJson, types.identifier('then')), [responseTail]);
1483
1790
  var arrow = cache
1484
1791
  ? types.arrowFunctionExpression([types.identifier('params')], buildCachedFetchBody(networkChain, cache))
1485
1792
  : types.arrowFunctionExpression([types.identifier('params')], networkChain);
@@ -1495,45 +1802,91 @@ function wireSearchInput(inputNode, vars) {
1495
1802
  // Add value
1496
1803
  inputNode.openingElement.attributes.push(types.jsxAttribute(types.jsxIdentifier('value'), types.jsxExpressionContainer(types.identifier(vars.searchQueryVar))));
1497
1804
  }
1805
+ /**
1806
+ * The `fetchData` attribute for one paginated usage.
1807
+ *
1808
+ * ⛔ An appending list gets no CLIENT cache: a cache hit resolves with one
1809
+ * page's rows without going through the accumulator, which would silently
1810
+ * discard every page already loaded. The SERVER cache is untouched — it sits
1811
+ * behind the API route and returns the same page either way.
1812
+ */
1813
+ function buildFetchDataForUsage(fileName, usage) {
1814
+ var _a;
1815
+ if (usage.infiniteScroll) {
1816
+ return createFetchDataAttribute(fileName, undefined, { usage: usage });
1817
+ }
1818
+ return createFetchDataAttribute(fileName, ((_a = usage.cache) === null || _a === void 0 ? void 0 : _a.client) ? usage.cache : undefined);
1819
+ }
1820
+ /**
1821
+ * Wires the authored Load More button of an appending list.
1822
+ *
1823
+ * Disabled — not hidden — when there is nothing left to load, so the author's
1824
+ * styling of the finished state is theirs to decide, and so the control does
1825
+ * not vanish from under the visitor's cursor mid-click.
1826
+ */
1827
+ function wireLoadMoreButton(paginationNode, usage, vars, hasLoadingState) {
1828
+ if (!usage.infiniteScrollLoadMore) {
1829
+ return;
1830
+ }
1831
+ var button = findPaginationControl(paginationNode, 'load-more');
1832
+ if (!button) {
1833
+ return;
1834
+ }
1835
+ var infiniteVars = getInfiniteScrollVars(usage.index);
1836
+ convertControlToButton(button);
1837
+ setJSXExpressionAttribute(button, 'onClick', types.arrowFunctionExpression([],
1838
+ // The advance statement is an expression statement; the handler needs the
1839
+ // expression itself.
1840
+ buildAdvancePageStatement(usage, vars).expression));
1841
+ // The in-flight term is only referenced when the provider declared it —
1842
+ // a mapper with no loading branch never gets `ds_N_isFetching`.
1843
+ var notHasMore = types.unaryExpression('!', types.identifier(infiniteVars.hasMoreVar), true);
1844
+ setJSXExpressionAttribute(button, 'disabled', hasLoadingState
1845
+ ? types.logicalExpression('||', notHasMore, types.identifier(getLoadingStateVars(usage.index).isFetchingVar))
1846
+ : notHasMore);
1847
+ }
1848
+ /**
1849
+ * Wires one mapper's pagination container according to the mode it is in, and
1850
+ * removes the controls belonging to the other modes.
1851
+ *
1852
+ * The builder never deletes a control when the author switches mode — doing so
1853
+ * would throw away their styling — so the container can hold a superset of what
1854
+ * any one mode needs. Pruning here is what keeps a Previous/Next list from
1855
+ * rendering a dead numbered strip, and an infinite list from rendering page
1856
+ * buttons beside its Load More.
1857
+ */
1858
+ function wirePaginationWidget(paginationNode, usage, vars, hasLoadingState) {
1859
+ if (usage.infiniteScroll) {
1860
+ // Only the Load More button survives; page controls make no sense for a
1861
+ // list the visitor never leaves.
1862
+ dropInactivePaginationControls(paginationNode, ['load-more']);
1863
+ wireLoadMoreButton(paginationNode, usage, vars, hasLoadingState);
1864
+ return;
1865
+ }
1866
+ var activeKinds = usage.paginationMode === 'numbered'
1867
+ ? ['first', 'previous', 'pages', 'page', 'ellipsis', 'next', 'last']
1868
+ : ['previous', 'next'];
1869
+ dropInactivePaginationControls(paginationNode, activeKinds);
1870
+ wirePaginationButtons(paginationNode, usage, vars);
1871
+ if (usage.paginationMode === 'numbered') {
1872
+ var _a = getPageAccessors(usage, vars), currentPageExpr = _a.currentPageExpr, buildSetPageStatement = _a.buildSetPageStatement;
1873
+ var numberedVars = {
1874
+ index: usage.index,
1875
+ maxPagesVar: vars.maxPagesStateVar,
1876
+ currentPageExpr: currentPageExpr,
1877
+ buildSetPageStatement: buildSetPageStatement,
1878
+ };
1879
+ wireNumberedPagination(paginationNode, numberedVars);
1880
+ }
1881
+ }
1498
1882
  function wirePaginationButtons(paginationNode, usage, vars) {
1499
- // Find prev and next buttons
1500
- var findButton = function (node, direction) {
1501
- var _a, _b, _c, _d, _e;
1502
- if (!node) {
1503
- return null;
1504
- }
1505
- if (node.type === 'JSXElement') {
1506
- var classAttr = (_b = (_a = node.openingElement) === null || _a === void 0 ? void 0 : _a.attributes) === null || _b === void 0 ? void 0 : _b.find(function (attr) { return attr.type === 'JSXAttribute' && attr.name.name === 'className'; });
1507
- var className = ((_c = classAttr === null || classAttr === void 0 ? void 0 : classAttr.value) === null || _c === void 0 ? void 0 : _c.value) || ((_e = (_d = classAttr === null || classAttr === void 0 ? void 0 : classAttr.value) === null || _d === void 0 ? void 0 : _d.expression) === null || _e === void 0 ? void 0 : _e.value) || '';
1508
- if (className.includes(direction)) {
1509
- return node;
1510
- }
1511
- }
1512
- if (node.children && Array.isArray(node.children)) {
1513
- for (var _i = 0, _f = node.children; _i < _f.length; _i++) {
1514
- var c = _f[_i];
1515
- var found = findButton(c, direction);
1516
- if (found) {
1517
- return found;
1518
- }
1519
- }
1520
- }
1521
- return null;
1522
- };
1523
- var prevButton = findButton(paginationNode, 'previous');
1524
- var nextButton = findButton(paginationNode, 'next');
1883
+ // Marker attribute first, class-name substring second — projects generated
1884
+ // before the marker existed carry only the class name.
1885
+ var prevButton = findPaginationControl(paginationNode, 'previous', 'previous');
1886
+ var nextButton = findPaginationControl(paginationNode, 'next', 'next');
1525
1887
  var isCombinedState = usage.category === 'paginated+search';
1526
1888
  if (prevButton) {
1527
- // Change to button element
1528
- prevButton.openingElement.name.name = 'button';
1529
- if (prevButton.closingElement) {
1530
- prevButton.closingElement.name.name = 'button';
1531
- }
1532
- // Add type="button"
1533
- var hasType = prevButton.openingElement.attributes.some(function (a) { var _a; return ((_a = a.name) === null || _a === void 0 ? void 0 : _a.name) === 'type'; });
1534
- if (!hasType) {
1535
- prevButton.openingElement.attributes.push(types.jsxAttribute(types.jsxIdentifier('type'), types.stringLiteral('button')));
1536
- }
1889
+ convertControlToButton(prevButton);
1537
1890
  // Add onClick
1538
1891
  prevButton.openingElement.attributes = prevButton.openingElement.attributes.filter(function (a) { var _a; return ((_a = a.name) === null || _a === void 0 ? void 0 : _a.name) !== 'onClick'; });
1539
1892
  if (isCombinedState) {
@@ -1562,14 +1915,7 @@ function wirePaginationButtons(paginationNode, usage, vars) {
1562
1915
  }
1563
1916
  }
1564
1917
  if (nextButton) {
1565
- nextButton.openingElement.name.name = 'button';
1566
- if (nextButton.closingElement) {
1567
- nextButton.closingElement.name.name = 'button';
1568
- }
1569
- var hasType = nextButton.openingElement.attributes.some(function (a) { var _a; return ((_a = a.name) === null || _a === void 0 ? void 0 : _a.name) === 'type'; });
1570
- if (!hasType) {
1571
- nextButton.openingElement.attributes.push(types.jsxAttribute(types.jsxIdentifier('type'), types.stringLiteral('button')));
1572
- }
1918
+ convertControlToButton(nextButton);
1573
1919
  nextButton.openingElement.attributes = nextButton.openingElement.attributes.filter(function (a) { var _a; return ((_a = a.name) === null || _a === void 0 ? void 0 : _a.name) !== 'onClick'; });
1574
1920
  if (isCombinedState) {
1575
1921
  nextButton.openingElement.attributes.push(types.jsxAttribute(types.jsxIdentifier('onClick'), types.jsxExpressionContainer(types.arrowFunctionExpression([], types.callExpression(types.identifier(vars.setCombinedStateVar), [