@teleporthq/teleport-plugin-common 0.43.21 → 0.43.27

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 (30) hide show
  1. package/__tests__/node-handlers/node-to-jsx/index.ts +69 -2
  2. package/__tests__/utils/url-search-param-sync.ts +92 -0
  3. package/dist/cjs/index.d.ts +2 -1
  4. package/dist/cjs/index.d.ts.map +1 -1
  5. package/dist/cjs/index.js +3 -1
  6. package/dist/cjs/index.js.map +1 -1
  7. package/dist/cjs/node-handlers/node-to-jsx/index.d.ts.map +1 -1
  8. package/dist/cjs/node-handlers/node-to-jsx/index.js +57 -30
  9. package/dist/cjs/node-handlers/node-to-jsx/index.js.map +1 -1
  10. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  11. package/dist/cjs/utils/url-search-param-sync.d.ts +4 -0
  12. package/dist/cjs/utils/url-search-param-sync.d.ts.map +1 -0
  13. package/dist/cjs/utils/url-search-param-sync.js +205 -0
  14. package/dist/cjs/utils/url-search-param-sync.js.map +1 -0
  15. package/dist/esm/index.d.ts +2 -1
  16. package/dist/esm/index.d.ts.map +1 -1
  17. package/dist/esm/index.js +2 -1
  18. package/dist/esm/index.js.map +1 -1
  19. package/dist/esm/node-handlers/node-to-jsx/index.d.ts.map +1 -1
  20. package/dist/esm/node-handlers/node-to-jsx/index.js +57 -30
  21. package/dist/esm/node-handlers/node-to-jsx/index.js.map +1 -1
  22. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  23. package/dist/esm/utils/url-search-param-sync.d.ts +4 -0
  24. package/dist/esm/utils/url-search-param-sync.d.ts.map +1 -0
  25. package/dist/esm/utils/url-search-param-sync.js +177 -0
  26. package/dist/esm/utils/url-search-param-sync.js.map +1 -0
  27. package/package.json +4 -4
  28. package/src/index.ts +2 -0
  29. package/src/node-handlers/node-to-jsx/index.ts +66 -43
  30. 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.21",
3
+ "version": "0.43.27",
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.14",
31
- "@teleporthq/teleport-types": "^0.43.14",
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": "41c68c00e06f6f811056979b4f0473c277a219cb"
35
+ "gitHead": "96a6f138f6e447af1c07b1746b67719c16c9dc7b"
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
  }
@@ -705,7 +705,35 @@ const generateElementNode: NodeToJSX<UIDLElementNode, types.JSXElement> = (
705
705
  )
706
706
  }
707
707
 
708
- if (!selfClosing && children) {
708
+ // A markdown-node can carry its rich-text content as a dynamic/expr child
709
+ // (e.g. CMS-bound rich text) rather than as a `raw` attribute. The raw-attribute
710
+ // path is handled in addAttributesToJSXTag; here we handle the children path so
711
+ // the content is injected as HTML (dangerouslySetInnerHTML) instead of being
712
+ // printed as an escaped raw value by the generic expression handler.
713
+ const isMarkdownNode =
714
+ node.content.semanticType === 'markdown-node' || originalElementName === 'markdown-node'
715
+ const hasRawAttr = attrs
716
+ ? Object.keys(attrs).some((attrKey) => attrs[attrKey]?.type === 'raw')
717
+ : false
718
+
719
+ let markdownChildrenHandled = false
720
+ if (isMarkdownNode && !selfClosing && !hasRawAttr && children && children.length > 0) {
721
+ const markdownChild = children.find(
722
+ (child) => child.type === 'expr' || child.type === 'dynamic'
723
+ )
724
+ let markdownExpr: types.Expression | undefined
725
+ if (markdownChild?.type === 'expr') {
726
+ markdownExpr = ASTUtils.getExpressionFromUIDLExpressionNode(markdownChild)
727
+ } else if (markdownChild?.type === 'dynamic') {
728
+ markdownExpr = resolveDynamicReferenceExpression(markdownChild, childParams, options)
729
+ }
730
+ if (markdownExpr) {
731
+ injectMarkdownExpression(elementTag, markdownExpr, '', childParams)
732
+ markdownChildrenHandled = true
733
+ }
734
+ }
735
+
736
+ if (!markdownChildrenHandled && !selfClosing && children) {
709
737
  // Reorder children to ensure search nodes appear before DataProvider nodes
710
738
  const reorderedChildren = reorderChildrenForSearch(children)
711
739
 
@@ -2005,25 +2033,7 @@ const applyStaticMarkdownInjection = (
2005
2033
  rawValue: UIDLRawValue,
2006
2034
  params: JSXGenerationParams
2007
2035
  ) => {
2008
- const content = rawValue.content || ''
2009
-
2010
- ;(elementTag.openingElement.name as types.JSXIdentifier).name = 'div'
2011
- elementTag.openingElement.selfClosing = true
2012
- elementTag.closingElement = null
2013
- elementTag.children = []
2014
-
2015
- elementTag.openingElement.attributes.push(
2016
- types.jsxAttribute(
2017
- types.jsxIdentifier('dangerouslySetInnerHTML'),
2018
- types.jsxExpressionContainer(
2019
- types.objectExpression([
2020
- types.objectProperty(types.identifier('__html'), types.stringLiteral(content)),
2021
- ])
2022
- )
2023
- )
2024
- )
2025
-
2026
- cleanupMarkdownMappingDependencies(params)
2036
+ injectMarkdownExpression(elementTag, types.stringLiteral(rawValue.content || ''), '', params)
2027
2037
  }
2028
2038
 
2029
2039
  const applyDynamicMarkdownInjection = (
@@ -2034,40 +2044,53 @@ const applyDynamicMarkdownInjection = (
2034
2044
  ) => {
2035
2045
  const dynamicExpr = resolveDynamicReferenceExpression(rawValue.dynamic, params, options)
2036
2046
  const fallbackContent = rawValue.fallback || rawValue.content
2047
+ injectMarkdownExpression(elementTag, dynamicExpr, fallbackContent, params)
2048
+ }
2037
2049
 
2038
- const reactMarkdownTag = createJSXTag('ReactMarkdown')
2039
- addChildJSXTag(reactMarkdownTag, types.jsxExpressionContainer(types.cloneNode(dynamicExpr)))
2040
-
2041
- const fallbackTag = ASTBuilders.createDOMInjectionNode(fallbackContent)
2042
-
2043
- const conditionalExpr = types.conditionalExpression(dynamicExpr, reactMarkdownTag, fallbackTag)
2050
+ /**
2051
+ * Renders a markdown-node's rich-text content through the `markdown-to-jsx` <Markdown>
2052
+ * component (the React generator's mapping for markdown-node).
2053
+ *
2054
+ * markdown-to-jsx parses BOTH markdown syntax AND embedded HTML, so it renders correctly
2055
+ * whether the CMS field returns markdown (`# Heading`) or HTML markup (`<p>…</p>`). A raw
2056
+ * HTML approach (dangerouslySetInnerHTML) would ignore markdown, and a markdown-only
2057
+ * renderer (react-markdown) escapes HTML — markdown-to-jsx covers both at once.
2058
+ *
2059
+ * Shared by every way a markdown-node can carry its content:
2060
+ * - a static `raw` attribute (see applyStaticMarkdownInjection)
2061
+ * - a `raw` attribute holding a dynamic reference (see applyDynamicMarkdownInjection)
2062
+ * - a dynamic/expr child, e.g. CMS-bound rich text (see generateElementNode)
2063
+ *
2064
+ * Produces: <Markdown>{expr || fallback}</Markdown>
2065
+ */
2066
+ const injectMarkdownExpression = (
2067
+ elementTag: types.JSXElement,
2068
+ expression: types.Expression,
2069
+ fallbackContent: string,
2070
+ params: JSXGenerationParams
2071
+ ) => {
2072
+ // markdown-to-jsx requires a string child; guard dynamic expressions with a fallback so
2073
+ // a nullish CMS value renders as empty rather than throwing. Static literals need no guard.
2074
+ const childExpr = types.isStringLiteral(expression)
2075
+ ? expression
2076
+ : types.logicalExpression('||', expression, types.stringLiteral(fallbackContent || ''))
2044
2077
 
2045
- ;(elementTag.openingElement.name as types.JSXIdentifier).name = 'div'
2078
+ ;(elementTag.openingElement.name as types.JSXIdentifier).name = 'Markdown'
2046
2079
  elementTag.openingElement.selfClosing = false
2047
2080
  if (!elementTag.closingElement) {
2048
- elementTag.closingElement = types.jsxClosingElement(types.jsxIdentifier('div'))
2081
+ elementTag.closingElement = types.jsxClosingElement(types.jsxIdentifier('Markdown'))
2049
2082
  } else {
2050
- ;(elementTag.closingElement.name as types.JSXIdentifier).name = 'div'
2083
+ ;(elementTag.closingElement.name as types.JSXIdentifier).name = 'Markdown'
2051
2084
  }
2052
- elementTag.children = [types.jsxExpressionContainer(conditionalExpr)]
2053
-
2054
- cleanupMarkdownMappingDependencies(params)
2085
+ elementTag.children = [types.jsxExpressionContainer(childExpr)]
2055
2086
 
2056
- params.dependencies.ReactMarkdown = {
2087
+ params.dependencies.Markdown = {
2057
2088
  type: 'package',
2058
- path: 'react-markdown',
2059
- version: '^8.0.7',
2089
+ path: 'markdown-to-jsx',
2090
+ version: '7.7.12',
2060
2091
  }
2061
2092
  }
2062
2093
 
2063
- const cleanupMarkdownMappingDependencies = (params: JSXGenerationParams) => {
2064
- Object.keys(params.dependencies).forEach((key) => {
2065
- if (params.dependencies[key]?.path?.includes('markdown-to-jsx')) {
2066
- delete params.dependencies[key]
2067
- }
2068
- })
2069
- }
2070
-
2071
2094
  /**
2072
2095
  * Handles raw attributes on a rich-text-editor-node element.
2073
2096
  *
@@ -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
+ }