@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.
- package/__tests__/node-handlers/node-to-jsx/index.ts +69 -2
- 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/node-handlers/node-to-jsx/index.d.ts.map +1 -1
- package/dist/cjs/node-handlers/node-to-jsx/index.js +57 -30
- package/dist/cjs/node-handlers/node-to-jsx/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/node-handlers/node-to-jsx/index.d.ts.map +1 -1
- package/dist/esm/node-handlers/node-to-jsx/index.js +57 -30
- package/dist/esm/node-handlers/node-to-jsx/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/node-handlers/node-to-jsx/index.ts +66 -43
- 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.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.
|
|
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": "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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
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 = '
|
|
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('
|
|
2081
|
+
elementTag.closingElement = types.jsxClosingElement(types.jsxIdentifier('Markdown'))
|
|
2049
2082
|
} else {
|
|
2050
|
-
;(elementTag.closingElement.name as types.JSXIdentifier).name = '
|
|
2083
|
+
;(elementTag.closingElement.name as types.JSXIdentifier).name = 'Markdown'
|
|
2051
2084
|
}
|
|
2052
|
-
elementTag.children = [types.jsxExpressionContainer(
|
|
2053
|
-
|
|
2054
|
-
cleanupMarkdownMappingDependencies(params)
|
|
2085
|
+
elementTag.children = [types.jsxExpressionContainer(childExpr)]
|
|
2055
2086
|
|
|
2056
|
-
params.dependencies.
|
|
2087
|
+
params.dependencies.Markdown = {
|
|
2057
2088
|
type: 'package',
|
|
2058
|
-
path: '
|
|
2059
|
-
version: '
|
|
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
|
+
}
|