@teleporthq/teleport-plugin-common 0.43.57 → 0.43.60

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 (54) hide show
  1. package/__tests__/builders/style-builders.ts +23 -0
  2. package/__tests__/node-handlers/node-to-jsx/calendar-events-reviver.ts +1 -0
  3. package/__tests__/node-handlers/node-to-jsx/cms-list-repeater-loading.ts +1 -0
  4. package/__tests__/node-handlers/node-to-jsx/index.ts +3 -0
  5. package/__tests__/node-handlers/node-to-jsx/utils.ts +1 -0
  6. package/__tests__/utils/url-query-writer.ts +239 -0
  7. package/__tests__/utils/url-search-param-sync.ts +30 -26
  8. package/dist/cjs/index.d.ts +2 -1
  9. package/dist/cjs/index.d.ts.map +1 -1
  10. package/dist/cjs/index.js +3 -1
  11. package/dist/cjs/index.js.map +1 -1
  12. package/dist/cjs/node-handlers/node-to-jsx/index.js +25 -3
  13. package/dist/cjs/node-handlers/node-to-jsx/index.js.map +1 -1
  14. package/dist/cjs/node-handlers/node-to-jsx/types.d.ts +9 -1
  15. package/dist/cjs/node-handlers/node-to-jsx/types.d.ts.map +1 -1
  16. package/dist/cjs/tsconfig.tsbuildinfo +1 -1
  17. package/dist/cjs/utils/ast-utils.d.ts +34 -0
  18. package/dist/cjs/utils/ast-utils.d.ts.map +1 -1
  19. package/dist/cjs/utils/ast-utils.js +53 -4
  20. package/dist/cjs/utils/ast-utils.js.map +1 -1
  21. package/dist/cjs/utils/url-query-writer.d.ts +123 -0
  22. package/dist/cjs/utils/url-query-writer.d.ts.map +1 -0
  23. package/dist/cjs/utils/url-query-writer.js +260 -0
  24. package/dist/cjs/utils/url-query-writer.js.map +1 -0
  25. package/dist/cjs/utils/url-search-param-sync.d.ts.map +1 -1
  26. package/dist/cjs/utils/url-search-param-sync.js +24 -52
  27. package/dist/cjs/utils/url-search-param-sync.js.map +1 -1
  28. package/dist/esm/index.d.ts +2 -1
  29. package/dist/esm/index.d.ts.map +1 -1
  30. package/dist/esm/index.js +2 -1
  31. package/dist/esm/index.js.map +1 -1
  32. package/dist/esm/node-handlers/node-to-jsx/index.js +26 -4
  33. package/dist/esm/node-handlers/node-to-jsx/index.js.map +1 -1
  34. package/dist/esm/node-handlers/node-to-jsx/types.d.ts +9 -1
  35. package/dist/esm/node-handlers/node-to-jsx/types.d.ts.map +1 -1
  36. package/dist/esm/tsconfig.tsbuildinfo +1 -1
  37. package/dist/esm/utils/ast-utils.d.ts +34 -0
  38. package/dist/esm/utils/ast-utils.d.ts.map +1 -1
  39. package/dist/esm/utils/ast-utils.js +50 -3
  40. package/dist/esm/utils/ast-utils.js.map +1 -1
  41. package/dist/esm/utils/url-query-writer.d.ts +123 -0
  42. package/dist/esm/utils/url-query-writer.d.ts.map +1 -0
  43. package/dist/esm/utils/url-query-writer.js +229 -0
  44. package/dist/esm/utils/url-query-writer.js.map +1 -0
  45. package/dist/esm/utils/url-search-param-sync.d.ts.map +1 -1
  46. package/dist/esm/utils/url-search-param-sync.js +24 -52
  47. package/dist/esm/utils/url-search-param-sync.js.map +1 -1
  48. package/package.json +4 -4
  49. package/src/index.ts +2 -0
  50. package/src/node-handlers/node-to-jsx/index.ts +31 -4
  51. package/src/node-handlers/node-to-jsx/types.ts +9 -0
  52. package/src/utils/ast-utils.ts +64 -3
  53. package/src/utils/url-query-writer.ts +318 -0
  54. package/src/utils/url-search-param-sync.ts +26 -86
@@ -24,6 +24,29 @@ describe('CSS Class Generation', () => {
24
24
  expect(result).toEqual(`.name h1 > h2 .ab.cd #id {
25
25
  some-key: value;
26
26
  other-key: otherValue;
27
+ }`)
28
+ })
29
+
30
+ it('attaches an ATTRIBUTE subselector with no separating space', () => {
31
+ // ⛔ THE SELECTED-PAGE CONTRACT. A numbered pagination strip repeats one
32
+ // authored node per visible page, so the copies cannot differ in the
33
+ // document. Both runtimes instead mark the showing page with
34
+ // `aria-current="page"` (`numbered-pagination.ts` here, `element-node.tsx`
35
+ // in the canvas editor), and the LOOK is a compound project class whose
36
+ // subselector is that attribute.
37
+ //
38
+ // A space before the bracket would turn this into a DESCENDANT selector —
39
+ // `.thq-page-number [aria-current='page']` matches something INSIDE the
40
+ // button, never the button — and the selected page would silently render
41
+ // identically to every other one. Which is exactly the bug the marker was
42
+ // added to fix.
43
+ const result = createCSSClassWithSelector('thq-page-number', "&[aria-current='page']", {
44
+ background: '#2563eb',
45
+ fontWeight: '600',
46
+ })
47
+ expect(result).toEqual(`.thq-page-number[aria-current='page'] {
48
+ background: #2563eb;
49
+ font-weight: 600;
27
50
  }`)
28
51
  })
29
52
  })
@@ -40,6 +40,7 @@ const buildParams = (): JSXGenerationParams => ({
40
40
  nodesLookup: {},
41
41
  windowImports: {},
42
42
  localeReferences: [],
43
+ localeAttributeReferences: [],
43
44
  globalReferences: [],
44
45
  globalStateReferences: [],
45
46
  hoistedConstants: [],
@@ -28,6 +28,7 @@ const params = (): JSXGenerationParams => ({
28
28
  nodesLookup: {},
29
29
  windowImports: {},
30
30
  localeReferences: [],
31
+ localeAttributeReferences: [],
31
32
  globalReferences: [],
32
33
  globalStateReferences: [],
33
34
  hoistedConstants: [],
@@ -20,6 +20,7 @@ describe('generateJSXSyntax', () => {
20
20
  nodesLookup: {},
21
21
  windowImports: {},
22
22
  localeReferences: [],
23
+ localeAttributeReferences: [],
23
24
  globalReferences: [],
24
25
  globalStateReferences: [],
25
26
  hoistedConstants: [],
@@ -119,6 +120,7 @@ describe('generateJSXSyntax', () => {
119
120
  nodesLookup: {},
120
121
  windowImports: {},
121
122
  localeReferences: [],
123
+ localeAttributeReferences: [],
122
124
  globalReferences: [],
123
125
  globalStateReferences: [],
124
126
  hoistedConstants: [],
@@ -545,6 +547,7 @@ describe('generateJSXSyntax', () => {
545
547
  nodesLookup: {},
546
548
  windowImports: {},
547
549
  localeReferences: [],
550
+ localeAttributeReferences: [],
548
551
  globalReferences: [],
549
552
  globalStateReferences: [],
550
553
  hoistedConstants: [],
@@ -188,6 +188,7 @@ describe('createConditionIdentifier', () => {
188
188
  nodesLookup: {},
189
189
  windowImports: {},
190
190
  localeReferences: [],
191
+ localeAttributeReferences: [],
191
192
  globalReferences: [],
192
193
  globalStateReferences: [],
193
194
  hoistedConstants: [],
@@ -0,0 +1,239 @@
1
+ import * as types from '@babel/types'
2
+ import generator from '@babel/generator'
3
+ import {
4
+ QUERY_SYNC_REF_ID,
5
+ QUERY_WRITER_ID,
6
+ buildQuerySettledExpr,
7
+ buildQuerySyncDeclarations,
8
+ buildQueryWriteCall,
9
+ ensureQuerySyncDeclarations,
10
+ hasQuerySyncDeclarations,
11
+ } from '../../src/utils/url-query-writer'
12
+
13
+ const codeOf = (nodes: types.Statement[]): string =>
14
+ generator(types.program(nodes) as types.Node).code
15
+
16
+ /**
17
+ * A router that behaves like Next's.
18
+ *
19
+ * The two properties that caused the defect are reproduced faithfully:
20
+ * • `replace` resolves ASYNCHRONOUSLY, and `query` is only updated when it
21
+ * does — so a second writer running in the same tick still sees the old one;
22
+ * • `query` is a fresh object each time, as `makePublicRouterInstance` makes it.
23
+ */
24
+ const makeRouter = (initialQuery: Record<string, string>) => {
25
+ const replaced: Array<Record<string, string>> = []
26
+ let resolveAll: Array<() => void> = []
27
+ const router = {
28
+ pathname: '/products',
29
+ query: { ...initialQuery },
30
+ replace(url: { query: Record<string, string> }) {
31
+ replaced.push({ ...url.query })
32
+ return new Promise<boolean>((resolve) => {
33
+ resolveAll.push(() => {
34
+ // Next applies the navigation, THEN resolves.
35
+ router.query = { ...url.query }
36
+ resolve(true)
37
+ })
38
+ })
39
+ },
40
+ }
41
+ const settleAll = async (): Promise<void> => {
42
+ while (resolveAll.length > 0) {
43
+ const pending = resolveAll
44
+ resolveAll = []
45
+ pending.forEach((fn) => fn())
46
+ await Promise.resolve()
47
+ await Promise.resolve()
48
+ }
49
+ }
50
+ return { router, replaced, settleAll }
51
+ }
52
+
53
+ /** Evaluates the emitted declarations and hands back the writer they define. */
54
+ const instantiate = (router: unknown) => {
55
+ const code = codeOf(buildQuerySyncDeclarations())
56
+ // tslint:disable-next-line:function-constructor
57
+ const factory = new Function(
58
+ 'useRef',
59
+ 'router',
60
+ `${code}\nreturn { ref: ${QUERY_SYNC_REF_ID}, write: ${QUERY_WRITER_ID} }`
61
+ )
62
+ // tslint:disable-next-line:no-any
63
+ const useRef = (initial: any) => ({ current: initial })
64
+ return factory(useRef, router) as {
65
+ ref: { current: { pending: Record<string, string> | null; inFlight: number } }
66
+ // tslint:disable-next-line:no-any
67
+ write: (key: string, value: any) => void
68
+ }
69
+ }
70
+
71
+ describe('URLQueryWriter — emitted shape', () => {
72
+ it('merges into the pending query, not into router.query', () => {
73
+ // Babel prints objects multi-line; the assertions read better flattened.
74
+ const code = codeOf(buildQuerySyncDeclarations()).replace(/\s+/g, ' ')
75
+
76
+ expect(code).toContain('const base = __tqQuerySyncRef.current.pending || router.query')
77
+ expect(code).toContain('const next = { ...base }')
78
+ // The only bail: this key already holds the value we would write.
79
+ expect(code).toContain('if (next[key] === base[key]) return')
80
+ expect(code).toContain('__tqQuerySyncRef.current.pending = next')
81
+ expect(code).toContain('__tqQuerySyncRef.current.inFlight += 1')
82
+ // Released once every replace it was built from has settled, so an external
83
+ // navigation is never merged into a stale snapshot.
84
+ expect(code).toContain('__tqQuerySyncRef.current.pending = null')
85
+ expect(code).toContain('shallow: true')
86
+ })
87
+
88
+ it('builds a call and a settled-check callers can compose', () => {
89
+ expect(generator(buildQueryWriteCall('page', types.identifier('__statePage'))).code).toBe(
90
+ '__tqWriteQueryParam("page", __statePage);'
91
+ )
92
+ expect(generator(buildQuerySettledExpr()).code).toBe('__tqQuerySyncRef.current.inFlight === 0')
93
+ })
94
+
95
+ it('declares itself once, after `const router = useRouter()`', () => {
96
+ const routerDecl = types.variableDeclaration('const', [
97
+ types.variableDeclarator(
98
+ types.identifier('router'),
99
+ types.callExpression(types.identifier('useRouter'), [])
100
+ ),
101
+ ])
102
+ const body: types.Statement[] = [routerDecl, types.returnStatement(types.nullLiteral())]
103
+
104
+ expect(hasQuerySyncDeclarations(body)).toBe(false)
105
+ expect(ensureQuerySyncDeclarations(body)).toBe(true)
106
+ expect(body.length).toBe(4)
107
+ expect(codeOf(body).indexOf('useRouter')).toBeLessThan(codeOf(body).indexOf(QUERY_SYNC_REF_ID))
108
+
109
+ // A second plugin needing the same pair must not emit a duplicate hook —
110
+ // that would also shift the hook order between renders.
111
+ expect(ensureQuerySyncDeclarations(body)).toBe(false)
112
+ expect(body.length).toBe(4)
113
+ })
114
+ })
115
+
116
+ /**
117
+ * ⛔ These run the generated code. Every other guard in this repo is a
118
+ * source-text assertion, and effect INTERLEAVING is exactly what source text
119
+ * cannot see — which is why the defect below shipped green.
120
+ */
121
+ describe('URLQueryWriter — behaviour under a Next-like async router', () => {
122
+ it('keeps both keys when two controls write in the same flush', async () => {
123
+ // The live repro: on `?page=5`, picking a category writes the filter AND
124
+ // resets the page, so both writers run before either replace has landed.
125
+ const { router, replaced, settleAll } = makeRouter({ page: '5' })
126
+ const { write } = instantiate(router)
127
+
128
+ write('categoryFilter', 'Rings')
129
+ write('page', undefined) // page 1 ⇒ remove the key
130
+
131
+ expect(replaced).toHaveLength(2)
132
+ // The SECOND write built on the first's payload rather than on the stale
133
+ // `router.query` — this is the whole fix.
134
+ expect(replaced[1]).toEqual({ categoryFilter: 'Rings' })
135
+
136
+ await settleAll()
137
+ expect(router.query).toEqual({ categoryFilter: 'Rings' })
138
+ })
139
+
140
+ it('does not resurrect a key another writer removed in the same flush', async () => {
141
+ // The mirror image: clearing the search box on page 3 removes
142
+ // `?searchKeyword` and resets the page. A stale base would have put the
143
+ // search term the visitor just deleted straight back into the URL.
144
+ const { router, replaced, settleAll } = makeRouter({ page: '3', searchKeyword: 'shoes' })
145
+ const { write } = instantiate(router)
146
+
147
+ write('searchKeyword', '')
148
+ write('page', undefined)
149
+
150
+ expect(replaced[1]).toEqual({})
151
+ await settleAll()
152
+ expect(router.query).toEqual({})
153
+ })
154
+
155
+ it('settles, and never oscillates when both sides are re-asserted', async () => {
156
+ // Re-running every writer after the URL lands — which is what a re-render
157
+ // does — must produce NO further navigation. An extra replace here is the
158
+ // first step of the infinite loop.
159
+ const { router, replaced, settleAll } = makeRouter({ page: '5' })
160
+ const { write } = instantiate(router)
161
+
162
+ write('categoryFilter', 'Rings')
163
+ write('page', undefined)
164
+ await settleAll()
165
+
166
+ const settledCount = replaced.length
167
+ write('categoryFilter', 'Rings')
168
+ write('page', undefined)
169
+ expect(replaced).toHaveLength(settledCount)
170
+ })
171
+
172
+ it('bails without a replace when the key already holds the value', () => {
173
+ const { router, replaced } = makeRouter({ sortBy: 'price-asc' })
174
+ const { write } = instantiate(router)
175
+
176
+ write('sortBy', 'price-asc')
177
+ expect(replaced).toHaveLength(0)
178
+
179
+ // ...and removing an absent key is equally a no-op.
180
+ write('missing', undefined)
181
+ expect(replaced).toHaveLength(0)
182
+ })
183
+
184
+ it('coerces to a string, so a numeric page and a string filter round-trip alike', () => {
185
+ const { router, replaced } = makeRouter({})
186
+ const { write } = instantiate(router)
187
+
188
+ write('page', 3)
189
+ expect(replaced[0]).toEqual({ page: '3' })
190
+ })
191
+
192
+ it('reports settled only once every replace it issued has landed', async () => {
193
+ const { router, settleAll } = makeRouter({ page: '5' })
194
+ const { ref, write } = instantiate(router)
195
+
196
+ expect(ref.current.inFlight).toBe(0)
197
+ write('categoryFilter', 'Rings')
198
+ write('page', undefined)
199
+ // The page ⇄ URL effect reads this to know that `router.query` is a value
200
+ // the page is on its way OUT of, and must not be adopted.
201
+ expect(ref.current.inFlight).toBe(2)
202
+
203
+ await settleAll()
204
+ expect(ref.current.inFlight).toBe(0)
205
+ expect(ref.current.pending).toBeNull()
206
+ })
207
+
208
+ it('releases the pending query even when a navigation is rejected', async () => {
209
+ // A cancelled route must not freeze every later write against a stale base.
210
+ const router = {
211
+ pathname: '/products',
212
+ query: { page: '5' } as Record<string, string>,
213
+ replace: () => Promise.reject(new Error('route cancelled')),
214
+ }
215
+ const { ref, write } = instantiate(router)
216
+
217
+ write('categoryFilter', 'Rings')
218
+ await Promise.resolve()
219
+ await Promise.resolve()
220
+
221
+ expect(ref.current.inFlight).toBe(0)
222
+ expect(ref.current.pending).toBeNull()
223
+ })
224
+
225
+ it('picks up an external navigation once nothing is in flight', async () => {
226
+ // Browser back/forward moves the URL without asking us. The next write must
227
+ // build on where the browser actually is, not on our last request.
228
+ const { router, replaced, settleAll } = makeRouter({ page: '5' })
229
+ const { write } = instantiate(router)
230
+
231
+ write('categoryFilter', 'Rings')
232
+ await settleAll()
233
+
234
+ router.query = { locale: 'fr' } // an external navigation
235
+ write('page', 2)
236
+
237
+ expect(replaced[replaced.length - 1]).toEqual({ locale: 'fr', page: '2' })
238
+ })
239
+ })
@@ -11,7 +11,7 @@ import {
11
11
  const codeOf = (node: types.Node): string => generator(node).code
12
12
 
13
13
  describe('URLSearchParamSync.buildUrlWriteBackEffect', () => {
14
- it('emits a loop-free state→URL write-back for a bare state identifier (dropdown case)', () => {
14
+ it('delegates the write to the shared query writer (dropdown case)', () => {
15
15
  const effect = buildUrlWriteBackEffect(
16
16
  'categoryFilter',
17
17
  types.identifier('selectedCategory'),
@@ -22,17 +22,25 @@ describe('URLSearchParamSync.buildUrlWriteBackEffect', () => {
22
22
  // SSR + router-ready guards.
23
23
  expect(code).toContain('if (typeof window === "undefined") return')
24
24
  expect(code).toContain('if (!router.isReady) return')
25
- // Snapshot the current query, then set or delete the bound key.
26
- expect(code).toContain('const __nextQuery =')
27
- expect(code).toContain('...router.query')
28
- expect(code).toContain('if (selectedCategory === "" || selectedCategory == null)')
29
- expect(code).toContain('delete __nextQuery.categoryFilter')
30
- expect(code).toContain('__nextQuery.categoryFilter = String(selectedCategory)')
31
- // Skip-if-equal guard prevents a redundant replace (the loop breaker on the URL side).
32
- expect(code).toContain('if (__nextQuery.categoryFilter === router.query.categoryFilter) return')
33
- expect(code).toContain('shallow: true')
34
- // Deps: the value + router.isReady (so it re-runs once the router hydrates).
25
+
26
+ // ⛔ The effect must NOT build the next query itself. Spreading
27
+ // `router.query` here is what let two controls writing in one flush erase
28
+ // each other's keys — and, through their read-backs, oscillate forever.
29
+ expect(code).not.toContain('__nextQuery')
30
+ expect(code).not.toContain('...router.query')
31
+ expect(code).not.toContain('router.replace')
32
+
33
+ // One key, one call. `undefined` is the writer's "remove this key".
34
+ expect(code).toContain(
35
+ '__tqWriteQueryParam("categoryFilter", selectedCategory === "" || selectedCategory == null ? undefined : selectedCategory)'
36
+ )
37
+
38
+ // ⛔ `router.query` is deliberately NOT a dependency. It was added once as a
39
+ // way to RE-ASSERT a key another writer had dropped, which is precisely the
40
+ // edge that closed the loop; with clobbering fixed at its source there is
41
+ // nothing to re-assert.
35
42
  expect(code).toContain('}, [selectedCategory, router.isReady])')
43
+ expect(code).not.toContain('router.isReady, router.query]')
36
44
  })
37
45
 
38
46
  it('emits the write-back reading a member expression (search debounced-query case)', () => {
@@ -44,16 +52,14 @@ describe('URLSearchParamSync.buildUrlWriteBackEffect', () => {
44
52
  // The value pushed to the URL is read from the combined state object,
45
53
  // not a bare identifier — this is the generalization the search input needs.
46
54
  expect(code).toContain(
47
- 'if (ds_0_state.debouncedQuery === "" || ds_0_state.debouncedQuery == null)'
55
+ '__tqWriteQueryParam("searchKeyword", ds_0_state.debouncedQuery === "" || ds_0_state.debouncedQuery == null ? undefined : ds_0_state.debouncedQuery)'
48
56
  )
49
- expect(code).toContain('__nextQuery.searchKeyword = String(ds_0_state.debouncedQuery)')
50
- expect(code).toContain('if (__nextQuery.searchKeyword === router.query.searchKeyword) return')
51
57
  // Dependency is the debounced member expression, so the URL only updates
52
58
  // after the debounce commits — never on every keystroke.
53
59
  expect(code).toContain('}, [ds_0_state.debouncedQuery, router.isReady])')
54
60
  })
55
61
 
56
- it('uses bracket notation for URL keys that are not valid JS identifiers', () => {
62
+ it('passes a non-identifier URL key through as a plain string argument', () => {
57
63
  const effect = buildUrlWriteBackEffect(
58
64
  'search-keyword',
59
65
  types.identifier('q'),
@@ -61,12 +67,12 @@ describe('URLSearchParamSync.buildUrlWriteBackEffect', () => {
61
67
  )
62
68
  const code = codeOf(effect)
63
69
 
64
- expect(code).toContain('delete __nextQuery["search-keyword"]')
65
- expect(code).toContain('__nextQuery["search-keyword"] = String(q)')
66
- expect(code).not.toContain('__nextQuery.search-keyword')
70
+ // The writer indexes with `next[key]`, so an awkward key needs no bracket
71
+ // gymnastics at the call site any more.
72
+ expect(code).toContain('__tqWriteQueryParam("search-keyword"')
67
73
  })
68
74
 
69
- it('also deletes the key when the value equals a non-empty default (clean canonical URL)', () => {
75
+ it('also removes the key when the value equals a non-empty default (clean canonical URL)', () => {
70
76
  const effect = buildUrlWriteBackEffect(
71
77
  'sortBy',
72
78
  types.identifier('sortBy'),
@@ -75,18 +81,16 @@ describe('URLSearchParamSync.buildUrlWriteBackEffect', () => {
75
81
  )
76
82
  const code = codeOf(effect)
77
83
 
78
- // The default now also routes to the delete branch, so loading the page at
79
- // its default sort never writes a sticky `?sortBy=name-asc`.
80
- expect(code).toContain('if (sortBy === "" || sortBy == null || sortBy === "name-asc")')
81
- expect(code).toContain('delete __nextQuery.sortBy')
82
- expect(code).toContain('__nextQuery.sortBy = String(sortBy)')
84
+ // The default routes to the remove branch, so loading the page at its
85
+ // default sort never writes a sticky `?sortBy=name-asc`.
86
+ expect(code).toContain('sortBy === "" || sortBy == null || sortBy === "name-asc" ? undefined')
83
87
  })
84
88
 
85
- it('omits the default clause when no default is supplied (byte-identical fallback)', () => {
89
+ it('omits the default clause when no default is supplied', () => {
86
90
  const withoutDefault = codeOf(
87
91
  buildUrlWriteBackEffect('sortBy', types.identifier('sortBy'), types.identifier('sortBy'))
88
92
  )
89
- expect(withoutDefault).toContain('if (sortBy === "" || sortBy == null)')
93
+ expect(withoutDefault).toContain('sortBy === "" || sortBy == null ? undefined : sortBy')
90
94
  expect(withoutDefault).not.toContain('=== "name-asc"')
91
95
  })
92
96
  })
@@ -5,10 +5,11 @@ import * as ASTUtils from './utils/ast-utils';
5
5
  import * as StyleUtils from './utils/style-utils';
6
6
  import * as HASTUtils from './utils/hast-utils';
7
7
  import * as URLSearchParamSync from './utils/url-search-param-sync';
8
+ import * as URLQueryWriter from './utils/url-query-writer';
8
9
  import * as RouteUtils from './utils/route-utils';
9
10
  import ParsedASTNode from './utils/parsed-ast';
10
11
  import { createBinaryExpression } from './node-handlers/node-to-jsx/utils';
11
- export { ASTBuilders, StyleBuilders, HASTBuilders, ASTUtils, StyleUtils, HASTUtils, URLSearchParamSync, RouteUtils, ParsedASTNode, createBinaryExpression, };
12
+ export { ASTBuilders, StyleBuilders, HASTBuilders, ASTUtils, StyleUtils, HASTUtils, URLSearchParamSync, URLQueryWriter, RouteUtils, ParsedASTNode, createBinaryExpression, };
12
13
  export { default as createHTMLTemplateSyntax } from './node-handlers/node-to-html';
13
14
  export { default as createJSXSyntax } from './node-handlers/node-to-jsx';
14
15
  export * from './node-handlers/node-to-html/types';
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAA;AACtD,OAAO,KAAK,aAAa,MAAM,2BAA2B,CAAA;AAC1D,OAAO,KAAK,YAAY,MAAM,0BAA0B,CAAA;AAExD,OAAO,KAAK,QAAQ,MAAM,mBAAmB,CAAA;AAC7C,OAAO,KAAK,UAAU,MAAM,qBAAqB,CAAA;AACjD,OAAO,KAAK,SAAS,MAAM,oBAAoB,CAAA;AAC/C,OAAO,KAAK,kBAAkB,MAAM,+BAA+B,CAAA;AACnE,OAAO,KAAK,UAAU,MAAM,qBAAqB,CAAA;AACjD,OAAO,aAAa,MAAM,oBAAoB,CAAA;AAE9C,OAAO,EAAE,sBAAsB,EAAE,MAAM,mCAAmC,CAAA;AAE1E,OAAO,EACL,WAAW,EACX,aAAa,EACb,YAAY,EACZ,QAAQ,EACR,UAAU,EACV,SAAS,EACT,kBAAkB,EAClB,UAAU,EACV,aAAa,EACb,sBAAsB,GACvB,CAAA;AAED,OAAO,EAAE,OAAO,IAAI,wBAAwB,EAAE,MAAM,8BAA8B,CAAA;AAClF,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,6BAA6B,CAAA;AAExE,cAAc,oCAAoC,CAAA;AAClD,cAAc,mCAAmC,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,WAAW,MAAM,yBAAyB,CAAA;AACtD,OAAO,KAAK,aAAa,MAAM,2BAA2B,CAAA;AAC1D,OAAO,KAAK,YAAY,MAAM,0BAA0B,CAAA;AAExD,OAAO,KAAK,QAAQ,MAAM,mBAAmB,CAAA;AAC7C,OAAO,KAAK,UAAU,MAAM,qBAAqB,CAAA;AACjD,OAAO,KAAK,SAAS,MAAM,oBAAoB,CAAA;AAC/C,OAAO,KAAK,kBAAkB,MAAM,+BAA+B,CAAA;AACnE,OAAO,KAAK,cAAc,MAAM,0BAA0B,CAAA;AAC1D,OAAO,KAAK,UAAU,MAAM,qBAAqB,CAAA;AACjD,OAAO,aAAa,MAAM,oBAAoB,CAAA;AAE9C,OAAO,EAAE,sBAAsB,EAAE,MAAM,mCAAmC,CAAA;AAE1E,OAAO,EACL,WAAW,EACX,aAAa,EACb,YAAY,EACZ,QAAQ,EACR,UAAU,EACV,SAAS,EACT,kBAAkB,EAClB,cAAc,EACd,UAAU,EACV,aAAa,EACb,sBAAsB,GACvB,CAAA;AAED,OAAO,EAAE,OAAO,IAAI,wBAAwB,EAAE,MAAM,8BAA8B,CAAA;AAClF,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,6BAA6B,CAAA;AAExE,cAAc,oCAAoC,CAAA;AAClD,cAAc,mCAAmC,CAAA"}
package/dist/cjs/index.js CHANGED
@@ -29,7 +29,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
29
29
  return (mod && mod.__esModule) ? mod : { "default": mod };
30
30
  };
31
31
  Object.defineProperty(exports, "__esModule", { value: true });
32
- exports.createJSXSyntax = exports.createHTMLTemplateSyntax = exports.createBinaryExpression = exports.ParsedASTNode = exports.RouteUtils = exports.URLSearchParamSync = exports.HASTUtils = exports.StyleUtils = exports.ASTUtils = exports.HASTBuilders = exports.StyleBuilders = exports.ASTBuilders = void 0;
32
+ exports.createJSXSyntax = exports.createHTMLTemplateSyntax = exports.createBinaryExpression = exports.ParsedASTNode = exports.RouteUtils = exports.URLQueryWriter = exports.URLSearchParamSync = exports.HASTUtils = exports.StyleUtils = exports.ASTUtils = exports.HASTBuilders = exports.StyleBuilders = exports.ASTBuilders = void 0;
33
33
  var ASTBuilders = __importStar(require("./builders/ast-builders"));
34
34
  exports.ASTBuilders = ASTBuilders;
35
35
  var StyleBuilders = __importStar(require("./builders/style-builders"));
@@ -44,6 +44,8 @@ var HASTUtils = __importStar(require("./utils/hast-utils"));
44
44
  exports.HASTUtils = HASTUtils;
45
45
  var URLSearchParamSync = __importStar(require("./utils/url-search-param-sync"));
46
46
  exports.URLSearchParamSync = URLSearchParamSync;
47
+ var URLQueryWriter = __importStar(require("./utils/url-query-writer"));
48
+ exports.URLQueryWriter = URLQueryWriter;
47
49
  var RouteUtils = __importStar(require("./utils/route-utils"));
48
50
  exports.RouteUtils = RouteUtils;
49
51
  var parsed_ast_1 = __importDefault(require("./utils/parsed-ast"));
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,mEAAsD;AAcpD,kCAAW;AAbb,uEAA0D;AAcxD,sCAAa;AAbf,qEAAwD;AActD,oCAAY;AAZd,0DAA6C;AAa3C,4BAAQ;AAZV,8DAAiD;AAa/C,gCAAU;AAZZ,4DAA+C;AAa7C,8BAAS;AAZX,gFAAmE;AAajE,gDAAkB;AAZpB,8DAAiD;AAa/C,gCAAU;AAZZ,kEAA8C;AAa5C,wBAbK,oBAAa,CAaL;AAXf,2DAA0E;AAYxE,uGAZO,8BAAsB,OAYP;AAGxB,6DAAkF;AAAzE,yIAAA,OAAO,OAA4B;AAC5C,2DAAwE;AAA/D,+HAAA,OAAO,OAAmB;AAEnC,qEAAkD;AAClD,oEAAiD"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,mEAAsD;AAepD,kCAAW;AAdb,uEAA0D;AAexD,sCAAa;AAdf,qEAAwD;AAetD,oCAAY;AAbd,0DAA6C;AAc3C,4BAAQ;AAbV,8DAAiD;AAc/C,gCAAU;AAbZ,4DAA+C;AAc7C,8BAAS;AAbX,gFAAmE;AAcjE,gDAAkB;AAbpB,uEAA0D;AAcxD,wCAAc;AAbhB,8DAAiD;AAc/C,gCAAU;AAbZ,kEAA8C;AAc5C,wBAdK,oBAAa,CAcL;AAZf,2DAA0E;AAaxE,uGAbO,8BAAsB,OAaP;AAGxB,6DAAkF;AAAzE,yIAAA,OAAO,OAA4B;AAC5C,2DAAwE;AAA/D,+HAAA,OAAO,OAAmB;AAEnC,qEAAkD;AAClD,oEAAiD"}
@@ -779,14 +779,36 @@ var addAttributesToJSXTag = function (attrs, elementTag, options, params, elemen
779
779
  params.globalReferences.push(normRef.content.id);
780
780
  // Use the normalized ref to generate the attribute expression
781
781
  var globalExpr = (0, utils_1.createDynamicValueExpression)(normRef, options);
782
- elementTag.openingElement.attributes.push(types.jsxAttribute(types.jsxIdentifier((0, ast_utils_1.convertToReactAttributeName)(attrKey)), types.jsxExpressionContainer(globalExpr)));
782
+ elementTag.openingElement.attributes.push(types.jsxAttribute(types.jsxIdentifier((0, ast_utils_1.resolveAttributeNameForTag)(elementTag, attrKey)), types.jsxExpressionContainer(globalExpr)));
783
783
  break;
784
784
  }
785
785
  if (referenceType === 'globalState') {
786
786
  var gsName = (0, utils_1.resolveGlobalStateName)(dynamicRef.content.id, params.globalStateDefinitions);
787
787
  params.globalStateReferences.push({ id: dynamicRef.content.id, name: gsName });
788
788
  var expr = (0, utils_1.createGlobalStateExpression)(dynamicRef, params.globalStateDefinitions);
789
- elementTag.openingElement.attributes.push(types.jsxAttribute(types.jsxIdentifier((0, ast_utils_1.convertToReactAttributeName)(attrKey)), types.jsxExpressionContainer(expr)));
789
+ elementTag.openingElement.attributes.push(types.jsxAttribute(types.jsxIdentifier((0, ast_utils_1.resolveAttributeNameForTag)(elementTag, attrKey)), types.jsxExpressionContainer(expr)));
790
+ break;
791
+ }
792
+ // A translated attribute (`placeholder`, `alt`, `title`, `aria-label`, …).
793
+ // It cannot be emitted the way a translated CHILD is — that path produces a
794
+ // `<span>` the framework plugin fills through `dangerouslySetInnerHTML`, and
795
+ // an attribute needs a plain string. So the attribute is written with the
796
+ // main-language text (carried on the reference's `fallback`) and recorded on
797
+ // `params.localeAttributeReferences`; the target framework's i18n plugin
798
+ // rewrites the recorded attributes into its own translation lookup — see
799
+ // `createNextInternationalizationPlugin`. A framework without such a plugin
800
+ // keeps the main-language string, which is valid, readable markup.
801
+ if (referenceType === 'locale') {
802
+ var translationKey = dynamicRef.content.id;
803
+ if (!translationKey) {
804
+ break;
805
+ }
806
+ // `fallback` only exists on the plain reference member of the union, and
807
+ // narrowing happened on the destructured `referenceType`, not on the ref.
808
+ var fallback = dynamicRef.content.fallback;
809
+ var attribute = types.jsxAttribute(types.jsxIdentifier((0, ast_utils_1.resolveAttributeNameForTag)(elementTag, attrKey)), types.stringLiteral(teleport_shared_1.StringUtils.encode(fallback === undefined ? '' : String(fallback))));
810
+ elementTag.openingElement.attributes.push(attribute);
811
+ params.localeAttributeReferences.push({ attribute: attribute, key: translationKey });
790
812
  break;
791
813
  }
792
814
  switch (referenceType) {
@@ -832,7 +854,7 @@ var addAttributesToJSXTag = function (attrs, elementTag, options, params, elemen
832
854
  var constName = "__staticProp".concat(params.hoistedConstants.length);
833
855
  var expression = (0, ast_utils_1.objectToObjectExpression)(attributeValue.content);
834
856
  params.hoistedConstants.push({ name: constName, expression: expression });
835
- elementTag.openingElement.attributes.push(types.jsxAttribute(types.jsxIdentifier((0, ast_utils_1.convertToReactAttributeName)(attrKey)), types.jsxExpressionContainer(types.identifier(constName))));
857
+ elementTag.openingElement.attributes.push(types.jsxAttribute(types.jsxIdentifier((0, ast_utils_1.resolveAttributeNameForTag)(elementTag, attrKey)), types.jsxExpressionContainer(types.identifier(constName))));
836
858
  }
837
859
  else {
838
860
  (0, ast_utils_1.addAttributeToJSXTag)(elementTag, attrKey, attributeValue.content);