@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.
- package/dist/adapters/env-signal.d.ts +8 -0
- package/dist/adapters/env-signal.d.ts.map +1 -1
- package/dist/analyzer-context.d.ts +16 -5
- package/dist/analyzer-context.d.ts.map +1 -1
- package/dist/analyzer.d.ts +10 -4
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/builtin-lowering-plugins.d.ts.map +1 -1
- package/dist/date-lowering.d.ts +16 -0
- package/dist/date-lowering.d.ts.map +1 -1
- package/dist/errors.d.ts +3 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/format-date-lowering.d.ts +30 -0
- package/dist/format-date-lowering.d.ts.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1124 -74
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/ir-to-client-js/html-template.d.ts +1 -0
- package/dist/ir-to-client-js/html-template.d.ts.map +1 -1
- package/dist/ir-to-client-js/imports.d.ts +2 -2
- package/dist/ir-to-client-js/imports.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/to-locale-date-lowering.d.ts +111 -0
- package/dist/to-locale-date-lowering.d.ts.map +1 -0
- package/dist/types.d.ts +47 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/format-date-lowering.test.ts +125 -0
- package/src/__tests__/reactive-factory-cross-file.test.ts +502 -0
- package/src/__tests__/reactive-factory-inlining.test.ts +293 -4
- package/src/__tests__/to-locale-date-lowering.test.ts +382 -0
- package/src/adapters/env-signal.ts +26 -3
- package/src/analyzer-context.ts +19 -4
- package/src/analyzer.ts +1012 -93
- package/src/builtin-lowering-plugins.ts +8 -1
- package/src/date-lowering.ts +1 -1
- package/src/errors.ts +19 -0
- package/src/format-date-lowering.ts +55 -0
- package/src/index.ts +1 -1
- package/src/ir-to-client-js/emit-reactive.ts +90 -1
- package/src/ir-to-client-js/html-template.ts +36 -2
- package/src/ir-to-client-js/imports.ts +4 -0
- package/src/jsx-to-ir.ts +90 -1
- package/src/rich-type-refusal.ts +9 -1
- package/src/to-locale-date-lowering.ts +563 -0
- 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
|
|
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
|
-
|
|
191
|
+
|
|
192
|
+
function getConfig() {
|
|
193
|
+
return { x: 1 }
|
|
194
|
+
}
|
|
183
195
|
|
|
184
196
|
export function Comp() {
|
|
185
|
-
const
|
|
186
|
-
return <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
|
+
})
|