@teleporthq/teleport-plugin-next-data-source 0.43.46 → 0.43.47

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.
@@ -0,0 +1,264 @@
1
+ import * as types from '@babel/types'
2
+ import generator from '@babel/generator'
3
+ import {
4
+ ChunkType,
5
+ FileType,
6
+ ComponentStructure,
7
+ ChunkDefinition,
8
+ } from '@teleporthq/teleport-types'
9
+ import { createNextArrayMapperPaginationPlugin } from '../src/pagination-plugin'
10
+
11
+ // A `<DataProvider>` shaped like the one `generateDataSourceNode` emits for an
12
+ // array mapper: a `renderSuccess` holding a `Repeater` (that is what marks it as
13
+ // an array-mapper provider) and, when the mapper has a loading state designed,
14
+ // a `renderLoading` slot.
15
+ const makeDataProviderJSX = (options: { withLoadingSlot: boolean }): types.JSXElement => {
16
+ const repeater = types.jsxElement(
17
+ types.jsxOpeningElement(
18
+ types.jsxIdentifier('Repeater'),
19
+ [
20
+ types.jsxAttribute(
21
+ types.jsxIdentifier('renderItem'),
22
+ types.jsxExpressionContainer(
23
+ types.arrowFunctionExpression(
24
+ [types.identifier('product'), types.identifier('index')],
25
+ types.jsxElement(
26
+ types.jsxOpeningElement(types.jsxIdentifier('div'), [], true),
27
+ null,
28
+ [],
29
+ true
30
+ )
31
+ )
32
+ )
33
+ ),
34
+ ],
35
+ true
36
+ ),
37
+ null,
38
+ [],
39
+ true
40
+ )
41
+
42
+ const attributes: types.JSXAttribute[] = [
43
+ types.jsxAttribute(
44
+ types.jsxIdentifier('name'),
45
+ types.jsxExpressionContainer(types.stringLiteral('items'))
46
+ ),
47
+ types.jsxAttribute(
48
+ types.jsxIdentifier('renderSuccess'),
49
+ types.jsxExpressionContainer(
50
+ types.arrowFunctionExpression([types.identifier('items')], repeater)
51
+ )
52
+ ),
53
+ ]
54
+
55
+ if (options.withLoadingSlot) {
56
+ attributes.push(
57
+ types.jsxAttribute(
58
+ types.jsxIdentifier('renderLoading'),
59
+ types.jsxExpressionContainer(
60
+ types.arrowFunctionExpression(
61
+ [],
62
+ types.jsxElement(
63
+ types.jsxOpeningElement(types.jsxIdentifier('p'), [], true),
64
+ null,
65
+ [],
66
+ true
67
+ )
68
+ )
69
+ )
70
+ )
71
+ )
72
+ }
73
+
74
+ return types.jsxElement(
75
+ types.jsxOpeningElement(types.jsxIdentifier('DataProvider'), attributes, true),
76
+ null,
77
+ [],
78
+ true
79
+ )
80
+ }
81
+
82
+ const makeComponentChunk = (dataProvider: types.JSXElement): ChunkDefinition => {
83
+ const body = types.blockStatement([types.returnStatement(dataProvider)])
84
+ const arrow = types.arrowFunctionExpression([types.identifier('props')], body)
85
+ const declaration = types.variableDeclaration('const', [
86
+ types.variableDeclarator(types.identifier('ProductsList'), arrow),
87
+ ])
88
+ return {
89
+ name: 'jsx-component',
90
+ type: ChunkType.AST,
91
+ fileType: FileType.JS,
92
+ linkAfter: [],
93
+ content: declaration,
94
+ meta: {},
95
+ }
96
+ }
97
+
98
+ // A `data-source-list > cms-list-repeater` UIDL. `category` decides which
99
+ // DataProvider updater runs, so every one of them is covered below.
100
+ const makeUidlNode = (
101
+ category: 'paginated+search' | 'paginated-only' | 'search-only' | 'plain'
102
+ // tslint:disable-next-line:no-any
103
+ ): any => ({
104
+ type: 'data-source-list',
105
+ content: {
106
+ renderPropIdentifier: 'items',
107
+ resourceDefinition: {
108
+ dataSourceId: 'ds1',
109
+ tableName: 'products',
110
+ dataSourceType: 'postgresql',
111
+ },
112
+ resource: { params: { queryColumns: { content: ['name'] } } },
113
+ nodes: {
114
+ success: {
115
+ type: 'cms-list-repeater',
116
+ content: {
117
+ renderPropIdentifier: 'product',
118
+ paginated: category === 'paginated+search' || category === 'paginated-only',
119
+ perPage: 20,
120
+ searchEnabled: category === 'paginated+search' || category === 'search-only',
121
+ searchDebounce: 300,
122
+ nodes: { list: { type: 'element', content: { elementType: 'div' } } },
123
+ },
124
+ },
125
+ },
126
+ },
127
+ })
128
+
129
+ const runPlugin = async (options: {
130
+ category?: 'paginated+search' | 'paginated-only' | 'search-only' | 'plain'
131
+ withLoadingSlot?: boolean
132
+ }): Promise<string> => {
133
+ const dataProvider = makeDataProviderJSX({ withLoadingSlot: options.withLoadingSlot !== false })
134
+ const chunk = makeComponentChunk(dataProvider)
135
+ const structure: ComponentStructure = {
136
+ uidl: { name: 'ProductsList', node: makeUidlNode(options.category || 'paginated+search') },
137
+ chunks: [chunk],
138
+ dependencies: {},
139
+ options: { dataSources: {}, extractedResources: {} },
140
+ } as never
141
+ const plugin = createNextArrayMapperPaginationPlugin()
142
+ await plugin(structure)
143
+ return generator(chunk.content as types.Node).code
144
+ }
145
+
146
+ describe('pagination plugin — refetch loading state', () => {
147
+ it('declares the in-flight tracking hooks and drives persistDataDuringLoading from them', async () => {
148
+ const code = await runPlugin({})
149
+
150
+ // The state definition that displays the loading state, plus the counter
151
+ // that keeps it honest across overlapping requests.
152
+ expect(code).toContain('const ds_0_fetchesInFlight = useRef(0)')
153
+ expect(code).toContain('const [ds_0_isFetching, setDs_0_isFetching] = useState(false)')
154
+
155
+ // While a fetch is in flight the provider stops persisting the previous rows
156
+ // and falls through to its renderLoading slot.
157
+ expect(code).toContain('persistDataDuringLoading={!ds_0_isFetching}')
158
+ expect(code).not.toContain('persistDataDuringLoading={true}')
159
+ })
160
+
161
+ it('raises the flag when the request starts and lowers it once it settles', async () => {
162
+ const code = await runPlugin({})
163
+
164
+ // Raised synchronously, before the request is issued, so the flip is batched
165
+ // with the provider's own switch to its loading status (no intermediate paint).
166
+ expect(code).toMatch(
167
+ /ds_0_fetchesInFlight\.current \+= 1;?\s*setDs_0_isFetching\(true\);?\s*return fetch\(/
168
+ )
169
+
170
+ // Lowered in `finally`, so a rejected request cannot leave it stuck on.
171
+ expect(code).toContain('.finally(() => {')
172
+ expect(code).toContain('ds_0_fetchesInFlight.current -= 1')
173
+ expect(code).toContain('setDs_0_isFetching(false)')
174
+
175
+ // Only the LAST outstanding request lowers the flag, and the counter is
176
+ // clamped so a late settle from a remounted provider cannot drive it negative.
177
+ expect(code).toContain('if (ds_0_fetchesInFlight.current <= 0)')
178
+ expect(code).toContain('ds_0_fetchesInFlight.current = 0')
179
+ })
180
+
181
+ it('keeps fetchData referentially stable so the provider does not refetch in a loop', async () => {
182
+ const code = await runPlugin({})
183
+
184
+ // The wrapped callback only closes over a ref object and a setState setter,
185
+ // both stable across renders — the empty dependency array stays correct.
186
+ expect(code).toMatch(/fetchData=\{useCallback\(params => \{[\s\S]*?\}, \[\]\)\}/)
187
+ })
188
+
189
+ it.each([
190
+ ['paginated+search' as const],
191
+ ['paginated-only' as const],
192
+ ['search-only' as const],
193
+ ['plain' as const],
194
+ ])('wires the loading state for the %s data source category', async (category) => {
195
+ const code = await runPlugin({ category })
196
+
197
+ expect(code).toContain('const [ds_0_isFetching, setDs_0_isFetching] = useState(false)')
198
+ expect(code).toContain('persistDataDuringLoading={!ds_0_isFetching}')
199
+ expect(code).toContain('setDs_0_isFetching(true)')
200
+ })
201
+
202
+ it('is idempotent — a second pass does not double-wrap fetchData', async () => {
203
+ const dataProvider = makeDataProviderJSX({ withLoadingSlot: true })
204
+ const chunk = makeComponentChunk(dataProvider)
205
+ const structure: ComponentStructure = {
206
+ uidl: { name: 'ProductsList', node: makeUidlNode('paginated+search') },
207
+ chunks: [chunk],
208
+ dependencies: {},
209
+ options: { dataSources: {}, extractedResources: {} },
210
+ } as never
211
+ const plugin = createNextArrayMapperPaginationPlugin()
212
+
213
+ await plugin(structure)
214
+ await plugin(structure)
215
+
216
+ const code = generator(chunk.content as types.Node).code
217
+ expect((code.match(/setDs_0_isFetching\(true\)/g) || []).length).toBe(1)
218
+ expect((code.match(/ds_0_fetchesInFlight\.current \+= 1/g) || []).length).toBe(1)
219
+ })
220
+
221
+ it('leaves an unmemoized fetchData alone rather than building a refetch loop', async () => {
222
+ // A `fetchData` that is re-created on every render already refetches on
223
+ // every render (DataProvider keys its effect on the fetcher identity);
224
+ // adding a setState to it would make that loop self-sustaining. Such a
225
+ // provider is skipped entirely.
226
+ const dataProvider = makeDataProviderJSX({ withLoadingSlot: true })
227
+ dataProvider.openingElement.attributes.push(
228
+ types.jsxAttribute(
229
+ types.jsxIdentifier('fetchData'),
230
+ types.jsxExpressionContainer(
231
+ types.arrowFunctionExpression(
232
+ [types.identifier('params')],
233
+ types.callExpression(types.identifier('fetch'), [types.stringLiteral('/api/items')])
234
+ )
235
+ )
236
+ )
237
+ )
238
+ const chunk = makeComponentChunk(dataProvider)
239
+ const structure: ComponentStructure = {
240
+ // `plain` is the only category whose updater keeps an existing fetchData
241
+ // instead of replacing it with the memoized one.
242
+ uidl: { name: 'ProductsList', node: makeUidlNode('plain') },
243
+ chunks: [chunk],
244
+ dependencies: {},
245
+ options: { dataSources: {}, extractedResources: {} },
246
+ } as never
247
+ await createNextArrayMapperPaginationPlugin()(structure)
248
+
249
+ const code = generator(chunk.content as types.Node).code
250
+ expect(code).not.toContain('ds_0_isFetching')
251
+ expect(code).not.toContain('ds_0_fetchesInFlight')
252
+ })
253
+
254
+ it('leaves a provider without a designed loading state untouched', async () => {
255
+ // With no renderLoading slot, dropping persistDataDuringLoading would blank
256
+ // the list out mid-refetch instead of showing something — keeping the
257
+ // previous rows is the better of the two, so nothing is wired.
258
+ const code = await runPlugin({ withLoadingSlot: false })
259
+
260
+ expect(code).not.toContain('ds_0_isFetching')
261
+ expect(code).not.toContain('ds_0_fetchesInFlight')
262
+ expect(code).toContain('persistDataDuringLoading={true}')
263
+ })
264
+ })
@@ -0,0 +1,92 @@
1
+ import * as types from '@babel/types';
2
+ /**
3
+ * ------------------------------------------------------------------
4
+ * Refetch loading state for array-mapper `DataProvider`s
5
+ * ------------------------------------------------------------------
6
+ *
7
+ * `DataProvider` (from `@teleporthq/react-components`) renders its LOADING
8
+ * slot only when it has no data to show:
9
+ *
10
+ * case "idle":
11
+ * case "loading":
12
+ * return props.persistDataDuringLoading && data
13
+ * ? renderSuccess(data, true)
14
+ * : renderLoading()
15
+ *
16
+ * Every generated array-mapper list ships `persistDataDuringLoading={true}`,
17
+ * and that flag is load-bearing on the FIRST paint: when the page prefetches
18
+ * rows in `getStaticProps` and hands them over as `initialData`, the provider
19
+ * deliberately skips its first client fetch and therefore never leaves the
20
+ * `idle` status. Without the flag that page would render the loading slot
21
+ * forever instead of the prefetched rows.
22
+ *
23
+ * The side effect is that once the provider owns data, EVERY later refetch is
24
+ * invisible: changing the category filter, the sort order or the search term
25
+ * re-runs the query while the stale rows stay on screen, so the buyer gets no
26
+ * feedback until the new rows pop in. (Page changes and search changes also
27
+ * remount the provider through its `key`, which resets `data` — but a filter
28
+ * or sort change keeps the same key, so nothing at all happens visually.)
29
+ *
30
+ * The provider does pass an `isLoading` flag as the second argument of
31
+ * `renderSuccess`, but it is `true` for the `idle` status as well — i.e.
32
+ * permanently true on any page that skipped its first fetch because it had
33
+ * `initialData` — so it cannot be used to decide what to paint.
34
+ *
35
+ * Fix: track the in-flight fetches of each data source in the PAGE component
36
+ * and hand `persistDataDuringLoading` the negated flag. `fetchData` is the one
37
+ * function the generator owns and the provider calls exactly once per fetch, so
38
+ * it is where the flag is raised (`true` when the request starts) and lowered
39
+ * (`false` once it settles, success or failure). While a refetch is in flight
40
+ * `persistDataDuringLoading` is `false`, so the provider falls through to
41
+ * `renderLoading()` and the array mapper's designed loading state shows; when
42
+ * the request settles the flag drops back to `true` and the rows return.
43
+ *
44
+ * Why not branch inside `renderSuccess` instead: the loading JSX would have to
45
+ * be duplicated into the success render prop, and `styled-jsx` only scopes JSX
46
+ * that lives inside the component's returned tree — every workaround that
47
+ * hoists the loading markup into a shared local helper silently loses its
48
+ * `jsx-<hash>` class and therefore all of its styles.
49
+ *
50
+ * Nothing changes for a provider that is idle or mounting: `isFetching` starts
51
+ * at `false`, so the server render and the first client render are identical
52
+ * (no hydration mismatch) and the `initialData` fast path is untouched.
53
+ */
54
+ export interface LoadingStateVars {
55
+ /** Boolean state: `true` while at least one fetch for this data source is in flight. */
56
+ isFetchingVar: string;
57
+ /** Setter for `isFetchingVar`. */
58
+ setIsFetchingVar: string;
59
+ /**
60
+ * Ref holding the number of fetches currently in flight. A boolean alone is
61
+ * not enough: two controls changed in quick succession (e.g. category then
62
+ * sort) start two overlapping requests, and the first one to settle would
63
+ * otherwise lower the flag while the second is still running — flashing the
64
+ * stale rows back for the rest of the second request.
65
+ */
66
+ inFlightRefVar: string;
67
+ }
68
+ export declare function getLoadingStateVars(index: number): LoadingStateVars;
69
+ /**
70
+ * `const ds_N_fetchesInFlight = useRef(0)`
71
+ * `const [ds_N_isFetching, setDs_N_isFetching] = useState(false)`
72
+ *
73
+ * Both are stable across renders (a ref object and a `useState` setter), which
74
+ * is what lets the wrapped `fetchData` keep its empty `useCallback` dependency
75
+ * array — a changing `fetchData` identity would retrigger the provider's fetch
76
+ * effect on every render.
77
+ */
78
+ export declare function buildLoadingStateDeclarations(vars: LoadingStateVars): types.Statement[];
79
+ /**
80
+ * Wires the refetch loading state into a single `DataProvider` JSX element.
81
+ *
82
+ * No-ops (returning `false`, so the caller emits no state declarations) when
83
+ * the provider cannot benefit from it:
84
+ * - no `renderLoading` slot — the array mapper has no loading state designed,
85
+ * so falling through to it would blank the list out instead of showing
86
+ * something; keeping the stale rows is the better of the two.
87
+ * - `fetchData` missing, not memoized, or not an expression-bodied promise
88
+ * chain — see `findMemoizedFetchDataArrow`.
89
+ * - already wired — keeps a second pass over the same AST idempotent.
90
+ */
91
+ export declare function applyLoadingStateToDataProvider(dataProvider: any, vars: LoadingStateVars): boolean;
92
+ //# sourceMappingURL=loading-state.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loading-state.d.ts","sourceRoot":"","sources":["../../src/loading-state.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,cAAc,CAAA;AAErC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,wFAAwF;IACxF,aAAa,EAAE,MAAM,CAAA;IACrB,kCAAkC;IAClC,gBAAgB,EAAE,MAAM,CAAA;IACxB;;;;;;OAMG;IACH,cAAc,EAAE,MAAM,CAAA;CACvB;AAED,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,gBAAgB,CAMnE;AAED;;;;;;;;GAQG;AACH,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,gBAAgB,GAAG,KAAK,CAAC,SAAS,EAAE,CAkBvF;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,+BAA+B,CAE7C,YAAY,EAAE,GAAG,EACjB,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAyBT"}
@@ -0,0 +1,186 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.applyLoadingStateToDataProvider = exports.buildLoadingStateDeclarations = exports.getLoadingStateVars = void 0;
27
+ var types = __importStar(require("@babel/types"));
28
+ function getLoadingStateVars(index) {
29
+ return {
30
+ isFetchingVar: "ds_".concat(index, "_isFetching"),
31
+ setIsFetchingVar: "setDs_".concat(index, "_isFetching"),
32
+ inFlightRefVar: "ds_".concat(index, "_fetchesInFlight"),
33
+ };
34
+ }
35
+ exports.getLoadingStateVars = getLoadingStateVars;
36
+ /**
37
+ * `const ds_N_fetchesInFlight = useRef(0)`
38
+ * `const [ds_N_isFetching, setDs_N_isFetching] = useState(false)`
39
+ *
40
+ * Both are stable across renders (a ref object and a `useState` setter), which
41
+ * is what lets the wrapped `fetchData` keep its empty `useCallback` dependency
42
+ * array — a changing `fetchData` identity would retrigger the provider's fetch
43
+ * effect on every render.
44
+ */
45
+ function buildLoadingStateDeclarations(vars) {
46
+ return [
47
+ types.variableDeclaration('const', [
48
+ types.variableDeclarator(types.identifier(vars.inFlightRefVar), types.callExpression(types.identifier('useRef'), [types.numericLiteral(0)])),
49
+ ]),
50
+ types.variableDeclaration('const', [
51
+ types.variableDeclarator(types.arrayPattern([
52
+ types.identifier(vars.isFetchingVar),
53
+ types.identifier(vars.setIsFetchingVar),
54
+ ]), types.callExpression(types.identifier('useState'), [types.booleanLiteral(false)])),
55
+ ]),
56
+ ];
57
+ }
58
+ exports.buildLoadingStateDeclarations = buildLoadingStateDeclarations;
59
+ /**
60
+ * Wires the refetch loading state into a single `DataProvider` JSX element.
61
+ *
62
+ * No-ops (returning `false`, so the caller emits no state declarations) when
63
+ * the provider cannot benefit from it:
64
+ * - no `renderLoading` slot — the array mapper has no loading state designed,
65
+ * so falling through to it would blank the list out instead of showing
66
+ * something; keeping the stale rows is the better of the two.
67
+ * - `fetchData` missing, not memoized, or not an expression-bodied promise
68
+ * chain — see `findMemoizedFetchDataArrow`.
69
+ * - already wired — keeps a second pass over the same AST idempotent.
70
+ */
71
+ function applyLoadingStateToDataProvider(
72
+ // tslint:disable-next-line:no-any
73
+ dataProvider, vars) {
74
+ var _a;
75
+ var attributes = (_a = dataProvider === null || dataProvider === void 0 ? void 0 : dataProvider.openingElement) === null || _a === void 0 ? void 0 : _a.attributes;
76
+ if (!Array.isArray(attributes)) {
77
+ return false;
78
+ }
79
+ if (!findAttribute(attributes, 'renderLoading')) {
80
+ return false;
81
+ }
82
+ var fetchDataAttr = findAttribute(attributes, 'fetchData');
83
+ if (!fetchDataAttr) {
84
+ return false;
85
+ }
86
+ var fetchArrow = findMemoizedFetchDataArrow(fetchDataAttr);
87
+ if (!fetchArrow || fetchArrow.body.type !== 'CallExpression') {
88
+ return false;
89
+ }
90
+ fetchArrow.body = buildTrackedFetchBody(fetchArrow.body, vars);
91
+ setPersistDataDuringLoading(attributes, vars);
92
+ return true;
93
+ }
94
+ exports.applyLoadingStateToDataProvider = applyLoadingStateToDataProvider;
95
+ // tslint:disable-next-line:no-any
96
+ function findAttribute(attributes, name) {
97
+ return attributes.find(
98
+ // tslint:disable-next-line:no-any
99
+ function (attr) { var _a; return (attr === null || attr === void 0 ? void 0 : attr.type) === 'JSXAttribute' && ((_a = attr.name) === null || _a === void 0 ? void 0 : _a.name) === name; });
100
+ }
101
+ /**
102
+ * The `useCallback(fn, [])`-wrapped arrow behind a `fetchData` attribute, or
103
+ * `undefined` when the value has any other shape.
104
+ *
105
+ * Requiring the memoized form is a safety condition, not a convenience.
106
+ * `DataProvider` refetches whenever the `fetchData` identity changes
107
+ * (`useEffect(..., [params, fetchData])`), so wrapping a fetcher that is
108
+ * re-created on every render with something that sets state would build a
109
+ * self-sustaining loop: fetch → setState → render → new fetcher identity →
110
+ * fetch. The array-mapper providers this plugin owns are always memoized; the
111
+ * few unmemoized `fetchData` values other plugins emit are left exactly as they
112
+ * are today.
113
+ */
114
+ function findMemoizedFetchDataArrow(attribute) {
115
+ var _a;
116
+ var value = attribute.value;
117
+ if (!value || value.type !== 'JSXExpressionContainer') {
118
+ return undefined;
119
+ }
120
+ var expression = value.expression;
121
+ if (expression.type !== 'CallExpression' ||
122
+ expression.callee.type !== 'Identifier' ||
123
+ expression.callee.name !== 'useCallback' ||
124
+ ((_a = expression.arguments[0]) === null || _a === void 0 ? void 0 : _a.type) !== 'ArrowFunctionExpression') {
125
+ return undefined;
126
+ }
127
+ return expression.arguments[0];
128
+ }
129
+ /**
130
+ * Turns `(params) => fetch(...).then(...)` into
131
+ *
132
+ * (params) => {
133
+ * ds_N_fetchesInFlight.current += 1
134
+ * setDs_N_isFetching(true)
135
+ * return fetch(...).then(...).finally(() => {
136
+ * ds_N_fetchesInFlight.current -= 1
137
+ * if (ds_N_fetchesInFlight.current <= 0) {
138
+ * ds_N_fetchesInFlight.current = 0
139
+ * setDs_N_isFetching(false)
140
+ * }
141
+ * })
142
+ * }
143
+ *
144
+ * `finally` (rather than a `then` pair) keeps the flag honest when the request
145
+ * rejects: the provider switches to its error status and the loading state must
146
+ * not stay on screen. The counter is clamped at 0 so a late settle from a
147
+ * provider instance that was remounted through its `key` can never drive it
148
+ * negative and wedge the flag on.
149
+ *
150
+ * `fetchExpression` is the `fetch(...).then(...).then(...)` chain the generator
151
+ * emitted, so `.finally` is always available on it — the caller only reaches
152
+ * here for a call-expression body.
153
+ */
154
+ function buildTrackedFetchBody(fetchExpression, vars) {
155
+ var inFlightCount = types.memberExpression(types.identifier(vars.inFlightRefVar), types.identifier('current'));
156
+ var settleHandler = types.arrowFunctionExpression([], types.blockStatement([
157
+ types.expressionStatement(types.assignmentExpression('-=', types.cloneNode(inFlightCount, true), types.numericLiteral(1))),
158
+ types.ifStatement(types.binaryExpression('<=', types.cloneNode(inFlightCount, true), types.numericLiteral(0)), types.blockStatement([
159
+ types.expressionStatement(types.assignmentExpression('=', types.cloneNode(inFlightCount, true), types.numericLiteral(0))),
160
+ types.expressionStatement(types.callExpression(types.identifier(vars.setIsFetchingVar), [
161
+ types.booleanLiteral(false),
162
+ ])),
163
+ ])),
164
+ ]));
165
+ return types.blockStatement([
166
+ types.expressionStatement(types.assignmentExpression('+=', types.cloneNode(inFlightCount, true), types.numericLiteral(1))),
167
+ types.expressionStatement(types.callExpression(types.identifier(vars.setIsFetchingVar), [types.booleanLiteral(true)])),
168
+ types.returnStatement(types.callExpression(types.memberExpression(fetchExpression, types.identifier('finally')), [
169
+ settleHandler,
170
+ ])),
171
+ ]);
172
+ }
173
+ /** `persistDataDuringLoading={!ds_N_isFetching}`, replacing any existing value. */
174
+ // tslint:disable-next-line:no-any
175
+ function setPersistDataDuringLoading(attributes, vars) {
176
+ var attribute = types.jsxAttribute(types.jsxIdentifier('persistDataDuringLoading'), types.jsxExpressionContainer(types.unaryExpression('!', types.identifier(vars.isFetchingVar), true)));
177
+ var existingIndex = attributes.findIndex(
178
+ // tslint:disable-next-line:no-any
179
+ function (attr) { var _a; return (attr === null || attr === void 0 ? void 0 : attr.type) === 'JSXAttribute' && ((_a = attr.name) === null || _a === void 0 ? void 0 : _a.name) === 'persistDataDuringLoading'; });
180
+ if (existingIndex === -1) {
181
+ attributes.push(attribute);
182
+ return;
183
+ }
184
+ attributes[existingIndex] = attribute;
185
+ }
186
+ //# sourceMappingURL=loading-state.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loading-state.js","sourceRoot":"","sources":["../../src/loading-state.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"pagination-plugin.d.ts","sourceRoot":"","sources":["../../src/pagination-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,sBAAsB,EAIvB,MAAM,4BAA4B,CAAA;AA0tBnC,eAAO,MAAM,qCAAqC,EAAE,sBAAsB,CAAC,EAAE,CAq2B5E,CAAA"}
1
+ {"version":3,"file":"pagination-plugin.d.ts","sourceRoot":"","sources":["../../src/pagination-plugin.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,sBAAsB,EAIvB,MAAM,4BAA4B,CAAA;AA+tBnC,eAAO,MAAM,qCAAqC,EAAE,sBAAsB,CAAC,EAAE,CAy3B5E,CAAA"}
@@ -69,6 +69,7 @@ var utils_1 = require("./utils");
69
69
  var data_source_fetchers_1 = require("./data-source-fetchers");
70
70
  var sort_utils_1 = require("./sort-utils");
71
71
  var filter_utils_1 = require("./filter-utils");
72
+ var loading_state_1 = require("./loading-state");
72
73
  // tslint:disable-next-line:no-any
73
74
  function parseSearchDefaultValue(raw) {
74
75
  if (!raw || typeof raw !== 'object') {
@@ -577,7 +578,7 @@ function pushSearchUrlSyncEffects(effectStatements, usage, vars) {
577
578
  // ==================== MAIN PLUGIN ====================
578
579
  var createNextArrayMapperPaginationPlugin = function () {
579
580
  var paginationPlugin = function (structure) { return __awaiter(void 0, void 0, void 0, function () {
580
- var uidl, chunks, dependencies, options, componentChunk, variableDeclaration, declarator, arrowFunction, blockStatement, registry, getStaticPropsChunk, isPage, stateDeclarations, effectStatements, needsUseRouter, hasRouterDecl, returnIndex, insertIndex, dataProviders, dataProvidersWithRepeaters, usageIndexByDataSourceId, dataProvidersWithoutRepeaters, searchInputs, searchEnabledUsages, paginationNodes, paginatedUsages;
581
+ var uidl, chunks, dependencies, options, componentChunk, variableDeclaration, declarator, arrowFunction, blockStatement, registry, getStaticPropsChunk, isPage, stateDeclarations, effectStatements, needsUseRouter, hasRouterDecl, returnIndex, insertIndex, dataProviders, dataProvidersWithRepeaters, usageIndexByDataSourceId, loadingStateDeclarations, dataProvidersWithoutRepeaters, searchInputs, searchEnabledUsages, paginationNodes, paginatedUsages;
581
582
  var _a;
582
583
  return __generator(this, function (_b) {
583
584
  uidl = structure.uidl, chunks = structure.chunks, dependencies = structure.dependencies, options = structure.options;
@@ -940,6 +941,7 @@ var createNextArrayMapperPaginationPlugin = function () {
940
941
  return hasRepeater;
941
942
  });
942
943
  usageIndexByDataSourceId = new Map();
944
+ loadingStateDeclarations = [];
943
945
  dataProvidersWithRepeaters.forEach(function (dp) {
944
946
  var _a, _b, _c;
945
947
  var nameAttr = dp.openingElement.attributes.find(function (attr) { return attr.type === 'JSXAttribute' && attr.name.name === 'name'; });
@@ -970,9 +972,21 @@ var createNextArrayMapperPaginationPlugin = function () {
970
972
  else if (usage.category === 'plain') {
971
973
  updateDataProviderForPlain(dp, fileName, usage);
972
974
  }
975
+ // Show the array mapper's loading state while a category/sort/search
976
+ // change refetches, instead of leaving the previous rows on screen with
977
+ // no feedback. Runs last so it sees the `fetchData` and
978
+ // `persistDataDuringLoading` attributes the updaters above just wrote.
979
+ var loadingVars = (0, loading_state_1.getLoadingStateVars)(usage.index);
980
+ if ((0, loading_state_1.applyLoadingStateToDataProvider)(dp, loadingVars)) {
981
+ loadingStateDeclarations.push.apply(loadingStateDeclarations, (0, loading_state_1.buildLoadingStateDeclarations)(loadingVars));
982
+ }
973
983
  // Create API route for all categories (including 'plain' for components)
974
984
  ensureAPIRouteExists(options.extractedResources, usage, options.dataSources, (_c = options.ecommerceSettings) === null || _c === void 0 ? void 0 : _c.categories);
975
985
  });
986
+ // Declare the loading-state hooks alongside the other data-source state, at
987
+ // the top of the component body. Unconditional and in a fixed order, so the
988
+ // hook order stays stable across renders.
989
+ loadingStateDeclarations.reverse().forEach(function (s) { return blockStatement.body.unshift(s); });
976
990
  dataProvidersWithoutRepeaters = dataProviders.filter(function (dp) {
977
991
  var hasRepeater = findArrayMapperRenderPropInDataProvider(dp) !== undefined;
978
992
  return !hasRepeater;