@teleporthq/teleport-plugin-common 0.43.14 → 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/resource-auth-scheme.ts +55 -0
- 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/ast-utils.d.ts.map +1 -1
- package/dist/cjs/utils/ast-utils.js +20 -5
- package/dist/cjs/utils/ast-utils.js.map +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/ast-utils.d.ts.map +1 -1
- package/dist/esm/utils/ast-utils.js +20 -5
- package/dist/esm/utils/ast-utils.js.map +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/ast-utils.ts +31 -12
- package/src/utils/url-search-param-sync.ts +286 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import * as types from '@babel/types';
|
|
2
|
+
// Shared builders for two-way `<state> ⇄ ?key=` URL search-param syncing.
|
|
3
|
+
//
|
|
4
|
+
// Used by BOTH:
|
|
5
|
+
// • `createNextUrlSearchParamsPlugin` (teleport-project-generator-next) for
|
|
6
|
+
// page/component state defs that declare `urlSearchParamBinding` — e.g.
|
|
7
|
+
// the products-list `selectedCategory` / `sortBy` dropdowns.
|
|
8
|
+
// • `createNextArrayMapperPaginationPlugin` (teleport-plugin-next-data-source)
|
|
9
|
+
// for the products-list search input, whose query lives in a
|
|
10
|
+
// pagination-managed state (`ds_N_searchQuery` / `ds_N_state.debouncedQuery`)
|
|
11
|
+
// rather than a UIDL state def, so it cannot ride the plugin above.
|
|
12
|
+
//
|
|
13
|
+
// Both emit the SAME loop-free read-back / write-back effect shapes, so the
|
|
14
|
+
// builders live here to keep the two call sites byte-identical and the
|
|
15
|
+
// loop-prevention reasoning in one place.
|
|
16
|
+
// Strict JS identifier (Latin only is fine: UIDL URL keys are limited to the
|
|
17
|
+
// subset Next.js supports as a query param key, which the GUI guarantees to
|
|
18
|
+
// be at minimum URL-safe; but the *generated* code must be valid JS, and
|
|
19
|
+
// `__nextQuery.foo-bar` parses as a subtraction). Anything off-pattern goes
|
|
20
|
+
// through bracket notation: `__nextQuery['foo-bar']`.
|
|
21
|
+
var VALID_JS_IDENTIFIER = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
|
|
22
|
+
// `object.<paramKey>` for identifier-safe keys, `object['param-key']` otherwise,
|
|
23
|
+
// so the emitted code is always valid JS regardless of the URL key shape.
|
|
24
|
+
var accessKeyOf = function (object, paramKey) {
|
|
25
|
+
if (VALID_JS_IDENTIFIER.test(paramKey)) {
|
|
26
|
+
return types.memberExpression(object, types.identifier(paramKey));
|
|
27
|
+
}
|
|
28
|
+
return types.memberExpression(object, types.stringLiteral(paramKey), true);
|
|
29
|
+
};
|
|
30
|
+
var routerQueryAccess = function (paramKey) {
|
|
31
|
+
return accessKeyOf(types.memberExpression(types.identifier('router'), types.identifier('query')), paramKey);
|
|
32
|
+
};
|
|
33
|
+
// Clones an expression so it can be slotted into multiple positions of the
|
|
34
|
+
// emitted AST — Babel does not support sharing the same node reference across
|
|
35
|
+
// the tree, and callers pass arbitrary value/dependency expressions.
|
|
36
|
+
var clone = function (expr) {
|
|
37
|
+
return types.cloneNode(expr, /* deep */ true);
|
|
38
|
+
};
|
|
39
|
+
// Builds the state→URL write-back useEffect. Emitted shape (for a value of
|
|
40
|
+
// `selectedCategory` bound to URL key `categoryFilter`):
|
|
41
|
+
//
|
|
42
|
+
// useEffect(() => {
|
|
43
|
+
// if (typeof window === 'undefined') return
|
|
44
|
+
// if (!router.isReady) return
|
|
45
|
+
// const __nextQuery = { ...router.query }
|
|
46
|
+
// if (selectedCategory === '' || selectedCategory == null) {
|
|
47
|
+
// delete __nextQuery.categoryFilter
|
|
48
|
+
// } else {
|
|
49
|
+
// __nextQuery.categoryFilter = String(selectedCategory)
|
|
50
|
+
// }
|
|
51
|
+
// if (__nextQuery.categoryFilter === router.query.categoryFilter) return
|
|
52
|
+
// router.replace(
|
|
53
|
+
// { pathname: router.pathname, query: __nextQuery },
|
|
54
|
+
// undefined,
|
|
55
|
+
// { shallow: true }
|
|
56
|
+
// )
|
|
57
|
+
// }, [selectedCategory, router.isReady])
|
|
58
|
+
//
|
|
59
|
+
// `valueExpr` is the expression read each render and pushed to the URL (a bare
|
|
60
|
+
// state identifier for dropdowns, `ds_N_state.debouncedQuery` for the search
|
|
61
|
+
// input). `depExpr` is the effect dependency that re-triggers the write-back
|
|
62
|
+
// when that value changes — usually the same expression as `valueExpr`.
|
|
63
|
+
//
|
|
64
|
+
// Notes:
|
|
65
|
+
// 1. `router.isReady` gate: on the very first SSG render `router.query` is
|
|
66
|
+
// empty, so without this gate any value initialized from
|
|
67
|
+
// `window.location.search` would immediately `router.replace` itself with
|
|
68
|
+
// the same value, racing with Next's own hydration. `router.isReady` is
|
|
69
|
+
// also in the deps array — once it flips to true, the effect re-runs with
|
|
70
|
+
// the now-hydrated `router.query` and decides correctly whether to replace.
|
|
71
|
+
// 2. The equality check after building `__nextQuery` short-circuits replaces
|
|
72
|
+
// that would not change the URL — important when the user re-enters the
|
|
73
|
+
// same value, when the value mounts equal to the URL, and when the
|
|
74
|
+
// read-back effect fires its setter with the value just pushed.
|
|
75
|
+
// 3. `shallow: true` keeps `getStaticProps` from re-running just because the
|
|
76
|
+
// URL bar changed; the data-source `useMemo` reacts to the state, not the
|
|
77
|
+
// URL.
|
|
78
|
+
// 4. Empty / null value deletes the key entirely so the URL never keeps a
|
|
79
|
+
// sticky `?key=` empty param.
|
|
80
|
+
export var buildUrlWriteBackEffect = function (paramKey, valueExpr, depExpr) {
|
|
81
|
+
var effectBody = types.blockStatement([
|
|
82
|
+
// if (typeof window === 'undefined') return
|
|
83
|
+
types.ifStatement(types.binaryExpression('===', types.unaryExpression('typeof', types.identifier('window')), types.stringLiteral('undefined')), types.returnStatement(null)),
|
|
84
|
+
// if (!router.isReady) return
|
|
85
|
+
types.ifStatement(types.unaryExpression('!', types.memberExpression(types.identifier('router'), types.identifier('isReady'))), types.returnStatement(null)),
|
|
86
|
+
// const __nextQuery = { ...router.query }
|
|
87
|
+
types.variableDeclaration('const', [
|
|
88
|
+
types.variableDeclarator(types.identifier('__nextQuery'), types.objectExpression([
|
|
89
|
+
types.spreadElement(types.memberExpression(types.identifier('router'), types.identifier('query'))),
|
|
90
|
+
])),
|
|
91
|
+
]),
|
|
92
|
+
// if (value === '' || value == null) { delete __nextQuery.key }
|
|
93
|
+
// else { __nextQuery.key = String(value) }
|
|
94
|
+
types.ifStatement(types.logicalExpression('||', types.binaryExpression('===', clone(valueExpr), types.stringLiteral('')), types.binaryExpression('==', clone(valueExpr), types.nullLiteral())), types.blockStatement([
|
|
95
|
+
types.expressionStatement(types.unaryExpression('delete', accessKeyOf(types.identifier('__nextQuery'), paramKey))),
|
|
96
|
+
]), types.blockStatement([
|
|
97
|
+
types.expressionStatement(types.assignmentExpression('=', accessKeyOf(types.identifier('__nextQuery'), paramKey), types.callExpression(types.identifier('String'), [clone(valueExpr)]))),
|
|
98
|
+
])),
|
|
99
|
+
// if (__nextQuery.key === router.query.key) return
|
|
100
|
+
types.ifStatement(types.binaryExpression('===', accessKeyOf(types.identifier('__nextQuery'), paramKey), routerQueryAccess(paramKey)), types.returnStatement(null)),
|
|
101
|
+
// router.replace({ pathname: router.pathname, query: __nextQuery }, undefined, { shallow: true })
|
|
102
|
+
types.expressionStatement(types.callExpression(types.memberExpression(types.identifier('router'), types.identifier('replace')), [
|
|
103
|
+
types.objectExpression([
|
|
104
|
+
types.objectProperty(types.identifier('pathname'), types.memberExpression(types.identifier('router'), types.identifier('pathname'))),
|
|
105
|
+
types.objectProperty(types.identifier('query'), types.identifier('__nextQuery')),
|
|
106
|
+
]),
|
|
107
|
+
types.identifier('undefined'),
|
|
108
|
+
types.objectExpression([
|
|
109
|
+
types.objectProperty(types.identifier('shallow'), types.booleanLiteral(true)),
|
|
110
|
+
]),
|
|
111
|
+
])),
|
|
112
|
+
]);
|
|
113
|
+
return types.expressionStatement(types.callExpression(types.identifier('useEffect'), [
|
|
114
|
+
types.arrowFunctionExpression([], effectBody),
|
|
115
|
+
types.arrayExpression([
|
|
116
|
+
clone(depExpr),
|
|
117
|
+
types.memberExpression(types.identifier('router'), types.identifier('isReady')),
|
|
118
|
+
]),
|
|
119
|
+
]));
|
|
120
|
+
};
|
|
121
|
+
// Builds the URL→state read-back useEffect. Emitted shape (for setter
|
|
122
|
+
// `setSelectedCategory` bound to URL key `categoryFilter`):
|
|
123
|
+
//
|
|
124
|
+
// useEffect(() => {
|
|
125
|
+
// if (!router.isReady) return
|
|
126
|
+
// const __urlValue = router.query.categoryFilter
|
|
127
|
+
// const __nextValue =
|
|
128
|
+
// typeof __urlValue === 'string' ? __urlValue : Array.isArray(__urlValue) ? (__urlValue[0] || '') : ''
|
|
129
|
+
// setSelectedCategory((prev) => (prev === __nextValue ? prev : __nextValue))
|
|
130
|
+
// }, [router.query.categoryFilter, router.isReady])
|
|
131
|
+
//
|
|
132
|
+
// Why pair this with the write-back effect:
|
|
133
|
+
// - On `<Link>` / `router.push` shallow navigations that land on the same path
|
|
134
|
+
// with a different query (or browser back/forward, which can change the query
|
|
135
|
+
// without remounting the page), `router.query` updates but the state hook does
|
|
136
|
+
// not. Without this effect, the control would be stuck on whatever the user
|
|
137
|
+
// last picked even though the URL has moved on.
|
|
138
|
+
// - The functional `setState((prev) => prev === next ? prev : next)` form
|
|
139
|
+
// guarantees React bails out when the URL value matches state, which prevents
|
|
140
|
+
// an infinite write-back ↔ read-back loop: when the user picks a new value,
|
|
141
|
+
// state→URL fires once, URL→state observes the matching value and returns
|
|
142
|
+
// `prev`, so React does not re-render or fire any deps.
|
|
143
|
+
// - `router.query[key]` can be either `string` (single value) or `string[]`
|
|
144
|
+
// (duplicate keys) per Next.js's typings; the helper normalizes both to a
|
|
145
|
+
// string before comparing, defaulting to `''` for the missing-key case so the
|
|
146
|
+
// comparison aligns with the write-back's `value === ''` empty-check.
|
|
147
|
+
export var buildUrlReadBackEffect = function (paramKey, setterName) {
|
|
148
|
+
var normalizeArg = types.conditionalExpression(
|
|
149
|
+
// typeof __urlValue === 'string' ? __urlValue
|
|
150
|
+
types.binaryExpression('===', types.unaryExpression('typeof', types.identifier('__urlValue')), types.stringLiteral('string')), types.identifier('__urlValue'), types.conditionalExpression(
|
|
151
|
+
// Array.isArray(__urlValue) ? (__urlValue[0] || '') : ''
|
|
152
|
+
types.callExpression(types.memberExpression(types.identifier('Array'), types.identifier('isArray')), [types.identifier('__urlValue')]), types.logicalExpression('||', types.memberExpression(types.identifier('__urlValue'), types.numericLiteral(0), true), types.stringLiteral('')), types.stringLiteral('')));
|
|
153
|
+
var effectBody = types.blockStatement([
|
|
154
|
+
// if (!router.isReady) return
|
|
155
|
+
types.ifStatement(types.unaryExpression('!', types.memberExpression(types.identifier('router'), types.identifier('isReady'))), types.returnStatement(null)),
|
|
156
|
+
// const __urlValue = router.query.<paramKey>
|
|
157
|
+
types.variableDeclaration('const', [
|
|
158
|
+
types.variableDeclarator(types.identifier('__urlValue'), routerQueryAccess(paramKey)),
|
|
159
|
+
]),
|
|
160
|
+
// const __nextValue = (normalized)
|
|
161
|
+
types.variableDeclaration('const', [
|
|
162
|
+
types.variableDeclarator(types.identifier('__nextValue'), normalizeArg),
|
|
163
|
+
]),
|
|
164
|
+
// setState((prev) => prev === __nextValue ? prev : __nextValue)
|
|
165
|
+
types.expressionStatement(types.callExpression(types.identifier(setterName), [
|
|
166
|
+
types.arrowFunctionExpression([types.identifier('prev')], types.conditionalExpression(types.binaryExpression('===', types.identifier('prev'), types.identifier('__nextValue')), types.identifier('prev'), types.identifier('__nextValue'))),
|
|
167
|
+
])),
|
|
168
|
+
]);
|
|
169
|
+
return types.expressionStatement(types.callExpression(types.identifier('useEffect'), [
|
|
170
|
+
types.arrowFunctionExpression([], effectBody),
|
|
171
|
+
types.arrayExpression([
|
|
172
|
+
routerQueryAccess(paramKey),
|
|
173
|
+
types.memberExpression(types.identifier('router'), types.identifier('isReady')),
|
|
174
|
+
]),
|
|
175
|
+
]));
|
|
176
|
+
};
|
|
177
|
+
//# sourceMappingURL=url-search-param-sync.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"url-search-param-sync.js","sourceRoot":"","sources":["../../../src/utils/url-search-param-sync.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@teleporthq/teleport-plugin-common",
|
|
3
|
-
"version": "0.43.
|
|
3
|
+
"version": "0.43.22",
|
|
4
4
|
"description": "Common building and modelating functions for ASTs and HASTs",
|
|
5
5
|
"author": "teleportHQ",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,10 +27,10 @@
|
|
|
27
27
|
"@babel/core": "^7.5.5",
|
|
28
28
|
"@babel/preset-react": "^7.22.5",
|
|
29
29
|
"@babel/types": "^7.5.5",
|
|
30
|
-
"@teleporthq/teleport-shared": "^0.43.
|
|
31
|
-
"@teleporthq/teleport-types": "^0.43.
|
|
30
|
+
"@teleporthq/teleport-shared": "^0.43.22",
|
|
31
|
+
"@teleporthq/teleport-types": "^0.43.22",
|
|
32
32
|
"jss": "^10.0.0",
|
|
33
33
|
"jss-preset-default": "^10.0.0"
|
|
34
34
|
},
|
|
35
|
-
"gitHead": "
|
|
35
|
+
"gitHead": "4bac1ec64c3c7e9f14a84a260331206d5797b773"
|
|
36
36
|
}
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import * as HASTBuilders from './builders/hast-builders'
|
|
|
5
5
|
import * as ASTUtils from './utils/ast-utils'
|
|
6
6
|
import * as StyleUtils from './utils/style-utils'
|
|
7
7
|
import * as HASTUtils from './utils/hast-utils'
|
|
8
|
+
import * as URLSearchParamSync from './utils/url-search-param-sync'
|
|
8
9
|
import ParsedASTNode from './utils/parsed-ast'
|
|
9
10
|
|
|
10
11
|
import { createBinaryExpression } from './node-handlers/node-to-jsx/utils'
|
|
@@ -16,6 +17,7 @@ export {
|
|
|
16
17
|
ASTUtils,
|
|
17
18
|
StyleUtils,
|
|
18
19
|
HASTUtils,
|
|
20
|
+
URLSearchParamSync,
|
|
19
21
|
ParsedASTNode,
|
|
20
22
|
createBinaryExpression,
|
|
21
23
|
}
|
package/src/utils/ast-utils.ts
CHANGED
|
@@ -1381,16 +1381,21 @@ const generateParamsAST = (
|
|
|
1381
1381
|
}
|
|
1382
1382
|
|
|
1383
1383
|
const generateRESTHeadersAST = (headers: UIDLResourceItem['headers']): types.ObjectProperty[] => {
|
|
1384
|
-
return
|
|
1385
|
-
.
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1384
|
+
return (
|
|
1385
|
+
Object.keys(headers)
|
|
1386
|
+
// `authToken` is rendered as the `Authorization` header; `authScheme` is a
|
|
1387
|
+
// generator-only directive for that header's prefix — neither is emitted as
|
|
1388
|
+
// its own literal HTTP header.
|
|
1389
|
+
.filter((header) => header !== 'authToken' && header !== 'authScheme')
|
|
1390
|
+
.map((header) => {
|
|
1391
|
+
const headerResolved = resolveResourceValue(headers[header])
|
|
1392
|
+
const value =
|
|
1393
|
+
headers[header].type === 'static'
|
|
1394
|
+
? types.stringLiteral(String(headerResolved))
|
|
1395
|
+
: types.identifier(String(headerResolved))
|
|
1396
|
+
return types.objectProperty(types.stringLiteral(header), value)
|
|
1397
|
+
})
|
|
1398
|
+
)
|
|
1394
1399
|
}
|
|
1395
1400
|
|
|
1396
1401
|
export const generateMemberExpressionASTFromBase = (
|
|
@@ -1529,14 +1534,28 @@ const computeAuthorizationHeaderAST = (headers: UIDLResourceItem['headers']) =>
|
|
|
1529
1534
|
|
|
1530
1535
|
const authTokenType = headers.authToken?.type
|
|
1531
1536
|
|
|
1537
|
+
// The auth scheme prefix defaults to `Bearer ` (back-compatible with every CMS
|
|
1538
|
+
// and data-source that stores a bare bearer token). A resource may override it
|
|
1539
|
+
// via a `headers.authScheme` static value — e.g. WordPress Basic / Application
|
|
1540
|
+
// Password auth sets `Basic ` so the rendered header is `Basic <base64>`
|
|
1541
|
+
// instead of the wrong `Bearer <base64>`. `authScheme` is consumed here and
|
|
1542
|
+
// filtered out of the emitted REST headers (see generateRESTHeadersAST).
|
|
1543
|
+
const authSchemeValue = headers.authScheme
|
|
1544
|
+
const schemePrefix =
|
|
1545
|
+
authSchemeValue &&
|
|
1546
|
+
authSchemeValue.type === 'static' &&
|
|
1547
|
+
typeof authSchemeValue.content === 'string'
|
|
1548
|
+
? authSchemeValue.content
|
|
1549
|
+
: 'Bearer '
|
|
1550
|
+
|
|
1532
1551
|
return types.objectProperty(
|
|
1533
1552
|
types.identifier('Authorization'),
|
|
1534
1553
|
types.templateLiteral(
|
|
1535
1554
|
[
|
|
1536
1555
|
types.templateElement(
|
|
1537
1556
|
{
|
|
1538
|
-
cooked: authTokenType === 'static' ?
|
|
1539
|
-
raw: authTokenType === 'static' ?
|
|
1557
|
+
cooked: authTokenType === 'static' ? `${schemePrefix}${authToken}` : schemePrefix,
|
|
1558
|
+
raw: authTokenType === 'static' ? `${schemePrefix}${authToken}` : schemePrefix,
|
|
1540
1559
|
},
|
|
1541
1560
|
false
|
|
1542
1561
|
),
|
|
@@ -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
|
+
}
|