@teleporthq/teleport-plugin-next-data-source 0.43.55 → 0.43.59

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 (110) hide show
  1. package/__tests__/array-mapper-cache-wiring.test.ts +425 -0
  2. package/__tests__/data-source-cache-wrapping.test.ts +152 -0
  3. package/__tests__/ecommerce-product-allow-backorders.test.ts +156 -0
  4. package/__tests__/ecommerce-product-model.test.ts +179 -0
  5. package/__tests__/ecommerce-product-storefront-tax.test.ts +118 -0
  6. package/dist/cjs/cache/ast.d.ts +71 -0
  7. package/dist/cjs/cache/ast.d.ts.map +1 -0
  8. package/dist/cjs/cache/ast.js +229 -0
  9. package/dist/cjs/cache/ast.js.map +1 -0
  10. package/dist/cjs/cache/config.d.ts +22 -0
  11. package/dist/cjs/cache/config.d.ts.map +1 -0
  12. package/dist/cjs/cache/config.js +67 -0
  13. package/dist/cjs/cache/config.js.map +1 -0
  14. package/dist/cjs/cache/types.d.ts +31 -0
  15. package/dist/cjs/cache/types.d.ts.map +1 -0
  16. package/dist/cjs/cache/types.js +3 -0
  17. package/dist/cjs/cache/types.js.map +1 -0
  18. package/dist/cjs/data-source-fetchers.d.ts +5 -3
  19. package/dist/cjs/data-source-fetchers.d.ts.map +1 -1
  20. package/dist/cjs/data-source-fetchers.js +51 -8
  21. package/dist/cjs/data-source-fetchers.js.map +1 -1
  22. package/dist/cjs/fetchers/teleport.d.ts +2 -2
  23. package/dist/cjs/fetchers/teleport.d.ts.map +1 -1
  24. package/dist/cjs/fetchers/teleport.js +3 -2
  25. package/dist/cjs/fetchers/teleport.js.map +1 -1
  26. package/dist/cjs/index.d.ts +2 -0
  27. package/dist/cjs/index.d.ts.map +1 -1
  28. package/dist/cjs/index.js +14 -9
  29. package/dist/cjs/index.js.map +1 -1
  30. package/dist/cjs/loading-state.d.ts.map +1 -1
  31. package/dist/cjs/loading-state.js +74 -2
  32. package/dist/cjs/loading-state.js.map +1 -1
  33. package/dist/cjs/pagination-plugin.d.ts.map +1 -1
  34. package/dist/cjs/pagination-plugin.js +204 -64
  35. package/dist/cjs/pagination-plugin.js.map +1 -1
  36. package/dist/cjs/transformations/ecommerce-product.d.ts +27 -1
  37. package/dist/cjs/transformations/ecommerce-product.d.ts.map +1 -1
  38. package/dist/cjs/transformations/ecommerce-product.js +8 -3
  39. package/dist/cjs/transformations/ecommerce-product.js.map +1 -1
  40. package/dist/cjs/transformations/index.d.ts +14 -2
  41. package/dist/cjs/transformations/index.d.ts.map +1 -1
  42. package/dist/cjs/transformations/index.js +37 -3
  43. package/dist/cjs/transformations/index.js.map +1 -1
  44. package/dist/cjs/transformations/shared-utils.d.ts.map +1 -1
  45. package/dist/cjs/transformations/shared-utils.js +1 -1
  46. package/dist/cjs/transformations/shared-utils.js.map +1 -1
  47. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  48. package/dist/cjs/utils.d.ts +4 -3
  49. package/dist/cjs/utils.d.ts.map +1 -1
  50. package/dist/cjs/utils.js +6 -4
  51. package/dist/cjs/utils.js.map +1 -1
  52. package/dist/esm/cache/ast.d.ts +71 -0
  53. package/dist/esm/cache/ast.d.ts.map +1 -0
  54. package/dist/esm/cache/ast.js +196 -0
  55. package/dist/esm/cache/ast.js.map +1 -0
  56. package/dist/esm/cache/config.d.ts +22 -0
  57. package/dist/esm/cache/config.d.ts.map +1 -0
  58. package/dist/esm/cache/config.js +62 -0
  59. package/dist/esm/cache/config.js.map +1 -0
  60. package/dist/esm/cache/types.d.ts +31 -0
  61. package/dist/esm/cache/types.d.ts.map +1 -0
  62. package/dist/esm/cache/types.js +2 -0
  63. package/dist/esm/cache/types.js.map +1 -0
  64. package/dist/esm/data-source-fetchers.d.ts +5 -3
  65. package/dist/esm/data-source-fetchers.d.ts.map +1 -1
  66. package/dist/esm/data-source-fetchers.js +51 -8
  67. package/dist/esm/data-source-fetchers.js.map +1 -1
  68. package/dist/esm/fetchers/teleport.d.ts +2 -2
  69. package/dist/esm/fetchers/teleport.d.ts.map +1 -1
  70. package/dist/esm/fetchers/teleport.js +3 -2
  71. package/dist/esm/fetchers/teleport.js.map +1 -1
  72. package/dist/esm/index.d.ts +2 -0
  73. package/dist/esm/index.d.ts.map +1 -1
  74. package/dist/esm/index.js +12 -8
  75. package/dist/esm/index.js.map +1 -1
  76. package/dist/esm/loading-state.d.ts.map +1 -1
  77. package/dist/esm/loading-state.js +74 -2
  78. package/dist/esm/loading-state.js.map +1 -1
  79. package/dist/esm/pagination-plugin.d.ts.map +1 -1
  80. package/dist/esm/pagination-plugin.js +205 -65
  81. package/dist/esm/pagination-plugin.js.map +1 -1
  82. package/dist/esm/transformations/ecommerce-product.d.ts +27 -1
  83. package/dist/esm/transformations/ecommerce-product.d.ts.map +1 -1
  84. package/dist/esm/transformations/ecommerce-product.js +8 -3
  85. package/dist/esm/transformations/ecommerce-product.js.map +1 -1
  86. package/dist/esm/transformations/index.d.ts +14 -2
  87. package/dist/esm/transformations/index.d.ts.map +1 -1
  88. package/dist/esm/transformations/index.js +36 -3
  89. package/dist/esm/transformations/index.js.map +1 -1
  90. package/dist/esm/transformations/shared-utils.d.ts.map +1 -1
  91. package/dist/esm/transformations/shared-utils.js +1 -1
  92. package/dist/esm/transformations/shared-utils.js.map +1 -1
  93. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  94. package/dist/esm/utils.d.ts +4 -3
  95. package/dist/esm/utils.d.ts.map +1 -1
  96. package/dist/esm/utils.js +6 -4
  97. package/dist/esm/utils.js.map +1 -1
  98. package/package.json +5 -5
  99. package/src/cache/ast.ts +265 -0
  100. package/src/cache/config.ts +79 -0
  101. package/src/cache/types.ts +31 -0
  102. package/src/data-source-fetchers.ts +66 -12
  103. package/src/fetchers/teleport.ts +3 -3
  104. package/src/index.ts +10 -4
  105. package/src/loading-state.ts +65 -2
  106. package/src/pagination-plugin.ts +315 -116
  107. package/src/transformations/ecommerce-product.ts +161 -5
  108. package/src/transformations/index.ts +43 -4
  109. package/src/transformations/shared-utils.ts +14 -0
  110. package/src/utils.ts +10 -5
@@ -3,14 +3,17 @@ import {
3
3
  ComponentPluginFactory,
4
4
  ChunkType,
5
5
  FileType,
6
- UIDLEcommerceCategory,
7
6
  } from '@teleporthq/teleport-types'
8
7
  import * as types from '@babel/types'
9
8
  import { parseExpression } from '@babel/parser'
10
- import { StringUtils } from '@teleporthq/teleport-shared'
9
+ import { ASTStatementOrder, StringUtils } from '@teleporthq/teleport-shared'
11
10
  import { ASTUtils, URLSearchParamSync } from '@teleporthq/teleport-plugin-common'
12
11
  import { generateSafeFileName } from './utils'
13
12
  import { generateDataSourceFetcherWithCore } from './data-source-fetchers'
13
+ import {
14
+ buildProductTransformOptions,
15
+ type EcommerceProductTransformOptions,
16
+ } from './transformations'
14
17
  import { appendSortsParam, DynamicSortAST, extractDynamicSort } from './sort-utils'
15
18
  import { appendFiltersParam, pushStateIdsAsDeps, pushPropIdsAsDeps } from './filter-utils'
16
19
  import {
@@ -18,6 +21,15 @@ import {
18
21
  buildLoadingStateDeclarations,
19
22
  getLoadingStateVars,
20
23
  } from './loading-state'
24
+ import type { DataSourceServerCacheOptions, ResolvedDataSourceCache } from './cache/types'
25
+ import { mergeServerCacheOptions, resolveCacheFromRepeaterContent } from './cache/config'
26
+ import {
27
+ buildCacheHydrationEffect,
28
+ buildCacheStoreThen,
29
+ buildCachedFetchBody,
30
+ buildCachedParamsDeclarations,
31
+ registerCacheClientImports,
32
+ } from './cache/ast'
21
33
 
22
34
  // ----- searchDefaultValue support -----
23
35
  //
@@ -186,6 +198,10 @@ interface DataSourceUsage {
186
198
  // client-side fetch refires when the buyer navigates between
187
199
  // `?categoryFilter=Rings` and `?categoryFilter=Necklaces`.
188
200
  filterUrlSearchParamKeys: string[]
201
+ // Resolved caching setup for this mapper, or `undefined` when it does not
202
+ // cache — in which case every emit site below must produce exactly what it
203
+ // produced before caching existed.
204
+ cache?: ResolvedDataSourceCache
189
205
  // Computed category
190
206
  category: 'paginated+search' | 'paginated-only' | 'search-only' | 'plain'
191
207
  }
@@ -568,6 +584,7 @@ function buildStateRegistry(uidlNode: any): StateRegistry {
568
584
  filterStateIds,
569
585
  filterPropIds,
570
586
  filterUrlSearchParamKeys,
587
+ cache: resolveCacheFromRepeaterContent(content.cache, parentDataSource.resourceDef),
571
588
  category: 'plain',
572
589
  }
573
590
 
@@ -1508,6 +1525,7 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
1508
1525
  // provider can use one depends on its JSX (it needs a `renderLoading` slot),
1509
1526
  // which is only known once the category updaters above have run.
1510
1527
  const loadingStateDeclarations: types.Statement[] = []
1528
+ const cacheDeclarations: types.Statement[] = []
1511
1529
 
1512
1530
  dataProvidersWithRepeaters.forEach((dp) => {
1513
1531
  const nameAttr = dp.openingElement.attributes.find(
@@ -1539,11 +1557,11 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
1539
1557
 
1540
1558
  // Update DataProvider based on category
1541
1559
  if (usage.category === 'paginated+search') {
1542
- updateDataProviderForPaginatedSearch(dp, usage, vars, fileName)
1560
+ updateDataProviderForPaginatedSearch(dp, usage, vars, fileName, cacheDeclarations)
1543
1561
  } else if (usage.category === 'paginated-only') {
1544
- updateDataProviderForPaginationOnly(dp, usage, vars, fileName)
1562
+ updateDataProviderForPaginationOnly(dp, usage, vars, fileName, cacheDeclarations)
1545
1563
  } else if (usage.category === 'search-only') {
1546
- updateDataProviderForSearchOnly(dp, usage, vars, fileName)
1564
+ updateDataProviderForSearchOnly(dp, usage, vars, fileName, cacheDeclarations)
1547
1565
  } else if (usage.category === 'plain') {
1548
1566
  updateDataProviderForPlain(dp, fileName, usage)
1549
1567
  }
@@ -1562,15 +1580,54 @@ export const createNextArrayMapperPaginationPlugin: ComponentPluginFactory<{}> =
1562
1580
  options.extractedResources,
1563
1581
  usage,
1564
1582
  options.dataSources,
1565
- options.ecommerceSettings?.categories
1583
+ buildProductTransformOptions(options)
1566
1584
  )
1567
1585
  })
1568
1586
 
1587
+ // tslint:disable-next-line:no-any
1588
+ const sharedOptions = options as any
1589
+ if (!sharedOptions.serverCacheByFile) {
1590
+ sharedOptions.serverCacheByFile = new Map()
1591
+ }
1592
+ applyServerCacheToDataSourceModules(
1593
+ options.extractedResources,
1594
+ registry,
1595
+ options.dataSources,
1596
+ buildProductTransformOptions(options),
1597
+ sharedOptions.serverCacheByFile
1598
+ )
1599
+
1569
1600
  // Declare the loading-state hooks alongside the other data-source state, at
1570
1601
  // the top of the component body. Unconditional and in a fixed order, so the
1571
1602
  // hook order stays stable across renders.
1572
1603
  loadingStateDeclarations.reverse().forEach((s) => blockStatement.body.unshift(s))
1573
1604
 
1605
+ const cachedScopes = Array.from(
1606
+ new Set(
1607
+ registry.usages
1608
+ .filter((usage) => usage.cache?.client)
1609
+ .map((usage) => (usage.cache as ResolvedDataSourceCache).scope)
1610
+ )
1611
+ )
1612
+
1613
+ if (cacheDeclarations.length || cachedScopes.length) {
1614
+ // One effect per page covering every scope on it: flips the hydration
1615
+ // latch, then asks once whether any of this page's tables changed while
1616
+ // the visitor was away. Without it a browser serving everything from its
1617
+ // own cache would never learn about a write at all. It has to land
1618
+ // BEFORE the component's `return` — a plain `push` at this point in the
1619
+ // pipeline appends after it, where React never registers the hook.
1620
+ ASTStatementOrder.insertStatementsBeforeReturn(blockStatement, [
1621
+ buildCacheHydrationEffect(cachedScopes),
1622
+ ])
1623
+ // `ds_N_params` / `ds_N_cached` read the `ds_N_state` hooks declared
1624
+ // above, so they are placed at the earliest point where everything they
1625
+ // reference is already in scope — never blindly at the top, which is
1626
+ // exactly the `Cannot access 'ds_0_state' before initialization` crash.
1627
+ ASTStatementOrder.insertStatementsAfterDependencies(blockStatement, cacheDeclarations)
1628
+ registerCacheClientImports(dependencies, uidl.outputOptions?.folderPath)
1629
+ }
1630
+
1574
1631
  // STEP 3.5: Handle DataProviders WITHOUT repeaters (data-source-item type)
1575
1632
  // These access single items like data[0].name and should not re-render on state changes
1576
1633
  // We wrap their params in useMemo to prevent reference changes from triggering re-renders
@@ -1826,11 +1883,61 @@ function findAllPaginationNodesInJSX(
1826
1883
  return results
1827
1884
  }
1828
1885
 
1886
+ /**
1887
+ * Emits the provider's `params` and, for a cached mapper, the peek beside it.
1888
+ *
1889
+ * An uncached mapper keeps the inline `params={useMemo(...)}` it has always had,
1890
+ * byte for byte. A cached one hoists that memo into a component const so the
1891
+ * peek can depend on the same object identity instead of rebuilding it.
1892
+ *
1893
+ * Returns the identifier holding the peeked rows, or `undefined` when this
1894
+ * mapper does not cache.
1895
+ */
1896
+ function pushParamsAttribute(
1897
+ // tslint:disable-next-line:no-any
1898
+ dp: any,
1899
+ usage: DataSourceUsage,
1900
+ paramsProps: types.ObjectProperty[],
1901
+ memoDeps: types.Expression[],
1902
+ cacheDeclarations: types.Statement[]
1903
+ ): string | undefined {
1904
+ const paramsMemo = types.callExpression(types.identifier('useMemo'), [
1905
+ types.arrowFunctionExpression([], types.objectExpression(paramsProps)),
1906
+ types.arrayExpression(memoDeps),
1907
+ ])
1908
+
1909
+ // A state-bound sort deliberately suppresses `initialData` (see the comment at
1910
+ // the call site): the prefetch ran without sort params, so reusing anything on
1911
+ // mount would mask the current sort. The cache peek is `initialData` by
1912
+ // another name, so it has to be suppressed for exactly the same reason.
1913
+ const cache = usage.cache?.client && !usage.dynamicSort ? usage.cache : undefined
1914
+
1915
+ if (!cache) {
1916
+ dp.openingElement.attributes.push(
1917
+ types.jsxAttribute(types.jsxIdentifier('params'), types.jsxExpressionContainer(paramsMemo))
1918
+ )
1919
+ return undefined
1920
+ }
1921
+
1922
+ const hoisted = buildCachedParamsDeclarations({ index: usage.index, paramsMemo, cache })
1923
+ cacheDeclarations.push(...hoisted.declarations)
1924
+
1925
+ dp.openingElement.attributes.push(
1926
+ types.jsxAttribute(
1927
+ types.jsxIdentifier('params'),
1928
+ types.jsxExpressionContainer(types.identifier(hoisted.paramsIdentifier))
1929
+ )
1930
+ )
1931
+
1932
+ return hoisted.cachedIdentifier
1933
+ }
1934
+
1829
1935
  function updateDataProviderForPaginatedSearch(
1830
1936
  dp: any,
1831
1937
  usage: DataSourceUsage,
1832
1938
  vars: ReturnType<typeof getStateVarsForUsage>,
1833
- fileName: string
1939
+ fileName: string,
1940
+ cacheDeclarations: types.Statement[]
1834
1941
  ): void {
1835
1942
  const attrs = dp.openingElement.attributes
1836
1943
 
@@ -1885,17 +1992,7 @@ function updateDataProviderForPaginatedSearch(
1885
1992
  }
1886
1993
  pushUrlSearchParamMemoDeps(memoDeps, usage)
1887
1994
 
1888
- dp.openingElement.attributes.push(
1889
- types.jsxAttribute(
1890
- types.jsxIdentifier('params'),
1891
- types.jsxExpressionContainer(
1892
- types.callExpression(types.identifier('useMemo'), [
1893
- types.arrowFunctionExpression([], types.objectExpression(paramsProps)),
1894
- types.arrayExpression(memoDeps),
1895
- ])
1896
- )
1897
- )
1898
- )
1995
+ const cachedInitialData = pushParamsAttribute(dp, usage, paramsProps, memoDeps, cacheDeclarations)
1899
1996
 
1900
1997
  // Add initialData. Skip the server-prefetched data entirely when a dynamic
1901
1998
  // state-bound sort is active — the prefetch ran without sort parameters, so
@@ -1935,7 +2032,7 @@ function updateDataProviderForPaginatedSearch(
1935
2032
  false,
1936
2033
  true
1937
2034
  ),
1938
- types.identifier('undefined')
2035
+ cachedInitialData ? types.identifier(cachedInitialData) : types.identifier('undefined')
1939
2036
  )
1940
2037
  )
1941
2038
  )
@@ -1976,7 +2073,9 @@ function updateDataProviderForPaginatedSearch(
1976
2073
  )
1977
2074
 
1978
2075
  // Add fetchData
1979
- dp.openingElement.attributes.push(createFetchDataAttribute(fileName))
2076
+ dp.openingElement.attributes.push(
2077
+ createFetchDataAttribute(fileName, usage.cache?.client ? usage.cache : undefined)
2078
+ )
1980
2079
 
1981
2080
  // Add persistDataDuringLoading
1982
2081
  dp.openingElement.attributes.push(
@@ -1991,7 +2090,8 @@ function updateDataProviderForPaginationOnly(
1991
2090
  dp: any,
1992
2091
  usage: DataSourceUsage,
1993
2092
  vars: ReturnType<typeof getStateVarsForUsage>,
1994
- fileName: string
2093
+ fileName: string,
2094
+ cacheDeclarations: types.Statement[]
1995
2095
  ): void {
1996
2096
  const attrs = dp.openingElement.attributes
1997
2097
 
@@ -2025,17 +2125,7 @@ function updateDataProviderForPaginationOnly(
2025
2125
  pushUrlSearchParamMemoDeps(memoDeps, usage)
2026
2126
 
2027
2127
  // Add params
2028
- dp.openingElement.attributes.push(
2029
- types.jsxAttribute(
2030
- types.jsxIdentifier('params'),
2031
- types.jsxExpressionContainer(
2032
- types.callExpression(types.identifier('useMemo'), [
2033
- types.arrowFunctionExpression([], types.objectExpression(paramsProps)),
2034
- types.arrayExpression(memoDeps),
2035
- ])
2036
- )
2037
- )
2038
- )
2128
+ const cachedInitialData = pushParamsAttribute(dp, usage, paramsProps, memoDeps, cacheDeclarations)
2039
2129
 
2040
2130
  // Add initialData. See paginated+search updater for why we skip prefetch
2041
2131
  // reuse when a dynamic state-bound sort is active.
@@ -2059,7 +2149,7 @@ function updateDataProviderForPaginationOnly(
2059
2149
  false,
2060
2150
  true
2061
2151
  ),
2062
- types.identifier('undefined')
2152
+ cachedInitialData ? types.identifier(cachedInitialData) : types.identifier('undefined')
2063
2153
  )
2064
2154
  )
2065
2155
  )
@@ -2086,7 +2176,9 @@ function updateDataProviderForPaginationOnly(
2086
2176
  )
2087
2177
 
2088
2178
  // Add fetchData
2089
- dp.openingElement.attributes.push(createFetchDataAttribute(fileName))
2179
+ dp.openingElement.attributes.push(
2180
+ createFetchDataAttribute(fileName, usage.cache?.client ? usage.cache : undefined)
2181
+ )
2090
2182
 
2091
2183
  dp.openingElement.attributes.push(
2092
2184
  types.jsxAttribute(
@@ -2100,7 +2192,8 @@ function updateDataProviderForSearchOnly(
2100
2192
  dp: any,
2101
2193
  usage: DataSourceUsage,
2102
2194
  vars: ReturnType<typeof getStateVarsForUsage>,
2103
- fileName: string
2195
+ fileName: string,
2196
+ cacheDeclarations: types.Statement[]
2104
2197
  ): void {
2105
2198
  const attrs = dp.openingElement.attributes
2106
2199
 
@@ -2143,17 +2236,7 @@ function updateDataProviderForSearchOnly(
2143
2236
  }
2144
2237
  pushUrlSearchParamMemoDeps(memoDeps, usage)
2145
2238
 
2146
- dp.openingElement.attributes.push(
2147
- types.jsxAttribute(
2148
- types.jsxIdentifier('params'),
2149
- types.jsxExpressionContainer(
2150
- types.callExpression(types.identifier('useMemo'), [
2151
- types.arrowFunctionExpression([], types.objectExpression(paramsProps)),
2152
- types.arrayExpression(memoDeps),
2153
- ])
2154
- )
2155
- )
2156
- )
2239
+ const cachedInitialData = pushParamsAttribute(dp, usage, paramsProps, memoDeps, cacheDeclarations)
2157
2240
 
2158
2241
  // Add initialData. See paginated+search updater for why we skip prefetch
2159
2242
  // reuse when a dynamic state-bound sort is active.
@@ -2173,7 +2256,7 @@ function updateDataProviderForSearchOnly(
2173
2256
  false,
2174
2257
  true
2175
2258
  ),
2176
- types.identifier('undefined')
2259
+ cachedInitialData ? types.identifier(cachedInitialData) : types.identifier('undefined')
2177
2260
  )
2178
2261
  )
2179
2262
  )
@@ -2197,7 +2280,9 @@ function updateDataProviderForSearchOnly(
2197
2280
  )
2198
2281
 
2199
2282
  // Add fetchData
2200
- dp.openingElement.attributes.push(createFetchDataAttribute(fileName))
2283
+ dp.openingElement.attributes.push(
2284
+ createFetchDataAttribute(fileName, usage.cache?.client ? usage.cache : undefined)
2285
+ )
2201
2286
 
2202
2287
  dp.openingElement.attributes.push(
2203
2288
  types.jsxAttribute(
@@ -2210,13 +2295,22 @@ function updateDataProviderForSearchOnly(
2210
2295
  function updateDataProviderForPlain(dp: any, fileName: string, usage: DataSourceUsage): void {
2211
2296
  const attrs = dp.openingElement.attributes
2212
2297
 
2298
+ const clientCache = usage.cache?.client ? usage.cache : undefined
2299
+
2213
2300
  // Check if fetchData already exists
2214
2301
  const hasFetchData = attrs.some(
2215
2302
  (attr: any) => attr.type === 'JSXAttribute' && attr.name?.name === 'fetchData'
2216
2303
  )
2217
2304
 
2218
- // If no fetchData, add it
2219
- if (!hasFetchData) {
2305
+ if (clientCache) {
2306
+ // A plain provider may already carry the UNMEMOIZED fetcher `utils.ts`
2307
+ // emits. Caching has to replace it rather than skip: the memoized form is
2308
+ // what both the cache peek and the in-flight tracking are attached to.
2309
+ dp.openingElement.attributes = attrs.filter(
2310
+ (attr: any) => !(attr.type === 'JSXAttribute' && attr.name?.name === 'fetchData')
2311
+ )
2312
+ dp.openingElement.attributes.push(createFetchDataAttribute(fileName, clientCache))
2313
+ } else if (!hasFetchData) {
2220
2314
  attrs.push(createFetchDataAttribute(fileName))
2221
2315
  }
2222
2316
 
@@ -2334,73 +2428,75 @@ function stabilizeDataProviderWithoutRepeater(dp: any): void {
2334
2428
  )
2335
2429
  }
2336
2430
 
2337
- function createFetchDataAttribute(fileName: string): types.JSXAttribute {
2431
+ function createFetchDataAttribute(
2432
+ fileName: string,
2433
+ cache?: ResolvedDataSourceCache
2434
+ ): types.JSXAttribute {
2435
+ const fetchCall = types.callExpression(types.identifier('fetch'), [
2436
+ types.templateLiteral(
2437
+ [
2438
+ types.templateElement({
2439
+ raw: `/api/${fileName}?`,
2440
+ cooked: `/api/${fileName}?`,
2441
+ }),
2442
+ types.templateElement({ raw: '', cooked: '' }),
2443
+ ],
2444
+ [types.newExpression(types.identifier('URLSearchParams'), [types.identifier('params')])]
2445
+ ),
2446
+ types.objectExpression([
2447
+ types.objectProperty(
2448
+ types.identifier('headers'),
2449
+ types.objectExpression([
2450
+ types.objectProperty(
2451
+ types.stringLiteral('Content-Type'),
2452
+ types.stringLiteral('application/json')
2453
+ ),
2454
+ ])
2455
+ ),
2456
+ ]),
2457
+ ])
2458
+
2459
+ const parsedJson = types.callExpression(
2460
+ types.memberExpression(fetchCall, types.identifier('then')),
2461
+ [
2462
+ types.arrowFunctionExpression(
2463
+ [types.identifier('res')],
2464
+ types.callExpression(
2465
+ types.memberExpression(types.identifier('res'), types.identifier('json')),
2466
+ []
2467
+ )
2468
+ ),
2469
+ ]
2470
+ )
2471
+
2472
+ // The uncached tail is left exactly as it has always been emitted, so a
2473
+ // project that does not cache produces byte-identical output.
2474
+ const uncachedTail = types.arrowFunctionExpression(
2475
+ [types.identifier('response')],
2476
+ types.optionalMemberExpression(
2477
+ types.identifier('response'),
2478
+ types.identifier('data'),
2479
+ false,
2480
+ true
2481
+ )
2482
+ )
2483
+
2484
+ const networkChain = types.callExpression(
2485
+ types.memberExpression(parsedJson, types.identifier('then')),
2486
+ [cache ? buildCacheStoreThen(cache) : uncachedTail]
2487
+ )
2488
+
2489
+ const arrow = cache
2490
+ ? types.arrowFunctionExpression(
2491
+ [types.identifier('params')],
2492
+ buildCachedFetchBody(networkChain, cache)
2493
+ )
2494
+ : types.arrowFunctionExpression([types.identifier('params')], networkChain)
2495
+
2338
2496
  return types.jsxAttribute(
2339
2497
  types.jsxIdentifier('fetchData'),
2340
2498
  types.jsxExpressionContainer(
2341
- types.callExpression(types.identifier('useCallback'), [
2342
- types.arrowFunctionExpression(
2343
- [types.identifier('params')],
2344
- types.callExpression(
2345
- types.memberExpression(
2346
- types.callExpression(
2347
- types.memberExpression(
2348
- types.callExpression(types.identifier('fetch'), [
2349
- types.templateLiteral(
2350
- [
2351
- types.templateElement({
2352
- raw: `/api/${fileName}?`,
2353
- cooked: `/api/${fileName}?`,
2354
- }),
2355
- types.templateElement({ raw: '', cooked: '' }),
2356
- ],
2357
- [
2358
- types.newExpression(types.identifier('URLSearchParams'), [
2359
- types.identifier('params'),
2360
- ]),
2361
- ]
2362
- ),
2363
- types.objectExpression([
2364
- types.objectProperty(
2365
- types.identifier('headers'),
2366
- types.objectExpression([
2367
- types.objectProperty(
2368
- types.stringLiteral('Content-Type'),
2369
- types.stringLiteral('application/json')
2370
- ),
2371
- ])
2372
- ),
2373
- ]),
2374
- ]),
2375
- types.identifier('then')
2376
- ),
2377
- [
2378
- types.arrowFunctionExpression(
2379
- [types.identifier('res')],
2380
- types.callExpression(
2381
- types.memberExpression(types.identifier('res'), types.identifier('json')),
2382
- []
2383
- )
2384
- ),
2385
- ]
2386
- ),
2387
- types.identifier('then')
2388
- ),
2389
- [
2390
- types.arrowFunctionExpression(
2391
- [types.identifier('response')],
2392
- types.optionalMemberExpression(
2393
- types.identifier('response'),
2394
- types.identifier('data'),
2395
- false,
2396
- true
2397
- )
2398
- ),
2399
- ]
2400
- )
2401
- ),
2402
- types.arrayExpression([]),
2403
- ])
2499
+ types.callExpression(types.identifier('useCallback'), [arrow, types.arrayExpression([])])
2404
2500
  )
2405
2501
  )
2406
2502
  }
@@ -2697,11 +2793,114 @@ function wirePaginationButtons(
2697
2793
  }
2698
2794
  }
2699
2795
 
2796
+ /**
2797
+ * Applies the SERVER cache to the generated data-source modules, once, after
2798
+ * every provider has been visited.
2799
+ *
2800
+ * A post-pass rather than a parameter to `ensureAPIRouteExists` because four
2801
+ * different places race to create `utils/data-sources/<file>.js` — the
2802
+ * getStaticProps path, the component path, the client-API path and this plugin —
2803
+ * and whichever wins would otherwise decide whether the file is cached. Since
2804
+ * the generator is deterministic, regenerating the winner with the cache options
2805
+ * is order-independent and cannot half-apply.
2806
+ *
2807
+ * The config is merged per FILE, not per mapper: one module serves every mapper
2808
+ * reading that `(type, table, dataSourceId)` triple, so two mappers on two pages
2809
+ * have to agree — and the fresher one wins (see `mergeServerCacheOptions`).
2810
+ */
2811
+ function applyServerCacheToDataSourceModules(
2812
+ // tslint:disable-next-line:no-any
2813
+ extractedResources: any,
2814
+ registry: StateRegistry,
2815
+ // tslint:disable-next-line:no-any
2816
+ dataSources: Record<string, any>,
2817
+ transformOptions: EcommerceProductTransformOptions = {},
2818
+ // Accumulated across every page/component this generation has processed. The
2819
+ // module is shared project-wide, so merging only the CURRENT page's usages
2820
+ // would let page ORDER decide the TTL: a page asking for 300s processed after
2821
+ // one asking for 60s would widen the window the second page was configured
2822
+ // with. Carried on the shared options object so the merge stays global.
2823
+ accumulator?: Map<string, { usage: DataSourceUsage; entries: DataSourceServerCacheOptions[] }>
2824
+ ): void {
2825
+ const byFileName =
2826
+ accumulator ??
2827
+ new Map<string, { usage: DataSourceUsage; entries: DataSourceServerCacheOptions[] }>()
2828
+
2829
+ registry.usages.forEach((usage) => {
2830
+ if (!usage.cache?.server) {
2831
+ return
2832
+ }
2833
+ const fileName = generateSafeFileName(
2834
+ usage.resourceDefinition.dataSourceType,
2835
+ usage.resourceDefinition.tableName,
2836
+ usage.resourceDefinition.dataSourceId
2837
+ )
2838
+ const bucket = byFileName.get(fileName) || { usage, entries: [] }
2839
+ bucket.entries.push({
2840
+ scope: usage.cache.scope,
2841
+ ttlSeconds: usage.cache.ttlSeconds,
2842
+ sMaxAge: usage.cache.sMaxAge,
2843
+ staleWhileRevalidate: usage.cache.staleWhileRevalidate,
2844
+ })
2845
+ byFileName.set(fileName, bucket)
2846
+ })
2847
+
2848
+ byFileName.forEach((bucket, fileName) => {
2849
+ const cacheOptions = mergeServerCacheOptions(bucket.entries)
2850
+ if (!cacheOptions) {
2851
+ return
2852
+ }
2853
+
2854
+ const dataSource = dataSources[bucket.usage.resourceDefinition.dataSourceId]
2855
+ if (!dataSource) {
2856
+ return
2857
+ }
2858
+
2859
+ const tableName = bucket.usage.resourceDefinition.tableName || 'data'
2860
+ const utilsKey = `utils/${fileName}`
2861
+ const apiKey = `api/${fileName}`
2862
+
2863
+ try {
2864
+ if (extractedResources[utilsKey]) {
2865
+ extractedResources[utilsKey].content = generateDataSourceFetcherWithCore(
2866
+ dataSource,
2867
+ tableName,
2868
+ false,
2869
+ transformOptions,
2870
+ cacheOptions
2871
+ )
2872
+ return
2873
+ }
2874
+
2875
+ // The variant that inlines the whole fetcher straight into `pages/api`
2876
+ // instead of re-exporting a utils module. Detected by content rather than
2877
+ // by which code path produced it, so a future fifth emitter is covered too.
2878
+ const apiEntry = extractedResources[apiKey]
2879
+ if (
2880
+ apiEntry &&
2881
+ typeof apiEntry.content === 'string' &&
2882
+ apiEntry.content.includes('async function handler')
2883
+ ) {
2884
+ apiEntry.content = generateDataSourceFetcherWithCore(
2885
+ dataSource,
2886
+ tableName,
2887
+ true,
2888
+ transformOptions,
2889
+ cacheOptions
2890
+ )
2891
+ }
2892
+ } catch (error) {
2893
+ // Caching must never be the reason a project fails to generate; the
2894
+ // uncached module that is already in place stays exactly as it was.
2895
+ }
2896
+ })
2897
+ }
2898
+
2700
2899
  function ensureAPIRouteExists(
2701
2900
  extractedResources: any,
2702
2901
  usage: DataSourceUsage,
2703
2902
  dataSources: Record<string, any>,
2704
- categories?: UIDLEcommerceCategory[]
2903
+ transformOptions: EcommerceProductTransformOptions = {}
2705
2904
  ): void {
2706
2905
  // Generate file name for the API route
2707
2906
  const fileName = generateSafeFileName(
@@ -2721,7 +2920,7 @@ function ensureAPIRouteExists(
2721
2920
  dataSource,
2722
2921
  usage.resourceDefinition.tableName || 'data',
2723
2922
  false,
2724
- categories
2923
+ transformOptions
2725
2924
  )
2726
2925
 
2727
2926
  extractedResources[`utils/${fileName}`] = {