@barefootjs/jsx 0.21.4 → 0.24.1

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 (46) hide show
  1. package/dist/adapters/env-signal.d.ts +8 -0
  2. package/dist/adapters/env-signal.d.ts.map +1 -1
  3. package/dist/analyzer-context.d.ts +16 -5
  4. package/dist/analyzer-context.d.ts.map +1 -1
  5. package/dist/analyzer.d.ts +10 -4
  6. package/dist/analyzer.d.ts.map +1 -1
  7. package/dist/builtin-lowering-plugins.d.ts.map +1 -1
  8. package/dist/date-lowering.d.ts +16 -0
  9. package/dist/date-lowering.d.ts.map +1 -1
  10. package/dist/errors.d.ts +3 -0
  11. package/dist/errors.d.ts.map +1 -1
  12. package/dist/format-date-lowering.d.ts +30 -0
  13. package/dist/format-date-lowering.d.ts.map +1 -0
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +1124 -74
  17. package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
  18. package/dist/ir-to-client-js/html-template.d.ts +1 -0
  19. package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
  20. package/dist/ir-to-client-js/imports.d.ts +2 -2
  21. package/dist/ir-to-client-js/imports.d.ts.map +1 -1
  22. package/dist/jsx-to-ir.d.ts.map +1 -1
  23. package/dist/to-locale-date-lowering.d.ts +111 -0
  24. package/dist/to-locale-date-lowering.d.ts.map +1 -0
  25. package/dist/types.d.ts +47 -1
  26. package/dist/types.d.ts.map +1 -1
  27. package/package.json +2 -2
  28. package/src/__tests__/format-date-lowering.test.ts +125 -0
  29. package/src/__tests__/reactive-factory-cross-file.test.ts +502 -0
  30. package/src/__tests__/reactive-factory-inlining.test.ts +293 -4
  31. package/src/__tests__/to-locale-date-lowering.test.ts +382 -0
  32. package/src/adapters/env-signal.ts +26 -3
  33. package/src/analyzer-context.ts +19 -4
  34. package/src/analyzer.ts +1012 -93
  35. package/src/builtin-lowering-plugins.ts +8 -1
  36. package/src/date-lowering.ts +1 -1
  37. package/src/errors.ts +19 -0
  38. package/src/format-date-lowering.ts +55 -0
  39. package/src/index.ts +1 -1
  40. package/src/ir-to-client-js/emit-reactive.ts +90 -1
  41. package/src/ir-to-client-js/html-template.ts +36 -2
  42. package/src/ir-to-client-js/imports.ts +4 -0
  43. package/src/jsx-to-ir.ts +90 -1
  44. package/src/rich-type-refusal.ts +9 -1
  45. package/src/to-locale-date-lowering.ts +563 -0
  46. package/src/types.ts +49 -1
@@ -176,14 +176,26 @@ describe('Reactive factory inlining (#931)', () => {
176
176
  })
177
177
 
178
178
  test('non-tuple single-value destructure is NOT treated as a factory call', () => {
179
- // Guard against false positives: `const { x } = obj()` should be left alone.
179
+ // Guard against false positives: `const { x } = obj()` should be left
180
+ // alone when `obj` is an ordinary (non-reactive) function.
181
+ //
182
+ // Correction (#2325): this fixture previously destructured `createSignal`
183
+ // itself with a TUPLE pattern (`const [count, setCount] = createSignal(0)`),
184
+ // which never exercised an object destructure at all — it was already
185
+ // covered by the `callee === 'createSignal'` skip in
186
+ // `validateReactiveFactoryCalls`. Rewritten to genuinely exercise the
187
+ // object-destructure path now that it performs real factory-shape
188
+ // dispatch (#2325 §4c).
180
189
  const source = `
181
190
  'use client'
182
- import { createSignal } from '@barefootjs/client'
191
+
192
+ function getConfig() {
193
+ return { x: 1 }
194
+ }
183
195
 
184
196
  export function Comp() {
185
- const [count, setCount] = createSignal(0)
186
- return <p>{count()}</p>
197
+ const { x } = getConfig()
198
+ return <p>{x}</p>
187
199
  }
188
200
  `
189
201
 
@@ -193,3 +205,280 @@ describe('Reactive factory inlining (#931)', () => {
193
205
  expect(bf110).toBeUndefined()
194
206
  })
195
207
  })
208
+
209
+ describe('Object-return reactive factories (#2325)', () => {
210
+ test('object-return factory: full destructure { count, setCount }', () => {
211
+ const source = `
212
+ 'use client'
213
+ import { createSignal } from '@barefootjs/client'
214
+
215
+ function createCounter(initial: number) {
216
+ const [count, setCount] = createSignal(initial)
217
+ return { count, setCount }
218
+ }
219
+
220
+ export function Counter() {
221
+ const { count, setCount } = createCounter(0)
222
+ return <button onClick={() => setCount(count() + 1)}>{count()}</button>
223
+ }
224
+ `
225
+
226
+ const ctx = analyzeComponent(source, 'Counter.tsx')
227
+ expect(ctx.signals.length).toBe(1)
228
+ expect(ctx.signals[0].getter).toBe('count')
229
+ expect(ctx.signals[0].setter).toBe('setCount')
230
+
231
+ const result = compileJSX(source, 'Counter.tsx', { adapter })
232
+ expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
233
+ const clientJs = result.files.find(f => f.type === 'clientJs')
234
+ expect(clientJs).toBeDefined()
235
+ expect(clientJs!.content).toContain('createSignal')
236
+ })
237
+
238
+ test('object-return factory: subset destructure suffix-renames the undestructured setter (C4)', () => {
239
+ const source = `
240
+ 'use client'
241
+ import { createSignal } from '@barefootjs/client'
242
+
243
+ function createCounter(initial: number) {
244
+ const [count, setCount] = createSignal(initial)
245
+ return { count, setCount }
246
+ }
247
+
248
+ export function Display() {
249
+ const { count } = createCounter(0)
250
+ return <p>{count()}</p>
251
+ }
252
+ `
253
+
254
+ const ctx = analyzeComponent(source, 'Display.tsx')
255
+ expect(ctx.signals.length).toBe(1)
256
+ expect(ctx.signals[0].getter).toBe('count')
257
+
258
+ const result = compileJSX(source, 'Display.tsx', { adapter })
259
+ expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
260
+ const clientJs = result.files.find(f => f.type === 'clientJs')!.content
261
+ // The setter was never destructured — it must still be suffix-renamed
262
+ // (not left as a bare, unresolved `setCount`).
263
+ expect(clientJs).toMatch(/setCount_\w+/)
264
+ })
265
+
266
+ test('object-return factory: custom setter wrapper (localStorage-backed signal)', () => {
267
+ // Matches the createPersistentSignal shape from PR #930.
268
+ const source = `
269
+ 'use client'
270
+ import { createSignal } from '@barefootjs/client'
271
+
272
+ function createPersistentSignal(key: string, initial: string) {
273
+ const [v, setV] = createSignal(initial)
274
+ const setAndStore = (next: string) => {
275
+ setV(next)
276
+ localStorage.setItem(key, next)
277
+ }
278
+ return { v, setAndStore }
279
+ }
280
+
281
+ export function Store() {
282
+ const { v, setAndStore } = createPersistentSignal('k', 'hello')
283
+ return <button onClick={() => setAndStore('world')}>{v()}</button>
284
+ }
285
+ `
286
+
287
+ const ctx = analyzeComponent(source, 'Store.tsx')
288
+ expect(ctx.signals.length).toBe(1)
289
+ expect(ctx.signals[0].getter).toBe('v')
290
+
291
+ const result = compileJSX(source, 'Store.tsx', { adapter })
292
+ expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
293
+ const clientJs = result.files.find(f => f.type === 'clientJs')!.content
294
+ expect(clientJs).toContain('createSignal')
295
+ expect(clientJs).toContain("localStorage.setItem('k'")
296
+ })
297
+
298
+ test('object-return factory: two subset-destructure call sites stay hygienic (C4)', () => {
299
+ const source = `
300
+ 'use client'
301
+ import { createSignal } from '@barefootjs/client'
302
+
303
+ function createPair(initial: number) {
304
+ const [count, setCount] = createSignal(initial)
305
+ return { count, setCount }
306
+ }
307
+
308
+ export function Two() {
309
+ const { count } = createPair(0)
310
+ const { setCount } = createPair(10)
311
+ return <p>{count()}</p>
312
+ }
313
+ `
314
+
315
+ const result = compileJSX(source, 'Two.tsx', { adapter })
316
+ expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
317
+ const clientJs = result.files.find(f => f.type === 'clientJs')!.content
318
+ expect(clientJs.match(/createSignal\(/g)?.length).toBe(2)
319
+ // Per-call-site-unique suffixed internal names: the first call site
320
+ // suffix-renames its undestructured `setCount`, the second suffix-
321
+ // renames its undestructured `count` — distinct call sites must not
322
+ // collide on either name.
323
+ expect(clientJs).toMatch(/setCount_bf\d+/)
324
+ expect(clientJs).toMatch(/count_bf\d+/)
325
+ })
326
+
327
+ test('object-return factory: mixed signal + memo body', () => {
328
+ const source = `
329
+ 'use client'
330
+ import { createSignal, createMemo } from '@barefootjs/client'
331
+
332
+ function createCounter(initial: number) {
333
+ const [count, setCount] = createSignal(initial)
334
+ const double = createMemo(() => count() * 2)
335
+ return { count, setCount, double }
336
+ }
337
+
338
+ export function Counter() {
339
+ const { count, setCount, double } = createCounter(0)
340
+ return <button onClick={() => setCount(count() + 1)}>{double()}</button>
341
+ }
342
+ `
343
+
344
+ const ctx = analyzeComponent(source, 'Counter.tsx')
345
+ expect(ctx.signals.length).toBe(1)
346
+ expect(ctx.memos.length).toBe(1)
347
+ expect(ctx.memos[0].name).toBe('double')
348
+
349
+ const result = compileJSX(source, 'Counter.tsx', { adapter })
350
+ expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
351
+ })
352
+
353
+ test('BF110: object destructure of an unknown imported callee emits a diagnostic', () => {
354
+ // Regression-locks the closed silent-failure hole: an unresolvable
355
+ // relative import whose name looks reactive-factory-shaped.
356
+ const source = `
357
+ 'use client'
358
+ import { useExternal } from './external'
359
+
360
+ export function Gadget() {
361
+ const { value, setValue } = useExternal('key')
362
+ return <button onClick={() => setValue('x')}>{value()}</button>
363
+ }
364
+ `
365
+
366
+ const result = compileJSX(source, 'Gadget.tsx', { adapter })
367
+ const bf110 = result.errors.find(e => e.code === 'BF110')
368
+ expect(bf110).toBeDefined()
369
+ expect(bf110!.message).toContain('useExternal')
370
+ })
371
+
372
+ test('BF111: non-shorthand factory return emits a diagnostic, no inlining', () => {
373
+ const source = `
374
+ 'use client'
375
+ import { createSignal } from '@barefootjs/client'
376
+
377
+ function createCounter(initial: number) {
378
+ const [count, setCount] = createSignal(initial)
379
+ return { count: count, setCount }
380
+ }
381
+
382
+ export function Counter() {
383
+ const { count, setCount } = createCounter(0)
384
+ return <button onClick={() => setCount(count() + 1)}>{count()}</button>
385
+ }
386
+ `
387
+
388
+ const ctx = analyzeComponent(source, 'Counter.tsx')
389
+ expect(ctx.signals.length).toBe(0)
390
+
391
+ const result = compileJSX(source, 'Counter.tsx', { adapter })
392
+ const bf111 = result.errors.find(e => e.code === 'BF111')
393
+ expect(bf111).toBeDefined()
394
+ })
395
+
396
+ test('BF111: non-shorthand call-site destructure of a shorthand factory emits a diagnostic, no inlining', () => {
397
+ const source = `
398
+ 'use client'
399
+ import { createSignal } from '@barefootjs/client'
400
+
401
+ function createCounter(initial: number) {
402
+ const [count, setCount] = createSignal(initial)
403
+ return { count, setCount }
404
+ }
405
+
406
+ export function Counter() {
407
+ const { count: c, setCount } = createCounter(0)
408
+ return <button onClick={() => setCount(c() + 1)}>{c()}</button>
409
+ }
410
+ `
411
+
412
+ const ctx = analyzeComponent(source, 'Counter.tsx')
413
+ expect(ctx.signals.length).toBe(0)
414
+
415
+ const result = compileJSX(source, 'Counter.tsx', { adapter })
416
+ const bf111 = result.errors.find(e => e.code === 'BF111')
417
+ expect(bf111).toBeDefined()
418
+ })
419
+
420
+ test('BF110 (not BF111): object destructure with a rename of a TUPLE-return factory reports the tuple mismatch', () => {
421
+ // A tuple-return factory destructured as an object is never valid,
422
+ // regardless of whether the object pattern is shorthand or uses a
423
+ // rename/default/rest element — it should always get the "this
424
+ // factory returns a tuple" BF110 message, not the shorthand-only
425
+ // BF111 guidance (which only makes sense for object-return factories).
426
+ const source = `
427
+ 'use client'
428
+ import { createSignal } from '@barefootjs/client'
429
+
430
+ function createCounter(initial: number) {
431
+ const [count, setCount] = createSignal(initial)
432
+ return [count, setCount] as const
433
+ }
434
+
435
+ export function Counter() {
436
+ const { count: c, setCount } = createCounter(0)
437
+ return <button onClick={() => setCount(c() + 1)}>{c()}</button>
438
+ }
439
+ `
440
+
441
+ const ctx = analyzeComponent(source, 'Counter.tsx')
442
+ expect(ctx.signals.length).toBe(0)
443
+
444
+ const result = compileJSX(source, 'Counter.tsx', { adapter })
445
+ const bf110 = result.errors.find(e => e.code === 'BF110')
446
+ expect(bf110).toBeDefined()
447
+ expect(bf110!.message).toContain('returns a tuple')
448
+ expect(result.errors.find(e => e.code === 'BF111')).toBeUndefined()
449
+ })
450
+
451
+ test('guard: plain-function object destructure is not flagged (false-positive guard)', () => {
452
+ const source = `
453
+ 'use client'
454
+
455
+ function parseConfig() {
456
+ return { data: 42 }
457
+ }
458
+
459
+ export function Comp() {
460
+ const { data } = parseConfig()
461
+ return <p>{data}</p>
462
+ }
463
+ `
464
+
465
+ const result = compileJSX(source, 'Comp.tsx', { adapter })
466
+ expect(result.errors.find(e => e.code === 'BF110')).toBeUndefined()
467
+ expect(result.errors.find(e => e.code === 'BF111')).toBeUndefined()
468
+ })
469
+
470
+ test('guard: object destructure of a @barefootjs-scoped import is not flagged (false-positive guard)', () => {
471
+ const source = `
472
+ 'use client'
473
+ import { loadItems } from '@barefootjs/something'
474
+
475
+ export function Comp() {
476
+ const { items } = loadItems()
477
+ return <p>{items}</p>
478
+ }
479
+ `
480
+
481
+ const result = compileJSX(source, 'Comp.tsx', { adapter })
482
+ expect(result.errors.find(e => e.code === 'BF110')).toBeUndefined()
483
+ })
484
+ })