@barefootjs/jsx 0.23.0 → 0.25.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.
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/errors.d.ts +2 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/format-date-lowering.d.ts +6 -3
- package/dist/format-date-lowering.d.ts.map +1 -1
- package/dist/index.js +696 -108
- package/dist/ir-to-client-js/emit-reactive.d.ts.map +1 -1
- package/dist/jsx-to-ir.d.ts.map +1 -1
- package/dist/to-locale-date-lowering.d.ts +62 -23
- package/dist/to-locale-date-lowering.d.ts.map +1 -1
- package/dist/types.d.ts +47 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/format-date-lowering.test.ts +22 -6
- package/src/__tests__/reactive-factory-cross-file.test.ts +845 -0
- package/src/__tests__/reactive-factory-inlining.test.ts +527 -1
- package/src/__tests__/reactive-factory-rename-fidelity.test.ts +318 -0
- package/src/__tests__/to-locale-date-lowering.test.ts +203 -2
- package/src/analyzer.ts +730 -131
- package/src/errors.ts +10 -0
- package/src/format-date-lowering.ts +9 -5
- package/src/ir-to-client-js/emit-reactive.ts +14 -4
- package/src/jsx-to-ir.ts +15 -4
- package/src/to-locale-date-lowering.ts +437 -49
- package/src/types.ts +49 -3
|
@@ -237,3 +237,848 @@ export function Config() {
|
|
|
237
237
|
expect(objectResult.errors.find(e => e.code === 'BF111')).toBeUndefined()
|
|
238
238
|
})
|
|
239
239
|
})
|
|
240
|
+
|
|
241
|
+
describe('Cross-file factory import re-provisioning (#2332)', () => {
|
|
242
|
+
beforeAll(() => {
|
|
243
|
+
// lib/mathmod.ts — the third module, deliberately .ts and in a different
|
|
244
|
+
// directory than both hook and component so the relative-path math is real.
|
|
245
|
+
writeFixture('lib/mathmod.ts', `export function doubleIt(x: number): number {
|
|
246
|
+
return x * 2
|
|
247
|
+
}
|
|
248
|
+
`)
|
|
249
|
+
// hooks/useDouble.tsx — the helper: factory body calls an import.
|
|
250
|
+
writeFixture('hooks/useDouble.tsx', `'use client'
|
|
251
|
+
import { createSignal } from '@barefootjs/client'
|
|
252
|
+
import { doubleIt } from '../lib/mathmod'
|
|
253
|
+
|
|
254
|
+
export function useDouble(initial: number) {
|
|
255
|
+
const [value, setValue] = createSignal(doubleIt(initial))
|
|
256
|
+
const bump = () => setValue(doubleIt(value()))
|
|
257
|
+
return { value, bump }
|
|
258
|
+
}
|
|
259
|
+
`)
|
|
260
|
+
})
|
|
261
|
+
|
|
262
|
+
test('matrix 1: re-provisioned import, different directory depths', () => {
|
|
263
|
+
const consumerSource = `'use client'
|
|
264
|
+
import { useDouble } from '../hooks/useDouble'
|
|
265
|
+
|
|
266
|
+
export function Doubler() {
|
|
267
|
+
const { value, bump } = useDouble(21)
|
|
268
|
+
return <button onClick={bump}>{value()}</button>
|
|
269
|
+
}
|
|
270
|
+
`
|
|
271
|
+
const consumerPath = writeFixture('components/Doubler.tsx', consumerSource)
|
|
272
|
+
const ctx = analyzeComponent(consumerSource, consumerPath, 'Doubler')
|
|
273
|
+
expect(ctx.signals.length).toBe(1)
|
|
274
|
+
expect(ctx.signals[0].getter).toBe('value')
|
|
275
|
+
|
|
276
|
+
const result = compileJSX(consumerSource, consumerPath, { adapter })
|
|
277
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
278
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
279
|
+
expect(clientJs).toBeDefined()
|
|
280
|
+
// Specifier rewritten relative to components/, not hooks/ — the off-by-one trap.
|
|
281
|
+
expect(clientJs!.content).toMatch(/import\s*\{\s*doubleIt\s*\}\s*from\s*'\.\.\/lib\/mathmod'/)
|
|
282
|
+
expect(clientJs!.content).not.toContain('../hooks/useDouble') // factory inlined, not imported
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
test('matrix 2: bare specifier passes through unchanged', () => {
|
|
286
|
+
writeFixture('hooks/useClamped.tsx', `'use client'
|
|
287
|
+
import { createSignal } from '@barefootjs/client'
|
|
288
|
+
import { clamp } from 'tiny-clamp'
|
|
289
|
+
|
|
290
|
+
export function useClamped(initial: number) {
|
|
291
|
+
const [value, setValue] = createSignal(clamp(initial))
|
|
292
|
+
const bump = () => setValue(clamp(value() + 1))
|
|
293
|
+
return { value, bump }
|
|
294
|
+
}
|
|
295
|
+
`)
|
|
296
|
+
const consumerSource = `'use client'
|
|
297
|
+
import { useClamped } from '../hooks/useClamped'
|
|
298
|
+
|
|
299
|
+
export function Clamped() {
|
|
300
|
+
const { value, bump } = useClamped(21)
|
|
301
|
+
return <button onClick={bump}>{value()}</button>
|
|
302
|
+
}
|
|
303
|
+
`
|
|
304
|
+
const consumerPath = writeFixture('components/Clamped.tsx', consumerSource)
|
|
305
|
+
const result = compileJSX(consumerSource, consumerPath, { adapter })
|
|
306
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
307
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
308
|
+
expect(clientJs).toBeDefined()
|
|
309
|
+
// Bare specifiers skip resolution entirely, so the package need not exist on disk.
|
|
310
|
+
expect(clientJs!.content).toMatch(/import\s*\{\s*clamp\s*\}\s*from\s*'tiny-clamp'/)
|
|
311
|
+
})
|
|
312
|
+
|
|
313
|
+
test('matrix 3: dedupe across two factories sharing the same helper import', () => {
|
|
314
|
+
writeFixture('hooks/pair.tsx', `'use client'
|
|
315
|
+
import { createSignal } from '@barefootjs/client'
|
|
316
|
+
import { doubleIt } from '../lib/mathmod'
|
|
317
|
+
|
|
318
|
+
export function useA(initial: number) {
|
|
319
|
+
const [value, setValue] = createSignal(doubleIt(initial))
|
|
320
|
+
const bump = () => setValue(doubleIt(value()))
|
|
321
|
+
return { value, bump }
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
export function useB(initial: number) {
|
|
325
|
+
const [other, setOther] = createSignal(doubleIt(initial))
|
|
326
|
+
const bumpOther = () => setOther(doubleIt(other()))
|
|
327
|
+
return { other, bumpOther }
|
|
328
|
+
}
|
|
329
|
+
`)
|
|
330
|
+
const consumerSource = `'use client'
|
|
331
|
+
import { useA, useB } from '../hooks/pair'
|
|
332
|
+
|
|
333
|
+
export function Pair() {
|
|
334
|
+
const { value, bump } = useA(1)
|
|
335
|
+
const { other, bumpOther } = useB(2)
|
|
336
|
+
return <button onClick={() => { bump(); bumpOther() }}>{value()} / {other()}</button>
|
|
337
|
+
}
|
|
338
|
+
`
|
|
339
|
+
const consumerPath = writeFixture('components/Pair.tsx', consumerSource)
|
|
340
|
+
const result = compileJSX(consumerSource, consumerPath, { adapter })
|
|
341
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
342
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
343
|
+
expect(clientJs).toBeDefined()
|
|
344
|
+
expect((clientJs!.content.match(/from '\.\.\/lib\/mathmod'/g) ?? []).length).toBe(1)
|
|
345
|
+
})
|
|
346
|
+
|
|
347
|
+
test('re-provisioned imports are emitted in sorted order regardless of inlining order (Copilot review, PR #2338)', () => {
|
|
348
|
+
// `importsBySpecifier`/`inlinedFactories` are populated in AST-traversal/
|
|
349
|
+
// insertion order — deliberately adversarial here: the component calls
|
|
350
|
+
// the factory needing '../lib/zmod' BEFORE the one needing
|
|
351
|
+
// '../lib/amod', so insertion order is z-then-a. Emission must still be
|
|
352
|
+
// alphabetical (a-then-z), or the generated import block would be
|
|
353
|
+
// order-unstable across unrelated analyzer refactors.
|
|
354
|
+
writeFixture('lib/zmod.ts', `export function zHelper(x: number): number {
|
|
355
|
+
return x
|
|
356
|
+
}
|
|
357
|
+
`)
|
|
358
|
+
writeFixture('lib/amod.ts', `export function aHelper(x: number): number {
|
|
359
|
+
return x
|
|
360
|
+
}
|
|
361
|
+
`)
|
|
362
|
+
writeFixture('hooks/useZFactory.tsx', `'use client'
|
|
363
|
+
import { createSignal } from '@barefootjs/client'
|
|
364
|
+
import { zHelper } from '../lib/zmod'
|
|
365
|
+
|
|
366
|
+
export function useZFactory(initial: number) {
|
|
367
|
+
const [zValue, setZValue] = createSignal(zHelper(initial))
|
|
368
|
+
return { zValue, setZValue }
|
|
369
|
+
}
|
|
370
|
+
`)
|
|
371
|
+
writeFixture('hooks/useAFactory.tsx', `'use client'
|
|
372
|
+
import { createSignal } from '@barefootjs/client'
|
|
373
|
+
import { aHelper } from '../lib/amod'
|
|
374
|
+
|
|
375
|
+
export function useAFactory(initial: number) {
|
|
376
|
+
const [aValue, setAValue] = createSignal(aHelper(initial))
|
|
377
|
+
return { aValue, setAValue }
|
|
378
|
+
}
|
|
379
|
+
`)
|
|
380
|
+
const consumerSource = `'use client'
|
|
381
|
+
import { useZFactory } from '../hooks/useZFactory'
|
|
382
|
+
import { useAFactory } from '../hooks/useAFactory'
|
|
383
|
+
|
|
384
|
+
export function Ordered() {
|
|
385
|
+
const { zValue } = useZFactory(1)
|
|
386
|
+
const { aValue } = useAFactory(2)
|
|
387
|
+
return <p>{zValue()} / {aValue()}</p>
|
|
388
|
+
}
|
|
389
|
+
`
|
|
390
|
+
const consumerPath = writeFixture('components/Ordered.tsx', consumerSource)
|
|
391
|
+
const result = compileJSX(consumerSource, consumerPath, { adapter })
|
|
392
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
393
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
394
|
+
expect(clientJs).toBeDefined()
|
|
395
|
+
const amodIndex = clientJs!.content.indexOf("from '../lib/amod'")
|
|
396
|
+
const zmodIndex = clientJs!.content.indexOf("from '../lib/zmod'")
|
|
397
|
+
expect(amodIndex).toBeGreaterThan(-1)
|
|
398
|
+
expect(zmodIndex).toBeGreaterThan(-1)
|
|
399
|
+
expect(amodIndex).toBeLessThan(zmodIndex)
|
|
400
|
+
})
|
|
401
|
+
|
|
402
|
+
test('matrix 4: local-name collision with a re-provisioned import declines with BF113', () => {
|
|
403
|
+
const consumerSource = `'use client'
|
|
404
|
+
import { useDouble } from '../hooks/useDouble'
|
|
405
|
+
|
|
406
|
+
function doubleIt(): number {
|
|
407
|
+
return 1
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export function Collide() {
|
|
411
|
+
const { value, bump } = useDouble(21)
|
|
412
|
+
return <button onClick={bump}>{value()} {doubleIt()}</button>
|
|
413
|
+
}
|
|
414
|
+
`
|
|
415
|
+
const consumerPath = writeFixture('components/Collide.tsx', consumerSource)
|
|
416
|
+
const ctx = analyzeComponent(consumerSource, consumerPath, 'Collide')
|
|
417
|
+
expect(ctx.signals.length).toBe(0)
|
|
418
|
+
|
|
419
|
+
const result = compileJSX(consumerSource, consumerPath, { adapter })
|
|
420
|
+
const bf113 = result.errors.find(e => e.code === 'BF113')
|
|
421
|
+
expect(bf113).toBeDefined()
|
|
422
|
+
expect(bf113!.message).toContain('doubleIt')
|
|
423
|
+
expect(bf113!.message).toContain('../lib/mathmod')
|
|
424
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
425
|
+
if (clientJs) {
|
|
426
|
+
expect(clientJs.content).not.toContain(`from '../lib/mathmod'`)
|
|
427
|
+
}
|
|
428
|
+
})
|
|
429
|
+
|
|
430
|
+
test('matrix 5b: default-import reference stays BF112, not re-provisioned', () => {
|
|
431
|
+
writeFixture('lib/mathmod-default.ts', `export default {
|
|
432
|
+
doubleIt(x: number): number {
|
|
433
|
+
return x * 2
|
|
434
|
+
},
|
|
435
|
+
}
|
|
436
|
+
`)
|
|
437
|
+
writeFixture('hooks/useDefault.tsx', `'use client'
|
|
438
|
+
import { createSignal } from '@barefootjs/client'
|
|
439
|
+
import mathmod from '../lib/mathmod-default'
|
|
440
|
+
|
|
441
|
+
export function useDefault(initial: number) {
|
|
442
|
+
const [value, setValue] = createSignal(mathmod.doubleIt(initial))
|
|
443
|
+
const bump = () => setValue(mathmod.doubleIt(value()))
|
|
444
|
+
return { value, bump }
|
|
445
|
+
}
|
|
446
|
+
`)
|
|
447
|
+
const consumerSource = `'use client'
|
|
448
|
+
import { useDefault } from '../hooks/useDefault'
|
|
449
|
+
|
|
450
|
+
export function Defaulter() {
|
|
451
|
+
const { value, bump } = useDefault(21)
|
|
452
|
+
return <button onClick={bump}>{value()}</button>
|
|
453
|
+
}
|
|
454
|
+
`
|
|
455
|
+
const consumerPath = writeFixture('components/Defaulter.tsx', consumerSource)
|
|
456
|
+
const result = compileJSX(consumerSource, consumerPath, { adapter })
|
|
457
|
+
const bf112 = result.errors.find(e => e.code === 'BF112')
|
|
458
|
+
expect(bf112).toBeDefined()
|
|
459
|
+
expect(bf112!.message).toContain('mathmod')
|
|
460
|
+
expect(result.errors.find(e => e.code === 'BF113')).toBeUndefined()
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
test('matrix 6: SSR output reflects the re-provisioned import', () => {
|
|
464
|
+
const consumerSource = `'use client'
|
|
465
|
+
import { useDouble } from '../hooks/useDouble'
|
|
466
|
+
|
|
467
|
+
export function DoublerSSR() {
|
|
468
|
+
const { value, bump } = useDouble(21)
|
|
469
|
+
return <button onClick={bump}>{value()}</button>
|
|
470
|
+
}
|
|
471
|
+
`
|
|
472
|
+
const consumerPath = writeFixture('components/DoublerSSR.tsx', consumerSource)
|
|
473
|
+
const result = compileJSX(consumerSource, consumerPath, { adapter })
|
|
474
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
475
|
+
const template = result.files.find(f => f.type === 'markedTemplate')
|
|
476
|
+
expect(template).toBeDefined()
|
|
477
|
+
// The injected import is part of the same rewritten source both passes read:
|
|
478
|
+
// the adapter re-emits it via metadata.templateImports…
|
|
479
|
+
expect(template!.content).toMatch(/import\s*\{\s*doubleIt\s*\}\s*from\s*'\.\.\/lib\/mathmod'/)
|
|
480
|
+
// …and the inlined signal initializer calls it at SSR render.
|
|
481
|
+
expect(template!.content).toContain('doubleIt(')
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
test('already-satisfied import dedupes: no BF113, no duplicate declaration', () => {
|
|
485
|
+
const consumerSource = `'use client'
|
|
486
|
+
import { useDouble } from '../hooks/useDouble'
|
|
487
|
+
import { doubleIt } from '../lib/mathmod'
|
|
488
|
+
|
|
489
|
+
export function DoublerSelfImporting() {
|
|
490
|
+
const { value, bump } = useDouble(21)
|
|
491
|
+
return <button onClick={() => { bump(); doubleIt(value()) }}>{value()}</button>
|
|
492
|
+
}
|
|
493
|
+
`
|
|
494
|
+
const consumerPath = writeFixture('components/DoublerSelfImporting.tsx', consumerSource)
|
|
495
|
+
const result = compileJSX(consumerSource, consumerPath, { adapter })
|
|
496
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
497
|
+
expect(result.errors.find(e => e.code === 'BF113')).toBeUndefined()
|
|
498
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
499
|
+
expect(clientJs).toBeDefined()
|
|
500
|
+
expect((clientJs!.content.match(/from '\.\.\/lib\/mathmod'/g) ?? []).length).toBe(1)
|
|
501
|
+
})
|
|
502
|
+
})
|
|
503
|
+
|
|
504
|
+
describe('Barrel re-exports (#2341 BUG-2)', () => {
|
|
505
|
+
beforeAll(() => {
|
|
506
|
+
writeFixture('barrel/useToggle.tsx', `'use client'
|
|
507
|
+
import { createSignal } from '@barefootjs/client'
|
|
508
|
+
|
|
509
|
+
export function useToggle(initial: boolean) {
|
|
510
|
+
const [on, setOn] = createSignal(initial)
|
|
511
|
+
return [on, setOn] as const
|
|
512
|
+
}
|
|
513
|
+
`)
|
|
514
|
+
writeFixture('barrel/index.ts', `export { useToggle } from './useToggle'
|
|
515
|
+
`)
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
test('B1: factory reached through a barrel index.ts inlines (issue repro)', () => {
|
|
519
|
+
// `../barrel` has no extension and is not itself a file — this also
|
|
520
|
+
// exercises directory-index resolution (`./hooks` -> `hooks/index.ts`),
|
|
521
|
+
// already supported by `resolveRelativeImportToFile`.
|
|
522
|
+
const consumerSource = `'use client'
|
|
523
|
+
import { useToggle } from '../barrel'
|
|
524
|
+
|
|
525
|
+
export function Switch() {
|
|
526
|
+
const [on, setOn] = useToggle(false)
|
|
527
|
+
return <button onClick={() => setOn(!on())}>{on() ? 'on' : 'off'}</button>
|
|
528
|
+
}
|
|
529
|
+
`
|
|
530
|
+
const consumerPath = writeFixture('barrel-consumer/Switch.tsx', consumerSource)
|
|
531
|
+
|
|
532
|
+
const ctx = analyzeComponent(consumerSource, consumerPath, 'Switch')
|
|
533
|
+
expect(ctx.signals.length).toBe(1)
|
|
534
|
+
expect(ctx.signals[0].getter).toBe('on')
|
|
535
|
+
|
|
536
|
+
const result = compileJSX(consumerSource, consumerPath, { adapter })
|
|
537
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
538
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
539
|
+
expect(clientJs).toBeDefined()
|
|
540
|
+
expect(clientJs!.content).toContain('createSignal')
|
|
541
|
+
expect(clientJs!.content).not.toContain('../barrel')
|
|
542
|
+
expect(clientJs!.content).toMatch(/import\s*\{[^}]*createSignal[^}]*\}\s*from\s*'@barefootjs\/client\/runtime'/)
|
|
543
|
+
})
|
|
544
|
+
|
|
545
|
+
test('B2: barrel alias (export { useToggle as useFlip } from) inlines', () => {
|
|
546
|
+
writeFixture('barrel-alias/index.ts', `export { useToggle as useFlip } from '../barrel/useToggle'
|
|
547
|
+
`)
|
|
548
|
+
const consumerSource = `'use client'
|
|
549
|
+
import { useFlip } from '../barrel-alias'
|
|
550
|
+
|
|
551
|
+
export function FlipSwitch() {
|
|
552
|
+
const [on, setOn] = useFlip(false)
|
|
553
|
+
return <button onClick={() => setOn(!on())}>{on() ? 'on' : 'off'}</button>
|
|
554
|
+
}
|
|
555
|
+
`
|
|
556
|
+
const consumerPath = writeFixture('barrel-consumer/FlipSwitch.tsx', consumerSource)
|
|
557
|
+
|
|
558
|
+
const ctx = analyzeComponent(consumerSource, consumerPath, 'FlipSwitch')
|
|
559
|
+
expect(ctx.signals.length).toBe(1)
|
|
560
|
+
|
|
561
|
+
const result = compileJSX(consumerSource, consumerPath, { adapter })
|
|
562
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
test('B3: barrel alias + consumer alias both resolve through the hop', () => {
|
|
566
|
+
const consumerSource = `'use client'
|
|
567
|
+
import { useFlip as flip } from '../barrel-alias'
|
|
568
|
+
|
|
569
|
+
export function FlipSwitch2() {
|
|
570
|
+
const [on, setOn] = flip(false)
|
|
571
|
+
return <button onClick={() => setOn(!on())}>{on() ? 'on' : 'off'}</button>
|
|
572
|
+
}
|
|
573
|
+
`
|
|
574
|
+
const consumerPath = writeFixture('barrel-consumer/FlipSwitch2.tsx', consumerSource)
|
|
575
|
+
|
|
576
|
+
const result = compileJSX(consumerSource, consumerPath, { adapter })
|
|
577
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
578
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
579
|
+
expect(clientJs).toBeDefined()
|
|
580
|
+
expect(clientJs!.content).toContain('createSignal')
|
|
581
|
+
})
|
|
582
|
+
|
|
583
|
+
test('B4: export * from stays loud (BF110), never silently marked clean', () => {
|
|
584
|
+
writeFixture('barrel-star/index.ts', `export * from '../barrel/useToggle'
|
|
585
|
+
`)
|
|
586
|
+
const consumerSource = `'use client'
|
|
587
|
+
import { useToggle } from '../barrel-star'
|
|
588
|
+
|
|
589
|
+
export function StarConsumer() {
|
|
590
|
+
const [on, setOn] = useToggle(false)
|
|
591
|
+
return <button onClick={() => setOn(!on())}>{on() ? 'on' : 'off'}</button>
|
|
592
|
+
}
|
|
593
|
+
`
|
|
594
|
+
const consumerPath = writeFixture('barrel-consumer/StarConsumer.tsx', consumerSource)
|
|
595
|
+
|
|
596
|
+
const result = compileJSX(consumerSource, consumerPath, { adapter })
|
|
597
|
+
const bf110 = result.errors.find(e => e.code === 'BF110')
|
|
598
|
+
expect(bf110).toBeDefined()
|
|
599
|
+
expect(bf110!.message).toContain('useToggle')
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
test('B5: self-referential barrel does not loop', () => {
|
|
603
|
+
writeFixture('loop/index.ts', `export { useToggle } from './index'
|
|
604
|
+
`)
|
|
605
|
+
const consumerSource = `'use client'
|
|
606
|
+
import { useToggle } from '../loop'
|
|
607
|
+
|
|
608
|
+
export function LoopConsumer() {
|
|
609
|
+
const [on, setOn] = useToggle(false)
|
|
610
|
+
return <button onClick={() => setOn(!on())}>{on() ? 'on' : 'off'}</button>
|
|
611
|
+
}
|
|
612
|
+
`
|
|
613
|
+
const consumerPath = writeFixture('barrel-consumer/LoopConsumer.tsx', consumerSource)
|
|
614
|
+
|
|
615
|
+
const result = compileJSX(consumerSource, consumerPath, { adapter })
|
|
616
|
+
const bf110 = result.errors.find(e => e.code === 'BF110')
|
|
617
|
+
expect(bf110).toBeDefined()
|
|
618
|
+
})
|
|
619
|
+
|
|
620
|
+
test('B6: unresolvable re-export target stays loud, never cleanFactoryImports-silenced', () => {
|
|
621
|
+
writeFixture('barrel-missing/index.ts', `export { useToggle } from './missing'
|
|
622
|
+
`)
|
|
623
|
+
const consumerSource = `'use client'
|
|
624
|
+
import { useToggle } from '../barrel-missing'
|
|
625
|
+
|
|
626
|
+
export function MissingConsumer() {
|
|
627
|
+
const [on, setOn] = useToggle(false)
|
|
628
|
+
return <button onClick={() => setOn(!on())}>{on() ? 'on' : 'off'}</button>
|
|
629
|
+
}
|
|
630
|
+
`
|
|
631
|
+
const consumerPath = writeFixture('barrel-consumer/MissingConsumer.tsx', consumerSource)
|
|
632
|
+
|
|
633
|
+
const result = compileJSX(consumerSource, consumerPath, { adapter })
|
|
634
|
+
const bf110 = result.errors.find(e => e.code === 'BF110')
|
|
635
|
+
expect(bf110).toBeDefined()
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
test('B7: module-scope capture through a barrel still declines with BF112 (defining-file anchor)', () => {
|
|
639
|
+
// If the capture check were (incorrectly) anchored to the barrel file
|
|
640
|
+
// instead of the file that actually DEFINES the factory, `readStored`/
|
|
641
|
+
// `KEY` (declared only in useStoredCounter.tsx, not in the barrel's
|
|
642
|
+
// index.ts) would never be found as a capture, and the factory would
|
|
643
|
+
// wrongly inline with a dangling `readStored()` reference (#2341 BUG-2).
|
|
644
|
+
writeFixture('barrel-capture/useStoredCounter.tsx', `'use client'
|
|
645
|
+
import { createSignal } from '@barefootjs/client'
|
|
646
|
+
|
|
647
|
+
const KEY = 'stored-value'
|
|
648
|
+
|
|
649
|
+
function readStored() {
|
|
650
|
+
return KEY.length
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
export function useStoredCounter() {
|
|
654
|
+
const [count, setCount] = createSignal(readStored())
|
|
655
|
+
return { count, setCount }
|
|
656
|
+
}
|
|
657
|
+
`)
|
|
658
|
+
writeFixture('barrel-capture/index.ts', `export { useStoredCounter } from './useStoredCounter'
|
|
659
|
+
`)
|
|
660
|
+
const consumerSource = `'use client'
|
|
661
|
+
import { useStoredCounter } from '../barrel-capture'
|
|
662
|
+
|
|
663
|
+
export function CaptureConsumer() {
|
|
664
|
+
const { count, setCount } = useStoredCounter()
|
|
665
|
+
return <button onClick={() => setCount(count() + 1)}>{count()}</button>
|
|
666
|
+
}
|
|
667
|
+
`
|
|
668
|
+
const consumerPath = writeFixture('barrel-consumer/CaptureConsumer.tsx', consumerSource)
|
|
669
|
+
|
|
670
|
+
const ctx = analyzeComponent(consumerSource, consumerPath, 'CaptureConsumer')
|
|
671
|
+
expect(ctx.signals.length).toBe(0)
|
|
672
|
+
|
|
673
|
+
const result = compileJSX(consumerSource, consumerPath, { adapter })
|
|
674
|
+
const bf112 = result.errors.find(e => e.code === 'BF112')
|
|
675
|
+
expect(bf112).toBeDefined()
|
|
676
|
+
expect(bf112!.message).toContain('readStored')
|
|
677
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
678
|
+
if (clientJs) {
|
|
679
|
+
expect(clientJs.content).not.toContain('readStored')
|
|
680
|
+
}
|
|
681
|
+
})
|
|
682
|
+
|
|
683
|
+
test('B8: re-provisioned helper import is anchored to the defining file, not the barrel', () => {
|
|
684
|
+
// The barrel (`hooks2barrel/nested/index.ts`) and the file that
|
|
685
|
+
// actually defines `useDouble` (`hooks2/useDouble.tsx`) live at
|
|
686
|
+
// DIFFERENT depths — anchoring the re-provisioned `doubleIt` import to
|
|
687
|
+
// the barrel's directory instead of the defining file's directory
|
|
688
|
+
// would resolve to a nonexistent path (#2341 BUG-2).
|
|
689
|
+
writeFixture('lib2/mathmod.ts', `export function doubleIt(x: number): number {
|
|
690
|
+
return x * 2
|
|
691
|
+
}
|
|
692
|
+
`)
|
|
693
|
+
writeFixture('hooks2/useDouble.tsx', `'use client'
|
|
694
|
+
import { createSignal } from '@barefootjs/client'
|
|
695
|
+
import { doubleIt } from '../lib2/mathmod'
|
|
696
|
+
|
|
697
|
+
export function useDouble(initial: number) {
|
|
698
|
+
const [value, setValue] = createSignal(doubleIt(initial))
|
|
699
|
+
const bump = () => setValue(doubleIt(value()))
|
|
700
|
+
return { value, bump }
|
|
701
|
+
}
|
|
702
|
+
`)
|
|
703
|
+
writeFixture('hooks2barrel/nested/index.ts', `export { useDouble } from '../../hooks2/useDouble'
|
|
704
|
+
`)
|
|
705
|
+
const consumerSource = `'use client'
|
|
706
|
+
import { useDouble } from '../../hooks2barrel/nested'
|
|
707
|
+
|
|
708
|
+
export function DoubleViaBarrel() {
|
|
709
|
+
const { value, bump } = useDouble(21)
|
|
710
|
+
return <button onClick={bump}>{value()}</button>
|
|
711
|
+
}
|
|
712
|
+
`
|
|
713
|
+
const consumerPath = writeFixture('components/deep/DoubleViaBarrel.tsx', consumerSource)
|
|
714
|
+
|
|
715
|
+
const result = compileJSX(consumerSource, consumerPath, { adapter })
|
|
716
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
717
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
718
|
+
expect(clientJs).toBeDefined()
|
|
719
|
+
expect(clientJs!.content).toMatch(/from\s*'\.\.\/\.\.\/lib2\/mathmod'/)
|
|
720
|
+
expect(clientJs!.content).not.toContain('hooks2barrel')
|
|
721
|
+
})
|
|
722
|
+
|
|
723
|
+
test('B9: two barrel hops exceed MAX_REEXPORT_HOPS and stay loud', () => {
|
|
724
|
+
writeFixture('twohop-b/useToggle.tsx', `'use client'
|
|
725
|
+
import { createSignal } from '@barefootjs/client'
|
|
726
|
+
|
|
727
|
+
export function useToggle(initial: boolean) {
|
|
728
|
+
const [on, setOn] = createSignal(initial)
|
|
729
|
+
return [on, setOn] as const
|
|
730
|
+
}
|
|
731
|
+
`)
|
|
732
|
+
writeFixture('twohop-b/index.ts', `export { useToggle } from './useToggle'
|
|
733
|
+
`)
|
|
734
|
+
writeFixture('twohop-a/index.ts', `export { useToggle } from '../twohop-b'
|
|
735
|
+
`)
|
|
736
|
+
const consumerSource = `'use client'
|
|
737
|
+
import { useToggle } from '../twohop-a'
|
|
738
|
+
|
|
739
|
+
export function TwoHopConsumer() {
|
|
740
|
+
const [on, setOn] = useToggle(false)
|
|
741
|
+
return <button onClick={() => setOn(!on())}>{on() ? 'on' : 'off'}</button>
|
|
742
|
+
}
|
|
743
|
+
`
|
|
744
|
+
const consumerPath = writeFixture('barrel-consumer/TwoHopConsumer.tsx', consumerSource)
|
|
745
|
+
|
|
746
|
+
const result = compileJSX(consumerSource, consumerPath, { adapter })
|
|
747
|
+
const bf110 = result.errors.find(e => e.code === 'BF110')
|
|
748
|
+
expect(bf110).toBeDefined()
|
|
749
|
+
})
|
|
750
|
+
|
|
751
|
+
test('B10: mixed barrel (own factory + re-export) inlines both', () => {
|
|
752
|
+
// Pins exportedFns-before-reexports lookup order: `useLocal` is defined
|
|
753
|
+
// directly in index.ts, `useToggle` only reaches it via a re-export.
|
|
754
|
+
writeFixture('mixed/useToggle.tsx', `'use client'
|
|
755
|
+
import { createSignal } from '@barefootjs/client'
|
|
756
|
+
|
|
757
|
+
export function useToggle(initial: boolean) {
|
|
758
|
+
const [on, setOn] = createSignal(initial)
|
|
759
|
+
return [on, setOn] as const
|
|
760
|
+
}
|
|
761
|
+
`)
|
|
762
|
+
writeFixture('mixed/index.ts', `'use client'
|
|
763
|
+
import { createSignal } from '@barefootjs/client'
|
|
764
|
+
|
|
765
|
+
export function useLocal(initial: number) {
|
|
766
|
+
const [count, setCount] = createSignal(initial)
|
|
767
|
+
return [count, setCount] as const
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
export { useToggle } from './useToggle'
|
|
771
|
+
`)
|
|
772
|
+
const consumerSource = `'use client'
|
|
773
|
+
import { useLocal, useToggle } from '../mixed'
|
|
774
|
+
|
|
775
|
+
export function MixedConsumer() {
|
|
776
|
+
const [count, setCount] = useLocal(0)
|
|
777
|
+
const [on, setOn] = useToggle(false)
|
|
778
|
+
return <button onClick={() => { setCount(count() + 1); setOn(!on()) }}>{count()} {on() ? 'on' : 'off'}</button>
|
|
779
|
+
}
|
|
780
|
+
`
|
|
781
|
+
const consumerPath = writeFixture('barrel-consumer/MixedConsumer.tsx', consumerSource)
|
|
782
|
+
|
|
783
|
+
const ctx = analyzeComponent(consumerSource, consumerPath, 'MixedConsumer')
|
|
784
|
+
expect(ctx.signals.length).toBe(2)
|
|
785
|
+
|
|
786
|
+
const result = compileJSX(consumerSource, consumerPath, { adapter })
|
|
787
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
788
|
+
})
|
|
789
|
+
})
|
|
790
|
+
|
|
791
|
+
describe('Real-world factory matrix, cross-file (#2341)', () => {
|
|
792
|
+
test('M15: deep relative paths resolve and inline', () => {
|
|
793
|
+
writeFixture('deep/hooks/state/useCounter.tsx', `'use client'
|
|
794
|
+
import { createSignal } from '@barefootjs/client'
|
|
795
|
+
|
|
796
|
+
export function useCounter(initial: number) {
|
|
797
|
+
const [count, setCount] = createSignal(initial)
|
|
798
|
+
return [count, setCount] as const
|
|
799
|
+
}
|
|
800
|
+
`)
|
|
801
|
+
const consumerSource = `'use client'
|
|
802
|
+
import { useCounter } from '../../hooks/state/useCounter'
|
|
803
|
+
|
|
804
|
+
export function Counter() {
|
|
805
|
+
const [count, setCount] = useCounter(0)
|
|
806
|
+
return <button onClick={() => setCount(count() + 1)}>{count()}</button>
|
|
807
|
+
}
|
|
808
|
+
`
|
|
809
|
+
const consumerPath = writeFixture('deep/components/pages/Counter.tsx', consumerSource)
|
|
810
|
+
|
|
811
|
+
const result = compileJSX(consumerSource, consumerPath, { adapter })
|
|
812
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
813
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
814
|
+
expect(clientJs).toBeDefined()
|
|
815
|
+
expect(clientJs!.content).toContain('createSignal')
|
|
816
|
+
})
|
|
817
|
+
|
|
818
|
+
test('M16: two factories from two different modules compose in one component', () => {
|
|
819
|
+
writeFixture('compose/useA.tsx', `'use client'
|
|
820
|
+
import { createSignal } from '@barefootjs/client'
|
|
821
|
+
|
|
822
|
+
export function useA(initial: number) {
|
|
823
|
+
const [a, setA] = createSignal(initial)
|
|
824
|
+
return [a, setA] as const
|
|
825
|
+
}
|
|
826
|
+
`)
|
|
827
|
+
writeFixture('compose/useB.tsx', `'use client'
|
|
828
|
+
import { createSignal } from '@barefootjs/client'
|
|
829
|
+
|
|
830
|
+
export function useB(initial: number) {
|
|
831
|
+
const [b, setB] = createSignal(initial)
|
|
832
|
+
return [b, setB] as const
|
|
833
|
+
}
|
|
834
|
+
`)
|
|
835
|
+
const consumerSource = `'use client'
|
|
836
|
+
import { useA } from './useA'
|
|
837
|
+
import { useB } from './useB'
|
|
838
|
+
|
|
839
|
+
export function Composed() {
|
|
840
|
+
const [a, setA] = useA(1)
|
|
841
|
+
const [b, setB] = useB(2)
|
|
842
|
+
return <button onClick={() => { setA(a() + 1); setB(b() + 1) }}>{a()} {b()}</button>
|
|
843
|
+
}
|
|
844
|
+
`
|
|
845
|
+
const consumerPath = writeFixture('compose/Composed.tsx', consumerSource)
|
|
846
|
+
|
|
847
|
+
const result = compileJSX(consumerSource, consumerPath, { adapter })
|
|
848
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
849
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
850
|
+
expect(clientJs).toBeDefined()
|
|
851
|
+
expect(clientJs!.content.match(/createSignal\(/g)?.length).toBe(2)
|
|
852
|
+
})
|
|
853
|
+
|
|
854
|
+
test('M17: onMount is provisioned from usage for a cross-file factory', () => {
|
|
855
|
+
writeFixture('matrix17/useTick.tsx', `'use client'
|
|
856
|
+
import { createSignal, onMount } from '@barefootjs/client'
|
|
857
|
+
|
|
858
|
+
export function useTick(initial: number) {
|
|
859
|
+
const [tick, setTick] = createSignal(initial)
|
|
860
|
+
onMount(() => setTick(initial))
|
|
861
|
+
return [tick, setTick] as const
|
|
862
|
+
}
|
|
863
|
+
`)
|
|
864
|
+
const consumerSource = `'use client'
|
|
865
|
+
import { useTick } from './useTick'
|
|
866
|
+
|
|
867
|
+
export function Ticker() {
|
|
868
|
+
const [tick, setTick] = useTick(0)
|
|
869
|
+
return <button onClick={() => setTick(tick() + 1)}>{tick()}</button>
|
|
870
|
+
}
|
|
871
|
+
`
|
|
872
|
+
const consumerPath = writeFixture('matrix17/Ticker.tsx', consumerSource)
|
|
873
|
+
|
|
874
|
+
const result = compileJSX(consumerSource, consumerPath, { adapter })
|
|
875
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
876
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
877
|
+
expect(clientJs).toBeDefined()
|
|
878
|
+
expect(clientJs!.content).toMatch(/import\s*\{[^}]*onMount[^}]*\}\s*from\s*'@barefootjs\/client\/runtime'/)
|
|
879
|
+
})
|
|
880
|
+
|
|
881
|
+
test('M18: a type-only helper import does not trigger BF112 and is not re-provisioned', () => {
|
|
882
|
+
writeFixture('matrix18/todo-types.ts', `export interface Todo {
|
|
883
|
+
id: number
|
|
884
|
+
text: string
|
|
885
|
+
}
|
|
886
|
+
`)
|
|
887
|
+
writeFixture('matrix18/useTodos.tsx', `'use client'
|
|
888
|
+
import { createSignal } from '@barefootjs/client'
|
|
889
|
+
import type { Todo } from './todo-types'
|
|
890
|
+
|
|
891
|
+
export function useTodos(initial: Todo[]) {
|
|
892
|
+
const [todos, setTodos] = createSignal<Todo[]>(initial)
|
|
893
|
+
return [todos, setTodos] as const
|
|
894
|
+
}
|
|
895
|
+
`)
|
|
896
|
+
const consumerSource = `'use client'
|
|
897
|
+
import { useTodos } from './useTodos'
|
|
898
|
+
|
|
899
|
+
export function TodoList() {
|
|
900
|
+
const [todos, setTodos] = useTodos([])
|
|
901
|
+
return <button onClick={() => setTodos([])}>{todos().length}</button>
|
|
902
|
+
}
|
|
903
|
+
`
|
|
904
|
+
const consumerPath = writeFixture('matrix18/TodoList.tsx', consumerSource)
|
|
905
|
+
|
|
906
|
+
const result = compileJSX(consumerSource, consumerPath, { adapter })
|
|
907
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
908
|
+
expect(result.errors.find(e => e.code === 'BF112')).toBeUndefined()
|
|
909
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
910
|
+
expect(clientJs).toBeDefined()
|
|
911
|
+
expect(clientJs!.content).not.toContain('./todo-types')
|
|
912
|
+
})
|
|
913
|
+
|
|
914
|
+
test('M19: a colliding binding nested inside a JSX callback still triggers BF113', () => {
|
|
915
|
+
// Pins collectEntryBindingNames's depth: the ONLY `doubleIt` binding in
|
|
916
|
+
// the consumer file is declared inside an onClick callback, not at any
|
|
917
|
+
// top level, yet the re-provisioning collision check must still find it
|
|
918
|
+
// (an over-broad scan is required — a narrower one would silently
|
|
919
|
+
// shadow the injected import at runtime instead of declining loudly).
|
|
920
|
+
writeFixture('matrix19/lib/mathmod.ts', `export function doubleIt(x: number): number {
|
|
921
|
+
return x * 2
|
|
922
|
+
}
|
|
923
|
+
`)
|
|
924
|
+
writeFixture('matrix19/hooks/useDouble.tsx', `'use client'
|
|
925
|
+
import { createSignal } from '@barefootjs/client'
|
|
926
|
+
import { doubleIt } from '../lib/mathmod'
|
|
927
|
+
|
|
928
|
+
export function useDouble(initial: number) {
|
|
929
|
+
const [value, setValue] = createSignal(doubleIt(initial))
|
|
930
|
+
const bump = () => setValue(doubleIt(value()))
|
|
931
|
+
return { value, bump }
|
|
932
|
+
}
|
|
933
|
+
`)
|
|
934
|
+
const consumerSource = `'use client'
|
|
935
|
+
import { useDouble } from '../hooks/useDouble'
|
|
936
|
+
|
|
937
|
+
export function Collide() {
|
|
938
|
+
const { value, bump } = useDouble(21)
|
|
939
|
+
return <button onClick={() => { const doubleIt = 1; bump(); console.log(doubleIt) }}>{value()}</button>
|
|
940
|
+
}
|
|
941
|
+
`
|
|
942
|
+
const consumerPath = writeFixture('matrix19/components/Collide.tsx', consumerSource)
|
|
943
|
+
|
|
944
|
+
const result = compileJSX(consumerSource, consumerPath, { adapter })
|
|
945
|
+
const bf113 = result.errors.find(e => e.code === 'BF113')
|
|
946
|
+
expect(bf113).toBeDefined()
|
|
947
|
+
expect(bf113!.message).toContain('doubleIt')
|
|
948
|
+
})
|
|
949
|
+
|
|
950
|
+
test('M20: 3 call sites of one imported factory with distinct tuple caller names', () => {
|
|
951
|
+
writeFixture('matrix20/useCounter.tsx', `'use client'
|
|
952
|
+
import { createSignal } from '@barefootjs/client'
|
|
953
|
+
|
|
954
|
+
export function useCounter(initial: number) {
|
|
955
|
+
const [count, setCount] = createSignal(initial)
|
|
956
|
+
return [count, setCount] as const
|
|
957
|
+
}
|
|
958
|
+
`)
|
|
959
|
+
const consumerSource = `'use client'
|
|
960
|
+
import { useCounter } from './useCounter'
|
|
961
|
+
|
|
962
|
+
export function Triple() {
|
|
963
|
+
const [a, setA] = useCounter(1)
|
|
964
|
+
const [b, setB] = useCounter(2)
|
|
965
|
+
const [c, setC] = useCounter(3)
|
|
966
|
+
return <button onClick={() => { setA(a() + 1); setB(b() + 1); setC(c() + 1) }}>{a()} {b()} {c()}</button>
|
|
967
|
+
}
|
|
968
|
+
`
|
|
969
|
+
const consumerPath = writeFixture('matrix20/Triple.tsx', consumerSource)
|
|
970
|
+
|
|
971
|
+
const result = compileJSX(consumerSource, consumerPath, { adapter })
|
|
972
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
973
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
974
|
+
expect(clientJs).toBeDefined()
|
|
975
|
+
expect(clientJs!.content.match(/createSignal\(/g)?.length).toBe(3)
|
|
976
|
+
for (const name of ['a', 'setA', 'b', 'setB', 'c', 'setC']) {
|
|
977
|
+
expect(clientJs!.content).toContain(name)
|
|
978
|
+
}
|
|
979
|
+
})
|
|
980
|
+
|
|
981
|
+
test('M21: SSR through a barrel reflects the re-provisioned import (mirrors matrix 6)', () => {
|
|
982
|
+
writeFixture('matrix21/lib2/mathmod.ts', `export function doubleIt(x: number): number {
|
|
983
|
+
return x * 2
|
|
984
|
+
}
|
|
985
|
+
`)
|
|
986
|
+
writeFixture('matrix21/hooks2/useDouble.tsx', `'use client'
|
|
987
|
+
import { createSignal } from '@barefootjs/client'
|
|
988
|
+
import { doubleIt } from '../lib2/mathmod'
|
|
989
|
+
|
|
990
|
+
export function useDouble(initial: number) {
|
|
991
|
+
const [value, setValue] = createSignal(doubleIt(initial))
|
|
992
|
+
const bump = () => setValue(doubleIt(value()))
|
|
993
|
+
return { value, bump }
|
|
994
|
+
}
|
|
995
|
+
`)
|
|
996
|
+
writeFixture('matrix21/hooks2barrel/index.ts', `export { useDouble } from '../hooks2/useDouble'
|
|
997
|
+
`)
|
|
998
|
+
const consumerSource = `'use client'
|
|
999
|
+
import { useDouble } from '../hooks2barrel'
|
|
1000
|
+
|
|
1001
|
+
export function DoublerSSR() {
|
|
1002
|
+
const { value, bump } = useDouble(21)
|
|
1003
|
+
return <button onClick={bump}>{value()}</button>
|
|
1004
|
+
}
|
|
1005
|
+
`
|
|
1006
|
+
const consumerPath = writeFixture('matrix21/components/DoublerSSR.tsx', consumerSource)
|
|
1007
|
+
|
|
1008
|
+
const result = compileJSX(consumerSource, consumerPath, { adapter })
|
|
1009
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
1010
|
+
const template = result.files.find(f => f.type === 'markedTemplate')
|
|
1011
|
+
expect(template).toBeDefined()
|
|
1012
|
+
expect(template!.content).toMatch(/import\s*\{\s*doubleIt\s*\}\s*from\s*'\.\.\/lib2\/mathmod'/)
|
|
1013
|
+
expect(template!.content).toContain('doubleIt(')
|
|
1014
|
+
})
|
|
1015
|
+
|
|
1016
|
+
test('M22: aliased factory import + aliased helper import both resolve correctly', () => {
|
|
1017
|
+
writeFixture('matrix22/lib/mathmod.ts', `export function doubleIt(x: number): number {
|
|
1018
|
+
return x * 2
|
|
1019
|
+
}
|
|
1020
|
+
`)
|
|
1021
|
+
writeFixture('matrix22/hooks/useDouble.tsx', `'use client'
|
|
1022
|
+
import { createSignal } from '@barefootjs/client'
|
|
1023
|
+
import { doubleIt as dbl } from '../lib/mathmod'
|
|
1024
|
+
|
|
1025
|
+
export function useDouble(initial: number) {
|
|
1026
|
+
const [value, setValue] = createSignal(dbl(initial))
|
|
1027
|
+
const bump = () => setValue(dbl(value()))
|
|
1028
|
+
return { value, bump }
|
|
1029
|
+
}
|
|
1030
|
+
`)
|
|
1031
|
+
const consumerSource = `'use client'
|
|
1032
|
+
import { useDouble as useDoubleAliased } from '../hooks/useDouble'
|
|
1033
|
+
|
|
1034
|
+
export function Doubler() {
|
|
1035
|
+
const { value, bump } = useDoubleAliased(21)
|
|
1036
|
+
return <button onClick={bump}>{value()}</button>
|
|
1037
|
+
}
|
|
1038
|
+
`
|
|
1039
|
+
const consumerPath = writeFixture('matrix22/components/Doubler.tsx', consumerSource)
|
|
1040
|
+
|
|
1041
|
+
const result = compileJSX(consumerSource, consumerPath, { adapter })
|
|
1042
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
1043
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
1044
|
+
expect(clientJs).toBeDefined()
|
|
1045
|
+
// The re-provisioned import preserves the HELPER file's own local alias.
|
|
1046
|
+
expect(clientJs!.content).toMatch(/import\s*\{\s*doubleIt as dbl\s*\}/)
|
|
1047
|
+
})
|
|
1048
|
+
|
|
1049
|
+
test('M23: an already-satisfied import through a barrel dedupes (no BF113, one occurrence)', () => {
|
|
1050
|
+
writeFixture('matrix23/lib/mathmod.ts', `export function doubleIt(x: number): number {
|
|
1051
|
+
return x * 2
|
|
1052
|
+
}
|
|
1053
|
+
`)
|
|
1054
|
+
writeFixture('matrix23/hooks/useDouble.tsx', `'use client'
|
|
1055
|
+
import { createSignal } from '@barefootjs/client'
|
|
1056
|
+
import { doubleIt } from '../lib/mathmod'
|
|
1057
|
+
|
|
1058
|
+
export function useDouble(initial: number) {
|
|
1059
|
+
const [value, setValue] = createSignal(doubleIt(initial))
|
|
1060
|
+
const bump = () => setValue(doubleIt(value()))
|
|
1061
|
+
return { value, bump }
|
|
1062
|
+
}
|
|
1063
|
+
`)
|
|
1064
|
+
writeFixture('matrix23/hooks/index.ts', `export { useDouble } from './useDouble'
|
|
1065
|
+
`)
|
|
1066
|
+
const consumerSource = `'use client'
|
|
1067
|
+
import { useDouble } from '../hooks'
|
|
1068
|
+
import { doubleIt } from '../lib/mathmod'
|
|
1069
|
+
|
|
1070
|
+
export function DoublerSelfImporting() {
|
|
1071
|
+
const { value, bump } = useDouble(21)
|
|
1072
|
+
return <button onClick={() => { bump(); doubleIt(value()) }}>{value()}</button>
|
|
1073
|
+
}
|
|
1074
|
+
`
|
|
1075
|
+
const consumerPath = writeFixture('matrix23/components/DoublerSelfImporting.tsx', consumerSource)
|
|
1076
|
+
|
|
1077
|
+
const result = compileJSX(consumerSource, consumerPath, { adapter })
|
|
1078
|
+
expect(result.errors.filter(e => e.severity === 'error')).toHaveLength(0)
|
|
1079
|
+
expect(result.errors.find(e => e.code === 'BF113')).toBeUndefined()
|
|
1080
|
+
const clientJs = result.files.find(f => f.type === 'clientJs')
|
|
1081
|
+
expect(clientJs).toBeDefined()
|
|
1082
|
+
expect((clientJs!.content.match(/from '\.\.\/lib\/mathmod'/g) ?? []).length).toBe(1)
|
|
1083
|
+
})
|
|
1084
|
+
})
|