@barefootjs/jsx 0.4.0 → 0.5.0

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.
@@ -31,9 +31,10 @@ describe('sort().map() / toSorted().map()', () => {
31
31
  expect(loop).toBeDefined()
32
32
  if (loop?.type === 'loop') {
33
33
  expect(loop.sortComparator).toBeDefined()
34
- expect(loop.sortComparator!.key).toEqual({ kind: 'field', field: 'price' })
35
- expect(loop.sortComparator!.type).toBe('numeric')
36
- expect(loop.sortComparator!.direction).toBe('asc')
34
+ expect(loop.sortComparator!.keys).toHaveLength(1)
35
+ expect(loop.sortComparator!.keys[0].key).toEqual({ kind: 'field', field: 'price' })
36
+ expect(loop.sortComparator!.keys[0].type).toBe('numeric')
37
+ expect(loop.sortComparator!.keys[0].direction).toBe('asc')
37
38
  expect(loop.sortComparator!.method).toBe('sort')
38
39
  expect(loop.sortComparator!.paramA).toBe('a')
39
40
  expect(loop.sortComparator!.paramB).toBe('b')
@@ -69,9 +70,10 @@ describe('sort().map() / toSorted().map()', () => {
69
70
  expect(loop).toBeDefined()
70
71
  if (loop?.type === 'loop') {
71
72
  expect(loop.sortComparator).toBeDefined()
72
- expect(loop.sortComparator!.key).toEqual({ kind: 'field', field: 'price' })
73
- expect(loop.sortComparator!.type).toBe('numeric')
74
- expect(loop.sortComparator!.direction).toBe('desc')
73
+ expect(loop.sortComparator!.keys).toHaveLength(1)
74
+ expect(loop.sortComparator!.keys[0].key).toEqual({ kind: 'field', field: 'price' })
75
+ expect(loop.sortComparator!.keys[0].type).toBe('numeric')
76
+ expect(loop.sortComparator!.keys[0].direction).toBe('desc')
75
77
  expect(loop.sortComparator!.method).toBe('toSorted')
76
78
  }
77
79
  }
@@ -105,9 +107,10 @@ describe('sort().map() / toSorted().map()', () => {
105
107
  expect(loop.filterPredicate).toBeDefined()
106
108
  expect(loop.filterPredicate!.param).toBe('t')
107
109
  expect(loop.sortComparator).toBeDefined()
108
- expect(loop.sortComparator!.key).toEqual({ kind: 'field', field: 'priority' })
109
- expect(loop.sortComparator!.type).toBe('numeric')
110
- expect(loop.sortComparator!.direction).toBe('asc')
110
+ expect(loop.sortComparator!.keys).toHaveLength(1)
111
+ expect(loop.sortComparator!.keys[0].key).toEqual({ kind: 'field', field: 'priority' })
112
+ expect(loop.sortComparator!.keys[0].type).toBe('numeric')
113
+ expect(loop.sortComparator!.keys[0].direction).toBe('asc')
111
114
  expect(loop.chainOrder).toBe('filter-sort')
112
115
  expect(loop.array).toBe('todos()')
113
116
  }
@@ -147,6 +150,206 @@ describe('sort().map() / toSorted().map()', () => {
147
150
  }
148
151
  })
149
152
 
153
+ test('multi-key (||-chain) produces one SortKey per operand', () => {
154
+ const source = `
155
+ 'use client'
156
+ import { createSignal } from '@barefootjs/client'
157
+
158
+ export function ProductList() {
159
+ const [products, setProducts] = createSignal<any[]>([])
160
+ return (
161
+ <ul>
162
+ {products().sort((a, b) => b.price - a.price || a.name.localeCompare(b.name)).map(p => (
163
+ <li>{p.name}</li>
164
+ ))}
165
+ </ul>
166
+ )
167
+ }
168
+ `
169
+
170
+ const ctx = analyzeComponent(source, 'ProductList.tsx')
171
+ const ir = jsxToIR(ctx)
172
+
173
+ expect(ir).not.toBeNull()
174
+ if (ir!.type === 'element') {
175
+ const loop = ir!.children.find(c => c.type === 'loop')
176
+ expect(loop?.type).toBe('loop')
177
+ if (loop?.type === 'loop') {
178
+ expect(loop.sortComparator).toBeDefined()
179
+ expect(loop.sortComparator!.keys).toEqual([
180
+ { key: { kind: 'field', field: 'price' }, type: 'numeric', direction: 'desc' },
181
+ { key: { kind: 'field', field: 'name' }, type: 'string', direction: 'asc' },
182
+ ])
183
+ }
184
+ }
185
+ })
186
+
187
+ test('relational ternary comparator lowers to an auto key', () => {
188
+ const source = `
189
+ 'use client'
190
+ import { createSignal } from '@barefootjs/client'
191
+
192
+ export function ProductList() {
193
+ const [products, setProducts] = createSignal<any[]>([])
194
+ return (
195
+ <ul>
196
+ {products().toSorted((a, b) => a.rank > b.rank ? 1 : -1).map(p => (
197
+ <li>{p.name}</li>
198
+ ))}
199
+ </ul>
200
+ )
201
+ }
202
+ `
203
+
204
+ const ctx = analyzeComponent(source, 'ProductList.tsx')
205
+ const ir = jsxToIR(ctx)
206
+
207
+ expect(ir).not.toBeNull()
208
+ if (ir!.type === 'element') {
209
+ const loop = ir!.children.find(c => c.type === 'loop')
210
+ if (loop?.type === 'loop') {
211
+ expect(loop.sortComparator).toBeDefined()
212
+ expect(loop.sortComparator!.keys).toEqual([
213
+ { key: { kind: 'field', field: 'rank' }, type: 'auto', direction: 'asc' },
214
+ ])
215
+ }
216
+ }
217
+ })
218
+
219
+ test('3-way ternary comparator derives direction from the outer comparison', () => {
220
+ const source = `
221
+ 'use client'
222
+ import { createSignal } from '@barefootjs/client'
223
+
224
+ export function NumList() {
225
+ const [nums, setNums] = createSignal<number[]>([])
226
+ return (
227
+ <ul>
228
+ {nums().sort((a, b) => a < b ? -1 : a > b ? 1 : 0).map(n => (
229
+ <li>{n}</li>
230
+ ))}
231
+ </ul>
232
+ )
233
+ }
234
+ `
235
+
236
+ const ctx = analyzeComponent(source, 'NumList.tsx')
237
+ const ir = jsxToIR(ctx)
238
+
239
+ expect(ir).not.toBeNull()
240
+ if (ir!.type === 'element') {
241
+ const loop = ir!.children.find(c => c.type === 'loop')
242
+ if (loop?.type === 'loop') {
243
+ expect(loop.sortComparator).toBeDefined()
244
+ expect(loop.sortComparator!.keys).toEqual([
245
+ { key: { kind: 'self' }, type: 'auto', direction: 'asc' },
246
+ ])
247
+ }
248
+ }
249
+ })
250
+
251
+ test('arrow block body with single return unwraps to the comparator', () => {
252
+ const source = `
253
+ 'use client'
254
+ import { createSignal } from '@barefootjs/client'
255
+
256
+ export function ProductList() {
257
+ const [products, setProducts] = createSignal<any[]>([])
258
+ return (
259
+ <ul>
260
+ {products().sort((a, b) => { return a.price - b.price }).map(p => (
261
+ <li>{p.name}</li>
262
+ ))}
263
+ </ul>
264
+ )
265
+ }
266
+ `
267
+
268
+ const ctx = analyzeComponent(source, 'ProductList.tsx')
269
+ const ir = jsxToIR(ctx)
270
+
271
+ expect(ir).not.toBeNull()
272
+ if (ir!.type === 'element') {
273
+ const loop = ir!.children.find(c => c.type === 'loop')
274
+ if (loop?.type === 'loop') {
275
+ expect(loop.sortComparator).toBeDefined()
276
+ expect(loop.sortComparator!.keys).toEqual([
277
+ { key: { kind: 'field', field: 'price' }, type: 'numeric', direction: 'asc' },
278
+ ])
279
+ // block body unwraps to the returned expression, keeping the
280
+ // @client fallback's synthetic `(a, b) => raw` arrow valid.
281
+ expect(loop.sortComparator!.raw).toBe('a.price - b.price')
282
+ }
283
+ }
284
+ })
285
+
286
+ test('leading-equality 3-way ternary (a.f === b.f ? 0 : …) lowers to an auto key', () => {
287
+ const source = `
288
+ 'use client'
289
+ import { createSignal } from '@barefootjs/client'
290
+
291
+ export function ProductList() {
292
+ const [products, setProducts] = createSignal<any[]>([])
293
+ return (
294
+ <ul>
295
+ {products().toSorted((a, b) => a.rank === b.rank ? 0 : a.rank > b.rank ? 1 : -1).map(p => (
296
+ <li>{p.name}</li>
297
+ ))}
298
+ </ul>
299
+ )
300
+ }
301
+ `
302
+
303
+ const ctx = analyzeComponent(source, 'ProductList.tsx')
304
+ const ir = jsxToIR(ctx)
305
+
306
+ expect(ir).not.toBeNull()
307
+ if (ir!.type === 'element') {
308
+ const loop = ir!.children.find(c => c.type === 'loop')
309
+ if (loop?.type === 'loop') {
310
+ expect(loop.sortComparator).toBeDefined()
311
+ // The `=== ? 0` arm is a tie; direction comes from the inner
312
+ // relational ternary (a.rank > b.rank ? 1 : -1 → ascending).
313
+ expect(loop.sortComparator!.keys).toEqual([
314
+ { key: { kind: 'field', field: 'rank' }, type: 'auto', direction: 'asc' },
315
+ ])
316
+ }
317
+ }
318
+ })
319
+
320
+ test('multi-key mixing a numeric leaf and an auto (ternary) leaf', () => {
321
+ const source = `
322
+ 'use client'
323
+ import { createSignal } from '@barefootjs/client'
324
+
325
+ export function ProductList() {
326
+ const [products, setProducts] = createSignal<any[]>([])
327
+ return (
328
+ <ul>
329
+ {products().sort((a, b) => a.price - b.price || (a.rank > b.rank ? 1 : -1)).map(p => (
330
+ <li>{p.name}</li>
331
+ ))}
332
+ </ul>
333
+ )
334
+ }
335
+ `
336
+
337
+ const ctx = analyzeComponent(source, 'ProductList.tsx')
338
+ const ir = jsxToIR(ctx)
339
+
340
+ expect(ir).not.toBeNull()
341
+ if (ir!.type === 'element') {
342
+ const loop = ir!.children.find(c => c.type === 'loop')
343
+ if (loop?.type === 'loop') {
344
+ expect(loop.sortComparator).toBeDefined()
345
+ expect(loop.sortComparator!.keys).toEqual([
346
+ { key: { kind: 'field', field: 'price' }, type: 'numeric', direction: 'asc' },
347
+ { key: { kind: 'field', field: 'rank' }, type: 'auto', direction: 'asc' },
348
+ ])
349
+ }
350
+ }
351
+ })
352
+
150
353
  test('complex sort comparator with @client keeps sort in array', () => {
151
354
  const source = `
152
355
  'use client'
@@ -129,6 +129,33 @@ describe('tokenContainsIdent', () => {
129
129
  })
130
130
  })
131
131
 
132
+ // Regex literals were invisible to the previous hand-rolled char scanner:
133
+ // a lone quote inside the regex flipped it into string state (swallowing
134
+ // real references), and an identifier inside the regex body was counted as
135
+ // a reference. The shared ts.createScanner-based lexer recognises regex
136
+ // literals, so both cases are now correct (#1370).
137
+ describe('regex literals', () => {
138
+ test('reference after a regex literal containing an apostrophe', () => {
139
+ expect(tokenContainsIdent("/it's/.test(className)", 'className')).toBe(true)
140
+ })
141
+
142
+ test('reference after a regex literal containing a quote', () => {
143
+ expect(tokenContainsIdent('/a"b/.test(className)', 'className')).toBe(true)
144
+ })
145
+
146
+ test('identifier inside a regex body is not a reference', () => {
147
+ expect(tokenContainsIdent('/className/.test(x)', 'className')).toBe(false)
148
+ })
149
+
150
+ test('regex with escaped slash does not leak into following code', () => {
151
+ expect(tokenContainsIdent('/a\\/b/.test(className)', 'className')).toBe(true)
152
+ })
153
+
154
+ test('division is not mistaken for a regex literal', () => {
155
+ expect(tokenContainsIdent('total / className', 'className')).toBe(true)
156
+ })
157
+ })
158
+
132
159
  describe('non-matches', () => {
133
160
  test('substring is not a match (word boundary)', () => {
134
161
  expect(tokenContainsIdent('myClassName', 'className')).toBe(false)
@@ -136,21 +136,22 @@ describe('Unsupported Expression Error (BF021)', () => {
136
136
  })
137
137
 
138
138
  describe('Unsupported Sort Comparator (BF021)', () => {
139
- test('emits BF021 for multi-key comparator (||-chained) — outside accepted catalogue', () => {
140
- // #1448 Tier B widened the accepted catalogue to include
141
- // `.localeCompare` and primitive `(a,b) => a - b`. Multi-key
142
- // shapes (`a.x - b.x || a.y - b.y`) are still out of scope —
143
- // they refuse here and must be `@client`-marked or rewritten
144
- // to a single-key sort.
139
+ test('emits BF021 for function-reference comparator — outside accepted catalogue', () => {
140
+ // #1448 Tier B follow-up widened the catalogue to include
141
+ // multi-key (`a.x - b.x || a.y - b.y`), relational ternary, and
142
+ // single-`return` block bodies. Function-reference comparators
143
+ // (`arr.sort(cmp)` where `cmp` is a named function) are still out
144
+ // of scope they need scope resolution and refuse here.
145
145
  const source = `
146
146
  'use client'
147
147
  import { createSignal } from '@barefootjs/client'
148
148
 
149
149
  export function TodoList() {
150
150
  const [items, setItems] = createSignal<any[]>([])
151
+ const cmp = (a, b) => a.priority - b.priority
151
152
  return (
152
153
  <ul>
153
- {items().sort((a, b) => a.priority - b.priority || a.id - b.id).map(t => (
154
+ {items().sort(cmp).map(t => (
154
155
  <li>{t.name}</li>
155
156
  ))}
156
157
  </ul>
@@ -162,7 +163,6 @@ describe('Unsupported Sort Comparator (BF021)', () => {
162
163
  const bf021 = errors.filter(e => e.code === ErrorCodes.UNSUPPORTED_JSX_PATTERN)
163
164
 
164
165
  expect(bf021).toHaveLength(1)
165
- expect(bf021[0].message).toContain('not a supported shape')
166
166
  })
167
167
 
168
168
  test('@client suppresses BF021 for unsupported sort comparator', () => {
@@ -172,9 +172,10 @@ describe('Unsupported Sort Comparator (BF021)', () => {
172
172
 
173
173
  export function TodoList() {
174
174
  const [items, setItems] = createSignal<any[]>([])
175
+ const cmp = (a, b) => a.priority - b.priority
175
176
  return (
176
177
  <ul>
177
- {/* @client */ items().sort((a, b) => a.priority - b.priority || a.id - b.id).map(t => (
178
+ {/* @client */ items().sort(cmp).map(t => (
178
179
  <li>{t.name}</li>
179
180
  ))}
180
181
  </ul>
@@ -188,9 +189,37 @@ describe('Unsupported Sort Comparator (BF021)', () => {
188
189
  expect(bf021).toHaveLength(0)
189
190
  })
190
191
 
191
- test('emits BF021 error for block body sort comparator', () => {
192
- // Block-body comparators are deferred to a Tier B follow-up
193
- // (the extractor only handles expression-body shapes for now).
192
+ test('emits BF021 for localeCompare with a locale/options argument', () => {
193
+ // The zero-arg `a.f.localeCompare(b.f)` form lowers, but the
194
+ // locale/options form needs per-adapter collation plumbing and
195
+ // stays refused (deferred #1448 Tier B follow-up).
196
+ const source = `
197
+ 'use client'
198
+ import { createSignal } from '@barefootjs/client'
199
+
200
+ export function TodoList() {
201
+ const [items, setItems] = createSignal<any[]>([])
202
+ return (
203
+ <ul>
204
+ {items().sort((a, b) => a.name.localeCompare(b.name, 'en', { numeric: true })).map(t => (
205
+ <li>{t.name}</li>
206
+ ))}
207
+ </ul>
208
+ )
209
+ }
210
+ `
211
+
212
+ const { errors } = compileToIR(source)
213
+ const bf021 = errors.filter(e => e.code === ErrorCodes.UNSUPPORTED_JSX_PATTERN)
214
+
215
+ expect(bf021).toHaveLength(1)
216
+ expect(bf021[0].message).toContain('not a supported shape')
217
+ })
218
+
219
+ test('emits BF021 error for multi-statement block-body sort comparator', () => {
220
+ // Single-`return` block bodies now lower (#1448 Tier B follow-up),
221
+ // but multi-statement / local-var bodies stay refused — generalising
222
+ // over arbitrary statement sequences isn't tractable in a template.
194
223
  const source = `
195
224
  'use client'
196
225
  import { createSignal } from '@barefootjs/client'
@@ -199,7 +228,7 @@ describe('Unsupported Sort Comparator (BF021)', () => {
199
228
  const [items, setItems] = createSignal<any[]>([])
200
229
  return (
201
230
  <ul>
202
- {items().sort((a, b) => { return a.price - b.price }).map(t => (
231
+ {items().sort((a, b) => { const x = a.price; return x - b.price }).map(t => (
203
232
  <li>{t.name}</li>
204
233
  ))}
205
234
  </ul>
@@ -123,6 +123,26 @@ export interface TemplateAdapter {
123
123
  * Required for adapters that look up templates by filename (e.g. Mojolicious).
124
124
  */
125
125
  templatesPerComponent?: boolean
126
+ /**
127
+ * How the application author injects the externals importmap (and any
128
+ * `<link rel="modulepreload">` hints) into the page `<head>` when
129
+ * `externals` / `bundleEntries` are configured.
130
+ *
131
+ * - `'component'` — the adapter ships a render-time component (e.g. Hono's
132
+ * `BfImportMap`) that reads `barefoot-externals.json`; `bf build` emits no
133
+ * static snippet.
134
+ * - `'html-snippet'` — the adapter targets a template-string language (Go
135
+ * html/template, Mojolicious EP) with no component layer, so `bf build`
136
+ * writes a ready-to-include `barefoot-importmap.html` alongside
137
+ * `barefoot-externals.json` (via `renderImportMapHtml`).
138
+ *
139
+ * Optional only for backward compatibility (and internal-only adapters like
140
+ * the CSR test adapter). Every *shipping* adapter must set it — the
141
+ * adapter-tests importmap-injection contract enforces this so a new adapter
142
+ * cannot silently leave configured `externals` with no injection point.
143
+ * See issue #1644.
144
+ */
145
+ importMapInjection?: 'component' | 'html-snippet'
126
146
  /**
127
147
  * Module specifier of the SSR shim for `@barefootjs/client` (and
128
148
  * `/runtime`). When set, the compiler rewrites client-package imports in