@teleporthq/teleport-plugin-next-data-source 0.44.0 → 0.44.4

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 (94) hide show
  1. package/__tests__/_helpers/first-page-runtime.ts +184 -0
  2. package/__tests__/_helpers/pagination-widget-fixture.ts +67 -1
  3. package/__tests__/browser-row-policy.test.ts +202 -2
  4. package/__tests__/data-source-request-locale.test.ts +4 -3
  5. package/__tests__/fetchers.test.ts +6 -2
  6. package/__tests__/infinite-scroll.test.ts +210 -5
  7. package/__tests__/page-data-visitor-read.test.ts +196 -0
  8. package/__tests__/plain-list-sort.test.ts +1 -1
  9. package/__tests__/plain-list-starting-sort.test.ts +323 -0
  10. package/__tests__/refetch-loading-state.test.ts +158 -2
  11. package/__tests__/starting-sort-first-page.test.ts +285 -0
  12. package/dist/cjs/cache/ast.d.ts +2 -0
  13. package/dist/cjs/cache/ast.d.ts.map +1 -1
  14. package/dist/cjs/cache/ast.js +5 -2
  15. package/dist/cjs/cache/ast.js.map +1 -1
  16. package/dist/cjs/data-source-fetchers.d.ts.map +1 -1
  17. package/dist/cjs/data-source-fetchers.js +10 -2
  18. package/dist/cjs/data-source-fetchers.js.map +1 -1
  19. package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
  20. package/dist/cjs/fetchers/teleport.js +1 -1
  21. package/dist/cjs/fetchers/teleport.js.map +1 -1
  22. package/dist/cjs/fetchers/utils/browser-row-policy.d.ts.map +1 -1
  23. package/dist/cjs/fetchers/utils/browser-row-policy.js +5 -1
  24. package/dist/cjs/fetchers/utils/browser-row-policy.js.map +1 -1
  25. package/dist/cjs/index.d.ts.map +1 -1
  26. package/dist/cjs/index.js +14 -2
  27. package/dist/cjs/index.js.map +1 -1
  28. package/dist/cjs/infinite-scroll.d.ts +64 -2
  29. package/dist/cjs/infinite-scroll.d.ts.map +1 -1
  30. package/dist/cjs/infinite-scroll.js +102 -3
  31. package/dist/cjs/infinite-scroll.js.map +1 -1
  32. package/dist/cjs/loading-state.d.ts +23 -2
  33. package/dist/cjs/loading-state.d.ts.map +1 -1
  34. package/dist/cjs/loading-state.js +50 -15
  35. package/dist/cjs/loading-state.js.map +1 -1
  36. package/dist/cjs/pagination-plugin.d.ts.map +1 -1
  37. package/dist/cjs/pagination-plugin.js +200 -64
  38. package/dist/cjs/pagination-plugin.js.map +1 -1
  39. package/dist/cjs/sort-utils.d.ts +53 -1
  40. package/dist/cjs/sort-utils.d.ts.map +1 -1
  41. package/dist/cjs/sort-utils.js +105 -13
  42. package/dist/cjs/sort-utils.js.map +1 -1
  43. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  44. package/dist/cjs/utils.d.ts +3 -2
  45. package/dist/cjs/utils.d.ts.map +1 -1
  46. package/dist/cjs/utils.js +48 -18
  47. package/dist/cjs/utils.js.map +1 -1
  48. package/dist/esm/cache/ast.d.ts +2 -0
  49. package/dist/esm/cache/ast.d.ts.map +1 -1
  50. package/dist/esm/cache/ast.js +3 -1
  51. package/dist/esm/cache/ast.js.map +1 -1
  52. package/dist/esm/data-source-fetchers.d.ts.map +1 -1
  53. package/dist/esm/data-source-fetchers.js +10 -2
  54. package/dist/esm/data-source-fetchers.js.map +1 -1
  55. package/dist/esm/fetchers/teleport.d.ts.map +1 -1
  56. package/dist/esm/fetchers/teleport.js +1 -1
  57. package/dist/esm/fetchers/teleport.js.map +1 -1
  58. package/dist/esm/fetchers/utils/browser-row-policy.d.ts.map +1 -1
  59. package/dist/esm/fetchers/utils/browser-row-policy.js +6 -2
  60. package/dist/esm/fetchers/utils/browser-row-policy.js.map +1 -1
  61. package/dist/esm/index.d.ts.map +1 -1
  62. package/dist/esm/index.js +14 -2
  63. package/dist/esm/index.js.map +1 -1
  64. package/dist/esm/infinite-scroll.d.ts +64 -2
  65. package/dist/esm/infinite-scroll.d.ts.map +1 -1
  66. package/dist/esm/infinite-scroll.js +97 -2
  67. package/dist/esm/infinite-scroll.js.map +1 -1
  68. package/dist/esm/loading-state.d.ts +23 -2
  69. package/dist/esm/loading-state.d.ts.map +1 -1
  70. package/dist/esm/loading-state.js +50 -15
  71. package/dist/esm/loading-state.js.map +1 -1
  72. package/dist/esm/pagination-plugin.d.ts.map +1 -1
  73. package/dist/esm/pagination-plugin.js +204 -68
  74. package/dist/esm/pagination-plugin.js.map +1 -1
  75. package/dist/esm/sort-utils.d.ts +53 -1
  76. package/dist/esm/sort-utils.d.ts.map +1 -1
  77. package/dist/esm/sort-utils.js +100 -12
  78. package/dist/esm/sort-utils.js.map +1 -1
  79. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  80. package/dist/esm/utils.d.ts +3 -2
  81. package/dist/esm/utils.d.ts.map +1 -1
  82. package/dist/esm/utils.js +49 -19
  83. package/dist/esm/utils.js.map +1 -1
  84. package/package.json +4 -4
  85. package/src/cache/ast.ts +4 -1
  86. package/src/data-source-fetchers.ts +33 -14
  87. package/src/fetchers/teleport.ts +8 -2
  88. package/src/fetchers/utils/browser-row-policy.ts +79 -5
  89. package/src/index.ts +8 -2
  90. package/src/infinite-scroll.ts +182 -4
  91. package/src/loading-state.ts +85 -13
  92. package/src/pagination-plugin.ts +369 -83
  93. package/src/sort-utils.ts +164 -21
  94. package/src/utils.ts +55 -18
@@ -3,15 +3,28 @@ import {
3
3
  ComponentPluginFactory,
4
4
  ChunkType,
5
5
  FileType,
6
+ UIDLStateDefinition,
6
7
  } from '@teleporthq/teleport-types'
7
8
  import * as types from '@babel/types'
8
9
  import { parseExpression } from '@babel/parser'
9
10
  import { ASTStatementOrder, GenericUtils, StringUtils } from '@teleporthq/teleport-shared'
10
- import { ASTUtils, URLQueryWriter, URLSearchParamSync } from '@teleporthq/teleport-plugin-common'
11
+ import {
12
+ ASTUtils,
13
+ RouteUtils,
14
+ URLQueryWriter,
15
+ URLSearchParamSync,
16
+ } from '@teleporthq/teleport-plugin-common'
11
17
  import { collectFilterLeafConditions, generateSafeFileName, pruneFilterTree } from './utils'
12
18
  import { generateDataSourceFetcherWithCore } from './data-source-fetchers'
13
19
  import { buildProductTransformOptions, type EntityTransformOptions } from './transformations'
14
- import { appendSortsParam, DynamicSortAST, extractDynamicSort, readFixedSort } from './sort-utils'
20
+ import {
21
+ appendSortsParam,
22
+ buildSortAtStartGuard,
23
+ buildStartingSortsValue,
24
+ DynamicSortAST,
25
+ readRepeaterSort,
26
+ type SortStateDefault,
27
+ } from './sort-utils'
15
28
  import {
16
29
  appendFiltersParam,
17
30
  buildStaticFiltersStringifyCall,
@@ -32,6 +45,7 @@ import {
32
45
  buildCacheStoreThen,
33
46
  buildCachedFetchBody,
34
47
  buildCachedParamsDeclarations,
48
+ paramsVarFor,
35
49
  registerCacheClientImports,
36
50
  } from './cache/ast'
37
51
  import {
@@ -60,9 +74,13 @@ import {
60
74
  import {
61
75
  buildAccumulatingRequestMark,
62
76
  buildAccumulatingResponseHandler,
77
+ buildAppendingRequestCheck,
78
+ buildFirstPageDeclaration,
79
+ buildFirstPageSeedEffect,
63
80
  buildInfiniteScrollDeclarations,
64
81
  buildSentinelElement,
65
82
  buildSentinelObserverEffect,
83
+ buildServerRowsExpression,
66
84
  getInfiniteScrollVars,
67
85
  } from './infinite-scroll'
68
86
  import {
@@ -197,6 +215,12 @@ interface DataSourceUsage {
197
215
  // Dynamic single-column sort bound to component state (set when legacy sorts empty
198
216
  // and cms-list-repeater declares sort/sortDirection fields)
199
217
  dynamicSort?: DynamicSortAST
218
+ // With `dynamicSort`: the states it reads, at the values a visitor starts
219
+ // with (`readSortStateDefaults`). The build fetches the first page by that
220
+ // sort, and the list takes those rows while the sort is still there. Absent
221
+ // when the build cannot know the starting sort: the list then fetches its
222
+ // first page in the browser.
223
+ startingSort?: SortStateDefault[]
200
224
  // Filters from resource params, as the TREE the inspector authored: condition
201
225
  // leaves and `{ type: 'group', operator, children }` entries (see
202
226
  // `readFilterTree`). Anything that needs the leaves walks the tree with
@@ -456,7 +480,10 @@ function buildFilterDestinationExpression(destination: unknown): types.Expressio
456
480
  }
457
481
 
458
482
  // Scan UIDL to find all data source usages and build a registry
459
- function buildStateRegistry(uidlNode: any): StateRegistry {
483
+ function buildStateRegistry(
484
+ uidlNode: any,
485
+ stateDefinitions?: Record<string, UIDLStateDefinition>
486
+ ): StateRegistry {
460
487
  const usages: DataSourceUsage[] = []
461
488
  const byDataSourceId = new Map<string, DataSourceUsage[]>()
462
489
  const byArrayMapperRenderProp = new Map<string, DataSourceUsage>()
@@ -525,15 +552,20 @@ function buildStateRegistry(uidlNode: any): StateRegistry {
525
552
  // is skipped for a dynamic sort) — the pre-rendered HTML carried no
526
553
  // posts, and the browser fetched page 1 a second time. Only a sort
527
554
  // bound to state (the admin panel's sortable columns) is dynamic.
528
- let dynamicSort: DynamicSortAST | undefined
529
- if ((!sorts || sorts.length === 0) && content.sort) {
530
- const fixedSort = readFixedSort(content.sort, content.sortDirection)
531
- if (fixedSort) {
532
- sorts = [fixedSort]
533
- } else {
534
- dynamicSort = extractDynamicSort(content.sort, content.sortDirection)
535
- }
555
+ //
556
+ // A sort the visitor picks still starts somewhere — the store's sort
557
+ // dropdown at "Name A-Z". Known at build time, it orders the
558
+ // server-side first page, which the list then shows while the visitor
559
+ // leaves the sort alone. Read as a plain list's page fetch reads it.
560
+ const repeaterSort =
561
+ !sorts || sorts.length === 0
562
+ ? readRepeaterSort(content.sort, content.sortDirection, stateDefinitions)
563
+ : {}
564
+ if (repeaterSort.fixed) {
565
+ sorts = [repeaterSort.fixed]
536
566
  }
567
+ const dynamicSort = repeaterSort.dynamic
568
+ const startingSort = repeaterSort.starting
537
569
 
538
570
  // Extract filters from parent's resource params, keeping the group tree
539
571
  // the inspector authored (see `readFilterTree`).
@@ -628,6 +660,7 @@ function buildStateRegistry(uidlNode: any): StateRegistry {
628
660
  queryColumns,
629
661
  sorts,
630
662
  dynamicSort,
663
+ startingSort,
631
664
  filters,
632
665
  filterStateIds,
633
666
  filterPropIds,
@@ -1220,7 +1253,7 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
1220
1253
  const hasRangeControl = rangeControls.length > 0
1221
1254
 
1222
1255
  // STEP 1: Build state registry from UIDL
1223
- const registry = buildStateRegistry(uidl.node)
1256
+ const registry = buildStateRegistry(uidl.node, uidl.stateDefinitions)
1224
1257
 
1225
1258
  if (registry.usages.length === 0) {
1226
1259
  return structure
@@ -1847,6 +1880,9 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
1847
1880
  // widgets can be matched to their owner rather than counted off in order.
1848
1881
  const usageByProviderNode = new Map<any, DataSourceUsage>()
1849
1882
  const cacheDeclarations: types.Statement[] = []
1883
+ // Only a page has rows of its own to start an appending list from: its
1884
+ // `getStaticProps` fetched page 1 (`updateGetStaticProps`).
1885
+ const firstPageDeclarations: types.Statement[] | undefined = isPage ? [] : undefined
1850
1886
 
1851
1887
  dataProvidersWithRepeaters.forEach((dp) => {
1852
1888
  const nameAttr = dp.openingElement.attributes.find(
@@ -1885,10 +1921,19 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
1885
1921
  vars,
1886
1922
  fileName,
1887
1923
  cacheDeclarations,
1888
- localized
1924
+ localized,
1925
+ firstPageDeclarations
1889
1926
  )
1890
1927
  } else if (usage.category === 'paginated-only') {
1891
- updateDataProviderForPaginationOnly(dp, usage, vars, fileName, cacheDeclarations, localized)
1928
+ updateDataProviderForPaginationOnly(
1929
+ dp,
1930
+ usage,
1931
+ vars,
1932
+ fileName,
1933
+ cacheDeclarations,
1934
+ localized,
1935
+ firstPageDeclarations
1936
+ )
1892
1937
  } else if (usage.category === 'search-only') {
1893
1938
  updateDataProviderForSearchOnly(dp, usage, vars, fileName, cacheDeclarations, localized)
1894
1939
  } else if (usage.category === 'plain') {
@@ -1899,9 +1944,17 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
1899
1944
  // change refetches, instead of leaving the previous rows on screen with
1900
1945
  // no feedback. Runs last so it sees the `fetchData` and
1901
1946
  // `persistDataDuringLoading` attributes the updaters above just wrote.
1947
+ // An appending list keeps its rows on screen while its next page loads;
1948
+ // only a list loading afresh falls through to the loading slot.
1902
1949
  const loadingVars = getLoadingStateVars(usage.index)
1903
- if (applyLoadingStateToDataProvider(dp, loadingVars)) {
1904
- loadingStateDeclarations.push(...buildLoadingStateDeclarations(loadingVars))
1950
+ const appendingRequest =
1951
+ usage.infiniteScroll && usage.paginated
1952
+ ? buildAppendingRequestCheck(getInfiniteScrollVars(usage.index))
1953
+ : undefined
1954
+ if (applyLoadingStateToDataProvider(dp, loadingVars, appendingRequest)) {
1955
+ loadingStateDeclarations.push(
1956
+ ...buildLoadingStateDeclarations(loadingVars, !!appendingRequest)
1957
+ )
1905
1958
  // Recorded because the loading state is only declared for a provider
1906
1959
  // that HAS a loading slot; a Load More button wired later may only
1907
1960
  // reference `ds_N_isFetching` if it actually exists.
@@ -1969,6 +2022,12 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
1969
2022
  registerCacheClientImports(dependencies, uidl.outputOptions?.folderPath)
1970
2023
  }
1971
2024
 
2025
+ // After the cache declarations: a cached list's first page reads the
2026
+ // `ds_N_params` they hoisted.
2027
+ if (firstPageDeclarations?.length) {
2028
+ ASTStatementOrder.insertStatementsAfterDependencies(blockStatement, firstPageDeclarations)
2029
+ }
2030
+
1972
2031
  // STEP 3.5: Handle DataProviders WITHOUT repeaters (data-source-item type)
1973
2032
  // These access single items like data[0].name and should not re-render on state changes
1974
2033
  // We wrap their params in useMemo to prevent reference changes from triggering re-renders
@@ -2049,14 +2108,17 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
2049
2108
  }
2050
2109
  })
2051
2110
 
2052
- // STEP 6: Update getStaticProps if this is a page
2111
+ // STEP 6: Update getStaticProps if this is a page. Its rows are what the
2112
+ // page renders, so they are read as a visitor is served them — unless only
2113
+ // the store's trusted readers can open the page (`pageDataReadArguments`).
2053
2114
  if (isPage) {
2054
2115
  updateGetStaticProps(
2055
2116
  chunks,
2056
2117
  registry,
2057
2118
  dependencies,
2058
2119
  uidl.outputOptions?.folderPath,
2059
- localized
2120
+ localized,
2121
+ () => RouteUtils.pageDataReadArguments(uidl, options.auth)
2060
2122
  )
2061
2123
  }
2062
2124
 
@@ -2353,10 +2415,12 @@ function findPaginationNodesWithOwners(
2353
2415
  *
2354
2416
  * An uncached mapper keeps the inline `params={useMemo(...)}` it has always had,
2355
2417
  * byte for byte. A cached one hoists that memo into a component const so the
2356
- * peek can depend on the same object identity instead of rebuilding it.
2418
+ * peek can depend on the same object identity instead of rebuilding it; so does
2419
+ * an appending list that starts from its page's rows (`hoistInto`), whose first
2420
+ * page is filed in the accumulator under those params.
2357
2421
  *
2358
- * Returns the identifier holding the peeked rows, or `undefined` when this
2359
- * mapper does not cache.
2422
+ * Returns the identifier holding the params when they were hoisted, and the one
2423
+ * holding the peeked rows when this mapper caches.
2360
2424
  */
2361
2425
  function pushParamsAttribute(
2362
2426
  // tslint:disable-next-line:no-any
@@ -2364,24 +2428,40 @@ function pushParamsAttribute(
2364
2428
  usage: DataSourceUsage,
2365
2429
  paramsProps: types.ObjectProperty[],
2366
2430
  memoDeps: types.Expression[],
2367
- cacheDeclarations: types.Statement[]
2368
- ): string | undefined {
2431
+ cacheDeclarations: types.Statement[],
2432
+ hoistInto?: types.Statement[]
2433
+ ): { paramsIdentifier?: string; cachedIdentifier?: string } {
2369
2434
  const paramsMemo = types.callExpression(types.identifier('useMemo'), [
2370
2435
  types.arrowFunctionExpression([], types.objectExpression(paramsProps)),
2371
2436
  types.arrayExpression(memoDeps),
2372
2437
  ])
2373
2438
 
2374
- // A state-bound sort deliberately suppresses `initialData` (see the comment at
2375
- // the call site): the prefetch ran without sort params, so reusing anything on
2376
- // mount would mask the current sort. The cache peek is `initialData` by
2377
- // another name, so it has to be suppressed for exactly the same reason.
2439
+ // A state-bound sort gets no cache peek. The peek is `initialData` by another
2440
+ // name, and such a list takes the server's rows only while its sort is still
2441
+ // where the visitor starts (`buildStartingSortGuard`); the peek was never
2442
+ // extended to it.
2378
2443
  const cache = usage.cache?.client && !usage.dynamicSort ? usage.cache : undefined
2379
2444
 
2380
2445
  if (!cache) {
2446
+ if (!hoistInto) {
2447
+ dp.openingElement.attributes.push(
2448
+ types.jsxAttribute(types.jsxIdentifier('params'), types.jsxExpressionContainer(paramsMemo))
2449
+ )
2450
+ return {}
2451
+ }
2452
+ const paramsIdentifier = paramsVarFor(usage.index)
2453
+ hoistInto.push(
2454
+ types.variableDeclaration('const', [
2455
+ types.variableDeclarator(types.identifier(paramsIdentifier), paramsMemo),
2456
+ ])
2457
+ )
2381
2458
  dp.openingElement.attributes.push(
2382
- types.jsxAttribute(types.jsxIdentifier('params'), types.jsxExpressionContainer(paramsMemo))
2459
+ types.jsxAttribute(
2460
+ types.jsxIdentifier('params'),
2461
+ types.jsxExpressionContainer(types.identifier(paramsIdentifier))
2462
+ )
2383
2463
  )
2384
- return undefined
2464
+ return { paramsIdentifier }
2385
2465
  }
2386
2466
 
2387
2467
  const hoisted = buildCachedParamsDeclarations({ index: usage.index, paramsMemo, cache })
@@ -2394,7 +2474,97 @@ function pushParamsAttribute(
2394
2474
  )
2395
2475
  )
2396
2476
 
2397
- return hoisted.cachedIdentifier
2477
+ return { paramsIdentifier: hoisted.paramsIdentifier, cachedIdentifier: hoisted.cachedIdentifier }
2478
+ }
2479
+
2480
+ // Whether the rows `getStaticProps` fetched can be this list's first page at
2481
+ // all. A sort bound to state can only be served them when the build knows the
2482
+ // sort the visitor starts with (`startingSort`) and fetched by it; otherwise
2483
+ // the fetch has no sort, and its rows are not the list's first page.
2484
+ function prefetchFollowsSort(usage: DataSourceUsage): boolean {
2485
+ return !usage.dynamicSort || !!usage.startingSort
2486
+ }
2487
+
2488
+ // `sortBy === "name-asc"`: the visitor has not moved the sort from where it
2489
+ // starts, so the build-time rows are still its first page. `null` for a list
2490
+ // whose sort the visitor cannot pick, or one that reads no state.
2491
+ function buildStartingSortGuard(usage: DataSourceUsage): types.Expression | null {
2492
+ return usage.dynamicSort && usage.startingSort ? buildSortAtStartGuard(usage.startingSort) : null
2493
+ }
2494
+
2495
+ // `<condition> && <starting-sort guard>`, or the condition alone.
2496
+ function withStartingSortGuard(
2497
+ condition: types.Expression,
2498
+ usage: DataSourceUsage
2499
+ ): types.Expression {
2500
+ const sortGuard = buildStartingSortGuard(usage)
2501
+ return sortGuard ? types.logicalExpression('&&', condition, sortGuard) : condition
2502
+ }
2503
+
2504
+ // An appending list on a page starts from the rows its `getStaticProps`
2505
+ // fetched, when they can be its first page (`prefetchFollowsSort`).
2506
+ function startsFromServerRows(usage: DataSourceUsage): boolean {
2507
+ return usage.infiniteScroll && usage.paginated && prefetchFollowsSort(usage)
2508
+ }
2509
+
2510
+ /**
2511
+ * `initialData={ds_N_firstPage}` on an appending list, beside the frozen first
2512
+ * page and the effect that files it as page 1 of the accumulator (see
2513
+ * `infinite-scroll.ts`).
2514
+ *
2515
+ * The rows stand in for page 1 only while they answer the list's query: no
2516
+ * search (`queryGuard`), the sort where the visitor starts, no filter set (the
2517
+ * guards the paged lists use), and at least one row — an empty build-time
2518
+ * answer, which a failed fetch also gives, leaves the browser to fetch page 1
2519
+ * as before.
2520
+ */
2521
+ function pushInfiniteScrollFirstPage(
2522
+ // tslint:disable-next-line:no-any
2523
+ dp: any,
2524
+ usage: DataSourceUsage,
2525
+ vars: ReturnType<typeof getStateVarsForUsage>,
2526
+ paramsIdentifier: string,
2527
+ declarations: types.Statement[],
2528
+ queryGuard: types.Expression | null,
2529
+ instanceDeps: types.Expression[]
2530
+ ): void {
2531
+ const infiniteVars = getInfiniteScrollVars(usage.index)
2532
+ const serverPayload = (): types.Expression =>
2533
+ types.optionalMemberExpression(
2534
+ types.identifier('props'),
2535
+ types.identifier(vars.propsPrefix),
2536
+ false,
2537
+ true
2538
+ )
2539
+ const hasServerRows = types.binaryExpression(
2540
+ '>',
2541
+ types.optionalMemberExpression(
2542
+ buildServerRowsExpression(serverPayload(), usage.itemsPath),
2543
+ types.identifier('length'),
2544
+ false,
2545
+ true
2546
+ ),
2547
+ types.numericLiteral(0)
2548
+ )
2549
+ const startsFromServer = [
2550
+ queryGuard,
2551
+ buildStartingSortGuard(usage),
2552
+ buildNoUrlFilterGuard(usage),
2553
+ hasServerRows,
2554
+ ]
2555
+ .filter((guard): guard is types.Expression => guard !== null)
2556
+ .reduce((all, guard) => types.logicalExpression('&&', all, guard))
2557
+
2558
+ declarations.push(
2559
+ buildFirstPageDeclaration(infiniteVars, startsFromServer, serverPayload(), instanceDeps),
2560
+ buildFirstPageSeedEffect(infiniteVars, paramsIdentifier, usage.perPage, usage.itemsPath)
2561
+ )
2562
+ dp.openingElement.attributes.push(
2563
+ types.jsxAttribute(
2564
+ types.jsxIdentifier('initialData'),
2565
+ types.jsxExpressionContainer(types.identifier(infiniteVars.firstPageVar))
2566
+ )
2567
+ )
2398
2568
  }
2399
2569
 
2400
2570
  function updateDataProviderForPaginatedSearch(
@@ -2403,7 +2573,8 @@ function updateDataProviderForPaginatedSearch(
2403
2573
  vars: ReturnType<typeof getStateVarsForUsage>,
2404
2574
  fileName: string,
2405
2575
  cacheDeclarations: types.Statement[],
2406
- localized: boolean
2576
+ localized: boolean,
2577
+ firstPageDeclarations?: types.Statement[]
2407
2578
  ): void {
2408
2579
  const attrs = dp.openingElement.attributes
2409
2580
 
@@ -2462,36 +2633,67 @@ function updateDataProviderForPaginatedSearch(
2462
2633
  appendClientLocaleParam(paramsProps, memoDeps)
2463
2634
  }
2464
2635
 
2465
- const cachedInitialData = pushParamsAttribute(dp, usage, paramsProps, memoDeps, cacheDeclarations)
2636
+ const seedInto = startsFromServerRows(usage) ? firstPageDeclarations : undefined
2637
+ const { paramsIdentifier, cachedIdentifier: cachedInitialData } = pushParamsAttribute(
2638
+ dp,
2639
+ usage,
2640
+ paramsProps,
2641
+ memoDeps,
2642
+ cacheDeclarations,
2643
+ seedInto
2644
+ )
2466
2645
 
2467
- // Add initialData. Skip the server-prefetched data entirely when a dynamic
2468
- // state-bound sort is active — the prefetch ran without sort parameters, so
2469
- // reusing it would mask the current sort state AND cause DataProvider to skip
2470
- // the first client fetch (its internal guard only skips when initialData is
2471
- // defined on mount). Without that skip, toggling sort correctly triggers a
2472
- // refetch via the useMemo params dependency chain.
2646
+ // Add initialData: the server-prefetched page 1 while it is the page on show
2647
+ // — page 1, no search, no filter set, and a sort the visitor picks still where
2648
+ // they start (the prefetch ran by that sort). A list whose starting sort the
2649
+ // build cannot know never takes it: that prefetch ran without a sort, so
2650
+ // reusing it would mask the current sort AND make DataProvider skip its first
2651
+ // client fetch (its internal guard only skips when initialData is defined on
2652
+ // mount). Any other sort is fetched fresh, through the useMemo params.
2473
2653
  //
2474
- // An appending list skips it for a second reason: `initialData` makes
2475
- // DataProvider skip its first fetch, so page 1 would render from the prefetch
2476
- // and never enter the accumulator — appending page 2 would then REPLACE the
2477
- // visible rows with page 2 alone.
2478
- if (!usage.dynamicSort && !usage.infiniteScroll) {
2654
+ // An appending list takes its own: `initialData` makes DataProvider skip its
2655
+ // first fetch, so the prefetched page 1 has to be filed in the accumulator
2656
+ // too, or appending page 2 would REPLACE the visible rows with page 2 alone.
2657
+ // The provider remounts per query (its `key`), so the first page is frozen
2658
+ // per query.
2659
+ if (seedInto && paramsIdentifier) {
2660
+ const appliedQuery = (): types.MemberExpression =>
2661
+ types.memberExpression(
2662
+ types.identifier(vars.combinedStateVar),
2663
+ types.identifier('debouncedQuery')
2664
+ )
2665
+ pushInfiniteScrollFirstPage(
2666
+ dp,
2667
+ usage,
2668
+ vars,
2669
+ paramsIdentifier,
2670
+ seedInto,
2671
+ types.unaryExpression('!', appliedQuery(), true),
2672
+ [appliedQuery()]
2673
+ )
2674
+ } else if (prefetchFollowsSort(usage) && !usage.infiniteScroll) {
2479
2675
  const initialDataCondition = wrapInitialDataWithUrlFilterGuard(
2480
- types.logicalExpression(
2481
- '&&',
2482
- types.binaryExpression(
2483
- '===',
2484
- types.memberExpression(types.identifier(vars.combinedStateVar), types.identifier('page')),
2485
- types.numericLiteral(1)
2486
- ),
2487
- types.unaryExpression(
2488
- '!',
2489
- types.memberExpression(
2490
- types.identifier(vars.combinedStateVar),
2491
- types.identifier('debouncedQuery')
2676
+ withStartingSortGuard(
2677
+ types.logicalExpression(
2678
+ '&&',
2679
+ types.binaryExpression(
2680
+ '===',
2681
+ types.memberExpression(
2682
+ types.identifier(vars.combinedStateVar),
2683
+ types.identifier('page')
2684
+ ),
2685
+ types.numericLiteral(1)
2492
2686
  ),
2493
- true
2494
- )
2687
+ types.unaryExpression(
2688
+ '!',
2689
+ types.memberExpression(
2690
+ types.identifier(vars.combinedStateVar),
2691
+ types.identifier('debouncedQuery')
2692
+ ),
2693
+ true
2694
+ )
2695
+ ),
2696
+ usage
2495
2697
  ),
2496
2698
  usage
2497
2699
  )
@@ -2585,7 +2787,8 @@ function updateDataProviderForPaginationOnly(
2585
2787
  vars: ReturnType<typeof getStateVarsForUsage>,
2586
2788
  fileName: string,
2587
2789
  cacheDeclarations: types.Statement[],
2588
- localized: boolean
2790
+ localized: boolean,
2791
+ firstPageDeclarations?: types.Statement[]
2589
2792
  ): void {
2590
2793
  const attrs = dp.openingElement.attributes
2591
2794
 
@@ -2623,22 +2826,36 @@ function updateDataProviderForPaginationOnly(
2623
2826
  }
2624
2827
 
2625
2828
  // Add params
2626
- const cachedInitialData = pushParamsAttribute(dp, usage, paramsProps, memoDeps, cacheDeclarations)
2829
+ const seedInto = startsFromServerRows(usage) ? firstPageDeclarations : undefined
2830
+ const { paramsIdentifier, cachedIdentifier: cachedInitialData } = pushParamsAttribute(
2831
+ dp,
2832
+ usage,
2833
+ paramsProps,
2834
+ memoDeps,
2835
+ cacheDeclarations,
2836
+ seedInto
2837
+ )
2627
2838
 
2628
- // Add initialData. See paginated+search updater for why we skip prefetch
2629
- // reuse when a dynamic state-bound sort is active, and why an appending list
2630
- // skips it too.
2631
- if (!usage.dynamicSort && !usage.infiniteScroll) {
2839
+ // Add initialData. See paginated+search updater for when a sort the visitor
2840
+ // picks may reuse the prefetch, and how an appending list takes its own. This
2841
+ // provider never remounts (no `key`), so its first page is frozen for the
2842
+ // page's lifetime.
2843
+ if (seedInto && paramsIdentifier) {
2844
+ pushInfiniteScrollFirstPage(dp, usage, vars, paramsIdentifier, seedInto, null, [])
2845
+ } else if (prefetchFollowsSort(usage) && !usage.infiniteScroll) {
2632
2846
  dp.openingElement.attributes.push(
2633
2847
  types.jsxAttribute(
2634
2848
  types.jsxIdentifier('initialData'),
2635
2849
  types.jsxExpressionContainer(
2636
2850
  types.conditionalExpression(
2637
2851
  wrapInitialDataWithUrlFilterGuard(
2638
- types.binaryExpression(
2639
- '===',
2640
- types.identifier(vars.pageStateVar),
2641
- types.numericLiteral(1)
2852
+ withStartingSortGuard(
2853
+ types.binaryExpression(
2854
+ '===',
2855
+ types.identifier(vars.pageStateVar),
2856
+ types.numericLiteral(1)
2857
+ ),
2858
+ usage
2642
2859
  ),
2643
2860
  usage
2644
2861
  ),
@@ -2743,18 +2960,27 @@ function updateDataProviderForSearchOnly(
2743
2960
  appendClientLocaleParam(paramsProps, memoDeps)
2744
2961
  }
2745
2962
 
2746
- const cachedInitialData = pushParamsAttribute(dp, usage, paramsProps, memoDeps, cacheDeclarations)
2963
+ const { cachedIdentifier: cachedInitialData } = pushParamsAttribute(
2964
+ dp,
2965
+ usage,
2966
+ paramsProps,
2967
+ memoDeps,
2968
+ cacheDeclarations
2969
+ )
2747
2970
 
2748
- // Add initialData. See paginated+search updater for why we skip prefetch
2749
- // reuse when a dynamic state-bound sort is active.
2750
- if (!usage.dynamicSort) {
2971
+ // Add initialData. See paginated+search updater for when a sort the visitor
2972
+ // picks may reuse the prefetch.
2973
+ if (prefetchFollowsSort(usage)) {
2751
2974
  dp.openingElement.attributes.push(
2752
2975
  types.jsxAttribute(
2753
2976
  types.jsxIdentifier('initialData'),
2754
2977
  types.jsxExpressionContainer(
2755
2978
  types.conditionalExpression(
2756
2979
  wrapInitialDataWithUrlFilterGuard(
2757
- types.unaryExpression('!', types.identifier(vars.debouncedSearchQueryVar), true),
2980
+ withStartingSortGuard(
2981
+ types.unaryExpression('!', types.identifier(vars.debouncedSearchQueryVar), true),
2982
+ usage
2983
+ ),
2758
2984
  usage
2759
2985
  ),
2760
2986
  types.optionalMemberExpression(
@@ -2799,20 +3025,58 @@ function updateDataProviderForSearchOnly(
2799
3025
  )
2800
3026
  }
2801
3027
 
3028
+ /**
3029
+ * A plain list on a page is seeded with the rows its `getStaticProps` fetched
3030
+ * (`extractDataSourceIntoGetStaticProps`), and a seeded provider skips its first
3031
+ * fetch. With a sort the visitor picks, those rows are the list's only while the
3032
+ * sort is where a visitor starts — the build fetched them by it:
3033
+ *
3034
+ * initialData={sortBy === "name-asc" ? props.items_products : undefined}
3035
+ *
3036
+ * — and any other sort is fetched fresh, through the params it changes. When the
3037
+ * build cannot know where the sort starts (`prefetchFollowsSort`), it fetched
3038
+ * the rows unsorted, and the list is never seeded with them. A plain list has no
3039
+ * search, and is only seeded without a filter a visitor sets.
3040
+ */
3041
+ function seedPlainListWhileSortStarts(dp: any, usage: DataSourceUsage): void {
3042
+ if (!usage.dynamicSort) {
3043
+ return
3044
+ }
3045
+ const attrs = dp.openingElement.attributes
3046
+ const index = attrs.findIndex(
3047
+ (attr: any) => attr.type === 'JSXAttribute' && attr.name?.name === 'initialData'
3048
+ )
3049
+ if (index === -1) {
3050
+ return
3051
+ }
3052
+ if (!prefetchFollowsSort(usage)) {
3053
+ attrs.splice(index, 1)
3054
+ return
3055
+ }
3056
+ const sortGuard = buildStartingSortGuard(usage)
3057
+ const seeded = (attrs[index] as types.JSXAttribute).value
3058
+ if (!sortGuard || seeded?.type !== 'JSXExpressionContainer') {
3059
+ return
3060
+ }
3061
+ attrs[index] = types.jsxAttribute(
3062
+ types.jsxIdentifier('initialData'),
3063
+ types.jsxExpressionContainer(
3064
+ types.conditionalExpression(
3065
+ sortGuard,
3066
+ seeded.expression as types.Expression,
3067
+ types.identifier('undefined')
3068
+ )
3069
+ )
3070
+ )
3071
+ }
3072
+
2802
3073
  function updateDataProviderForPlain(
2803
3074
  dp: any,
2804
3075
  fileName: string,
2805
3076
  usage: DataSourceUsage,
2806
3077
  localized: boolean
2807
3078
  ): void {
2808
- // A sort bound to state is the visitor's, which the server cannot know: rows
2809
- // pre-rendered in the table's order would stand in for it, since a seeded
2810
- // provider skips its first fetch — the paginated lists drop them the same way.
2811
- if (usage.dynamicSort) {
2812
- dp.openingElement.attributes = dp.openingElement.attributes.filter(
2813
- (attr: any) => !(attr.type === 'JSXAttribute' && attr.name?.name === 'initialData')
2814
- )
2815
- }
3079
+ seedPlainListWhileSortStarts(dp, usage)
2816
3080
 
2817
3081
  const attrs = dp.openingElement.attributes
2818
3082
 
@@ -3627,12 +3891,24 @@ export default dataSourceModule.getCount
3627
3891
  }
3628
3892
  }
3629
3893
 
3894
+ // `fetchCount(<params>, <read arguments>)`: the params may be left out only
3895
+ // when nothing follows them.
3896
+ const countReadArguments = (
3897
+ countParams: types.ObjectProperty[],
3898
+ readArguments: types.Expression[]
3899
+ ): types.Expression[] =>
3900
+ countParams.length > 0 || readArguments.length > 0
3901
+ ? [types.objectExpression(countParams), ...readArguments]
3902
+ : []
3903
+
3630
3904
  function updateGetStaticProps(
3631
3905
  chunks: any[],
3632
3906
  registry: StateRegistry,
3633
3907
  dependencies: Record<string, any>,
3634
3908
  folderPath: string[] | undefined,
3635
- localized: boolean
3909
+ localized: boolean,
3910
+ // The arguments every read hands the module after its params (fresh nodes per call).
3911
+ readArguments: () => types.Expression[]
3636
3912
  ): void {
3637
3913
  const getStaticPropsChunk = chunks.find((c) => c.name === 'getStaticProps')
3638
3914
  if (!getStaticPropsChunk || getStaticPropsChunk.type !== ChunkType.AST) {
@@ -3764,6 +4040,16 @@ function updateGetStaticProps(
3764
4040
  )
3765
4041
  )
3766
4042
  )
4043
+ } else if (usage.dynamicSort && usage.startingSort) {
4044
+ // A sort the visitor picks: page 1 by the sort they start with, sent
4045
+ // as the very string the browser sends for it, so page 2 continues
4046
+ // this page exactly (see `buildStartingSortsValue`).
4047
+ fetchParams.push(
4048
+ types.objectProperty(
4049
+ types.identifier('sorts'),
4050
+ buildStartingSortsValue(usage.dynamicSort, usage.startingSort)
4051
+ )
4052
+ )
3767
4053
  }
3768
4054
 
3769
4055
  // Add filters if present
@@ -3801,7 +4087,7 @@ function updateGetStaticProps(
3801
4087
  types.identifier(fetcherImportName),
3802
4088
  types.identifier('fetchData')
3803
4089
  ),
3804
- [types.objectExpression(fetchParams)]
4090
+ [types.objectExpression(fetchParams), ...readArguments()]
3805
4091
  ),
3806
4092
  types.identifier('catch')
3807
4093
  ),
@@ -3990,7 +4276,7 @@ function updateGetStaticProps(
3990
4276
  types.identifier(fetcherImportName),
3991
4277
  types.identifier('fetchCount')
3992
4278
  ),
3993
- countParams.length > 0 ? [types.objectExpression(countParams)] : []
4279
+ countReadArguments(countParams, readArguments())
3994
4280
  ),
3995
4281
  types.identifier('catch')
3996
4282
  ),