@teleporthq/teleport-plugin-common 0.43.21 → 0.43.22
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.
- package/__tests__/utils/url-search-param-sync.ts +92 -0
- package/dist/cjs/index.d.ts +2 -1
- package/dist/cjs/index.d.ts.map +1 -1
- package/dist/cjs/index.js +3 -1
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/tsconfig.tsbuildinfo +1 -1
- package/dist/cjs/utils/url-search-param-sync.d.ts +4 -0
- package/dist/cjs/utils/url-search-param-sync.d.ts.map +1 -0
- package/dist/cjs/utils/url-search-param-sync.js +205 -0
- package/dist/cjs/utils/url-search-param-sync.js.map +1 -0
- package/dist/esm/index.d.ts +2 -1
- package/dist/esm/index.d.ts.map +1 -1
- package/dist/esm/index.js +2 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/tsconfig.tsbuildinfo +1 -1
- package/dist/esm/utils/url-search-param-sync.d.ts +4 -0
- package/dist/esm/utils/url-search-param-sync.d.ts.map +1 -0
- package/dist/esm/utils/url-search-param-sync.js +177 -0
- package/dist/esm/utils/url-search-param-sync.js.map +1 -0
- package/package.json +4 -4
- package/src/index.ts +2 -0
- package/src/utils/url-search-param-sync.ts +286 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import * as types from '@babel/types'
|
|
2
|
+
|
|
3
|
+
// Shared builders for two-way `<state> ⇄ ?key=` URL search-param syncing.
|
|
4
|
+
//
|
|
5
|
+
// Used by BOTH:
|
|
6
|
+
// • `createNextUrlSearchParamsPlugin` (teleport-project-generator-next) for
|
|
7
|
+
// page/component state defs that declare `urlSearchParamBinding` — e.g.
|
|
8
|
+
// the products-list `selectedCategory` / `sortBy` dropdowns.
|
|
9
|
+
// • `createNextArrayMapperPaginationPlugin` (teleport-plugin-next-data-source)
|
|
10
|
+
// for the products-list search input, whose query lives in a
|
|
11
|
+
// pagination-managed state (`ds_N_searchQuery` / `ds_N_state.debouncedQuery`)
|
|
12
|
+
// rather than a UIDL state def, so it cannot ride the plugin above.
|
|
13
|
+
//
|
|
14
|
+
// Both emit the SAME loop-free read-back / write-back effect shapes, so the
|
|
15
|
+
// builders live here to keep the two call sites byte-identical and the
|
|
16
|
+
// loop-prevention reasoning in one place.
|
|
17
|
+
|
|
18
|
+
// Strict JS identifier (Latin only is fine: UIDL URL keys are limited to the
|
|
19
|
+
// subset Next.js supports as a query param key, which the GUI guarantees to
|
|
20
|
+
// be at minimum URL-safe; but the *generated* code must be valid JS, and
|
|
21
|
+
// `__nextQuery.foo-bar` parses as a subtraction). Anything off-pattern goes
|
|
22
|
+
// through bracket notation: `__nextQuery['foo-bar']`.
|
|
23
|
+
const VALID_JS_IDENTIFIER = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/
|
|
24
|
+
|
|
25
|
+
// `object.<paramKey>` for identifier-safe keys, `object['param-key']` otherwise,
|
|
26
|
+
// so the emitted code is always valid JS regardless of the URL key shape.
|
|
27
|
+
const accessKeyOf = (object: types.Expression, paramKey: string): types.MemberExpression => {
|
|
28
|
+
if (VALID_JS_IDENTIFIER.test(paramKey)) {
|
|
29
|
+
return types.memberExpression(object, types.identifier(paramKey))
|
|
30
|
+
}
|
|
31
|
+
return types.memberExpression(object, types.stringLiteral(paramKey), true)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const routerQueryAccess = (paramKey: string): types.MemberExpression =>
|
|
35
|
+
accessKeyOf(
|
|
36
|
+
types.memberExpression(types.identifier('router'), types.identifier('query')),
|
|
37
|
+
paramKey
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
// Clones an expression so it can be slotted into multiple positions of the
|
|
41
|
+
// emitted AST — Babel does not support sharing the same node reference across
|
|
42
|
+
// the tree, and callers pass arbitrary value/dependency expressions.
|
|
43
|
+
const clone = (expr: types.Expression): types.Expression =>
|
|
44
|
+
types.cloneNode(expr, /* deep */ true) as types.Expression
|
|
45
|
+
|
|
46
|
+
// Builds the state→URL write-back useEffect. Emitted shape (for a value of
|
|
47
|
+
// `selectedCategory` bound to URL key `categoryFilter`):
|
|
48
|
+
//
|
|
49
|
+
// useEffect(() => {
|
|
50
|
+
// if (typeof window === 'undefined') return
|
|
51
|
+
// if (!router.isReady) return
|
|
52
|
+
// const __nextQuery = { ...router.query }
|
|
53
|
+
// if (selectedCategory === '' || selectedCategory == null) {
|
|
54
|
+
// delete __nextQuery.categoryFilter
|
|
55
|
+
// } else {
|
|
56
|
+
// __nextQuery.categoryFilter = String(selectedCategory)
|
|
57
|
+
// }
|
|
58
|
+
// if (__nextQuery.categoryFilter === router.query.categoryFilter) return
|
|
59
|
+
// router.replace(
|
|
60
|
+
// { pathname: router.pathname, query: __nextQuery },
|
|
61
|
+
// undefined,
|
|
62
|
+
// { shallow: true }
|
|
63
|
+
// )
|
|
64
|
+
// }, [selectedCategory, router.isReady])
|
|
65
|
+
//
|
|
66
|
+
// `valueExpr` is the expression read each render and pushed to the URL (a bare
|
|
67
|
+
// state identifier for dropdowns, `ds_N_state.debouncedQuery` for the search
|
|
68
|
+
// input). `depExpr` is the effect dependency that re-triggers the write-back
|
|
69
|
+
// when that value changes — usually the same expression as `valueExpr`.
|
|
70
|
+
//
|
|
71
|
+
// Notes:
|
|
72
|
+
// 1. `router.isReady` gate: on the very first SSG render `router.query` is
|
|
73
|
+
// empty, so without this gate any value initialized from
|
|
74
|
+
// `window.location.search` would immediately `router.replace` itself with
|
|
75
|
+
// the same value, racing with Next's own hydration. `router.isReady` is
|
|
76
|
+
// also in the deps array — once it flips to true, the effect re-runs with
|
|
77
|
+
// the now-hydrated `router.query` and decides correctly whether to replace.
|
|
78
|
+
// 2. The equality check after building `__nextQuery` short-circuits replaces
|
|
79
|
+
// that would not change the URL — important when the user re-enters the
|
|
80
|
+
// same value, when the value mounts equal to the URL, and when the
|
|
81
|
+
// read-back effect fires its setter with the value just pushed.
|
|
82
|
+
// 3. `shallow: true` keeps `getStaticProps` from re-running just because the
|
|
83
|
+
// URL bar changed; the data-source `useMemo` reacts to the state, not the
|
|
84
|
+
// URL.
|
|
85
|
+
// 4. Empty / null value deletes the key entirely so the URL never keeps a
|
|
86
|
+
// sticky `?key=` empty param.
|
|
87
|
+
export const buildUrlWriteBackEffect = (
|
|
88
|
+
paramKey: string,
|
|
89
|
+
valueExpr: types.Expression,
|
|
90
|
+
depExpr: types.Expression
|
|
91
|
+
): types.ExpressionStatement => {
|
|
92
|
+
const effectBody = types.blockStatement([
|
|
93
|
+
// if (typeof window === 'undefined') return
|
|
94
|
+
types.ifStatement(
|
|
95
|
+
types.binaryExpression(
|
|
96
|
+
'===',
|
|
97
|
+
types.unaryExpression('typeof', types.identifier('window')),
|
|
98
|
+
types.stringLiteral('undefined')
|
|
99
|
+
),
|
|
100
|
+
types.returnStatement(null)
|
|
101
|
+
),
|
|
102
|
+
// if (!router.isReady) return
|
|
103
|
+
types.ifStatement(
|
|
104
|
+
types.unaryExpression(
|
|
105
|
+
'!',
|
|
106
|
+
types.memberExpression(types.identifier('router'), types.identifier('isReady'))
|
|
107
|
+
),
|
|
108
|
+
types.returnStatement(null)
|
|
109
|
+
),
|
|
110
|
+
// const __nextQuery = { ...router.query }
|
|
111
|
+
types.variableDeclaration('const', [
|
|
112
|
+
types.variableDeclarator(
|
|
113
|
+
types.identifier('__nextQuery'),
|
|
114
|
+
types.objectExpression([
|
|
115
|
+
types.spreadElement(
|
|
116
|
+
types.memberExpression(types.identifier('router'), types.identifier('query'))
|
|
117
|
+
),
|
|
118
|
+
])
|
|
119
|
+
),
|
|
120
|
+
]),
|
|
121
|
+
// if (value === '' || value == null) { delete __nextQuery.key }
|
|
122
|
+
// else { __nextQuery.key = String(value) }
|
|
123
|
+
types.ifStatement(
|
|
124
|
+
types.logicalExpression(
|
|
125
|
+
'||',
|
|
126
|
+
types.binaryExpression('===', clone(valueExpr), types.stringLiteral('')),
|
|
127
|
+
types.binaryExpression('==', clone(valueExpr), types.nullLiteral())
|
|
128
|
+
),
|
|
129
|
+
types.blockStatement([
|
|
130
|
+
types.expressionStatement(
|
|
131
|
+
types.unaryExpression(
|
|
132
|
+
'delete',
|
|
133
|
+
accessKeyOf(types.identifier('__nextQuery'), paramKey)
|
|
134
|
+
) as unknown as types.Expression
|
|
135
|
+
),
|
|
136
|
+
]),
|
|
137
|
+
types.blockStatement([
|
|
138
|
+
types.expressionStatement(
|
|
139
|
+
types.assignmentExpression(
|
|
140
|
+
'=',
|
|
141
|
+
accessKeyOf(types.identifier('__nextQuery'), paramKey),
|
|
142
|
+
types.callExpression(types.identifier('String'), [clone(valueExpr)])
|
|
143
|
+
)
|
|
144
|
+
),
|
|
145
|
+
])
|
|
146
|
+
),
|
|
147
|
+
// if (__nextQuery.key === router.query.key) return
|
|
148
|
+
types.ifStatement(
|
|
149
|
+
types.binaryExpression(
|
|
150
|
+
'===',
|
|
151
|
+
accessKeyOf(types.identifier('__nextQuery'), paramKey),
|
|
152
|
+
routerQueryAccess(paramKey)
|
|
153
|
+
),
|
|
154
|
+
types.returnStatement(null)
|
|
155
|
+
),
|
|
156
|
+
// router.replace({ pathname: router.pathname, query: __nextQuery }, undefined, { shallow: true })
|
|
157
|
+
types.expressionStatement(
|
|
158
|
+
types.callExpression(
|
|
159
|
+
types.memberExpression(types.identifier('router'), types.identifier('replace')),
|
|
160
|
+
[
|
|
161
|
+
types.objectExpression([
|
|
162
|
+
types.objectProperty(
|
|
163
|
+
types.identifier('pathname'),
|
|
164
|
+
types.memberExpression(types.identifier('router'), types.identifier('pathname'))
|
|
165
|
+
),
|
|
166
|
+
types.objectProperty(types.identifier('query'), types.identifier('__nextQuery')),
|
|
167
|
+
]),
|
|
168
|
+
types.identifier('undefined'),
|
|
169
|
+
types.objectExpression([
|
|
170
|
+
types.objectProperty(types.identifier('shallow'), types.booleanLiteral(true)),
|
|
171
|
+
]),
|
|
172
|
+
]
|
|
173
|
+
)
|
|
174
|
+
),
|
|
175
|
+
])
|
|
176
|
+
|
|
177
|
+
return types.expressionStatement(
|
|
178
|
+
types.callExpression(types.identifier('useEffect'), [
|
|
179
|
+
types.arrowFunctionExpression([], effectBody),
|
|
180
|
+
types.arrayExpression([
|
|
181
|
+
clone(depExpr),
|
|
182
|
+
types.memberExpression(types.identifier('router'), types.identifier('isReady')),
|
|
183
|
+
]),
|
|
184
|
+
])
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Builds the URL→state read-back useEffect. Emitted shape (for setter
|
|
189
|
+
// `setSelectedCategory` bound to URL key `categoryFilter`):
|
|
190
|
+
//
|
|
191
|
+
// useEffect(() => {
|
|
192
|
+
// if (!router.isReady) return
|
|
193
|
+
// const __urlValue = router.query.categoryFilter
|
|
194
|
+
// const __nextValue =
|
|
195
|
+
// typeof __urlValue === 'string' ? __urlValue : Array.isArray(__urlValue) ? (__urlValue[0] || '') : ''
|
|
196
|
+
// setSelectedCategory((prev) => (prev === __nextValue ? prev : __nextValue))
|
|
197
|
+
// }, [router.query.categoryFilter, router.isReady])
|
|
198
|
+
//
|
|
199
|
+
// Why pair this with the write-back effect:
|
|
200
|
+
// - On `<Link>` / `router.push` shallow navigations that land on the same path
|
|
201
|
+
// with a different query (or browser back/forward, which can change the query
|
|
202
|
+
// without remounting the page), `router.query` updates but the state hook does
|
|
203
|
+
// not. Without this effect, the control would be stuck on whatever the user
|
|
204
|
+
// last picked even though the URL has moved on.
|
|
205
|
+
// - The functional `setState((prev) => prev === next ? prev : next)` form
|
|
206
|
+
// guarantees React bails out when the URL value matches state, which prevents
|
|
207
|
+
// an infinite write-back ↔ read-back loop: when the user picks a new value,
|
|
208
|
+
// state→URL fires once, URL→state observes the matching value and returns
|
|
209
|
+
// `prev`, so React does not re-render or fire any deps.
|
|
210
|
+
// - `router.query[key]` can be either `string` (single value) or `string[]`
|
|
211
|
+
// (duplicate keys) per Next.js's typings; the helper normalizes both to a
|
|
212
|
+
// string before comparing, defaulting to `''` for the missing-key case so the
|
|
213
|
+
// comparison aligns with the write-back's `value === ''` empty-check.
|
|
214
|
+
export const buildUrlReadBackEffect = (
|
|
215
|
+
paramKey: string,
|
|
216
|
+
setterName: string
|
|
217
|
+
): types.ExpressionStatement => {
|
|
218
|
+
const normalizeArg = types.conditionalExpression(
|
|
219
|
+
// typeof __urlValue === 'string' ? __urlValue
|
|
220
|
+
types.binaryExpression(
|
|
221
|
+
'===',
|
|
222
|
+
types.unaryExpression('typeof', types.identifier('__urlValue')),
|
|
223
|
+
types.stringLiteral('string')
|
|
224
|
+
),
|
|
225
|
+
types.identifier('__urlValue'),
|
|
226
|
+
types.conditionalExpression(
|
|
227
|
+
// Array.isArray(__urlValue) ? (__urlValue[0] || '') : ''
|
|
228
|
+
types.callExpression(
|
|
229
|
+
types.memberExpression(types.identifier('Array'), types.identifier('isArray')),
|
|
230
|
+
[types.identifier('__urlValue')]
|
|
231
|
+
),
|
|
232
|
+
types.logicalExpression(
|
|
233
|
+
'||',
|
|
234
|
+
types.memberExpression(types.identifier('__urlValue'), types.numericLiteral(0), true),
|
|
235
|
+
types.stringLiteral('')
|
|
236
|
+
),
|
|
237
|
+
types.stringLiteral('')
|
|
238
|
+
)
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
const effectBody = types.blockStatement([
|
|
242
|
+
// if (!router.isReady) return
|
|
243
|
+
types.ifStatement(
|
|
244
|
+
types.unaryExpression(
|
|
245
|
+
'!',
|
|
246
|
+
types.memberExpression(types.identifier('router'), types.identifier('isReady'))
|
|
247
|
+
),
|
|
248
|
+
types.returnStatement(null)
|
|
249
|
+
),
|
|
250
|
+
// const __urlValue = router.query.<paramKey>
|
|
251
|
+
types.variableDeclaration('const', [
|
|
252
|
+
types.variableDeclarator(types.identifier('__urlValue'), routerQueryAccess(paramKey)),
|
|
253
|
+
]),
|
|
254
|
+
// const __nextValue = (normalized)
|
|
255
|
+
types.variableDeclaration('const', [
|
|
256
|
+
types.variableDeclarator(types.identifier('__nextValue'), normalizeArg),
|
|
257
|
+
]),
|
|
258
|
+
// setState((prev) => prev === __nextValue ? prev : __nextValue)
|
|
259
|
+
types.expressionStatement(
|
|
260
|
+
types.callExpression(types.identifier(setterName), [
|
|
261
|
+
types.arrowFunctionExpression(
|
|
262
|
+
[types.identifier('prev')],
|
|
263
|
+
types.conditionalExpression(
|
|
264
|
+
types.binaryExpression(
|
|
265
|
+
'===',
|
|
266
|
+
types.identifier('prev'),
|
|
267
|
+
types.identifier('__nextValue')
|
|
268
|
+
),
|
|
269
|
+
types.identifier('prev'),
|
|
270
|
+
types.identifier('__nextValue')
|
|
271
|
+
)
|
|
272
|
+
),
|
|
273
|
+
])
|
|
274
|
+
),
|
|
275
|
+
])
|
|
276
|
+
|
|
277
|
+
return types.expressionStatement(
|
|
278
|
+
types.callExpression(types.identifier('useEffect'), [
|
|
279
|
+
types.arrowFunctionExpression([], effectBody),
|
|
280
|
+
types.arrayExpression([
|
|
281
|
+
routerQueryAccess(paramKey),
|
|
282
|
+
types.memberExpression(types.identifier('router'), types.identifier('isReady')),
|
|
283
|
+
]),
|
|
284
|
+
])
|
|
285
|
+
)
|
|
286
|
+
}
|