tjs-lang 0.7.7 → 0.7.8
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/CLAUDE.md +90 -33
- package/bin/docs.js +4 -1
- package/demo/docs.json +45 -11
- package/demo/src/examples.test.ts +1 -0
- package/demo/src/imports.test.ts +16 -4
- package/demo/src/imports.ts +60 -15
- package/demo/src/playground-shared.ts +9 -8
- package/demo/src/tfs-worker.js +205 -147
- package/demo/src/tjs-playground.ts +34 -10
- package/demo/src/ts-playground.ts +24 -8
- package/dist/index.js +118 -101
- package/dist/index.js.map +4 -4
- package/dist/src/lang/bool-coercion.d.ts +50 -0
- package/dist/src/lang/docs.d.ts +31 -6
- package/dist/src/lang/linter.d.ts +8 -0
- package/dist/src/lang/parser-transforms.d.ts +18 -0
- package/dist/src/lang/parser-types.d.ts +2 -0
- package/dist/src/lang/parser.d.ts +3 -0
- package/dist/src/lang/runtime.d.ts +34 -0
- package/dist/src/lang/types.d.ts +9 -1
- package/dist/src/rbac/index.d.ts +1 -1
- package/dist/src/vm/runtime.d.ts +1 -1
- package/dist/tjs-eval.js +38 -36
- package/dist/tjs-eval.js.map +4 -4
- package/dist/tjs-from-ts.js +20 -20
- package/dist/tjs-from-ts.js.map +3 -3
- package/dist/tjs-lang.js +85 -83
- package/dist/tjs-lang.js.map +4 -4
- package/dist/tjs-vm.js +47 -45
- package/dist/tjs-vm.js.map +4 -4
- package/llms.txt +79 -0
- package/package.json +3 -2
- package/src/cli/commands/convert.test.ts +16 -21
- package/src/lang/bool-coercion.test.ts +203 -0
- package/src/lang/bool-coercion.ts +314 -0
- package/src/lang/codegen.test.ts +137 -0
- package/src/lang/docs.test.ts +328 -1
- package/src/lang/docs.ts +424 -24
- package/src/lang/emitters/ast.ts +11 -12
- package/src/lang/emitters/dts.test.ts +41 -0
- package/src/lang/emitters/dts.ts +9 -0
- package/src/lang/emitters/js-tests.ts +9 -4
- package/src/lang/emitters/js.ts +182 -2
- package/src/lang/inference.ts +54 -0
- package/src/lang/linter.test.ts +104 -1
- package/src/lang/linter.ts +124 -1
- package/src/lang/parser-params.ts +31 -0
- package/src/lang/parser-transforms.ts +304 -0
- package/src/lang/parser-types.ts +2 -0
- package/src/lang/parser.test.ts +73 -1
- package/src/lang/parser.ts +34 -1
- package/src/lang/runtime.ts +98 -0
- package/src/lang/types.ts +6 -0
- package/src/rbac/index.ts +2 -2
- package/src/rbac/rules.tjs.d.ts +9 -0
- package/src/vm/atoms/batteries.ts +2 -2
- package/src/vm/runtime.ts +10 -3
- package/dist/src/rbac/rules.d.ts +0 -184
- package/src/rbac/rules.js +0 -338
package/src/lang/docs.test.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import { describe, it, expect } from 'bun:test'
|
|
9
|
-
import { generateDocs, generateDocsMarkdown } from './docs'
|
|
9
|
+
import { generateDocs, generateDocsMarkdown, prettifyTestBody } from './docs'
|
|
10
10
|
|
|
11
11
|
describe('generateDocs', () => {
|
|
12
12
|
describe('basic output', () => {
|
|
@@ -452,3 +452,330 @@ function second(y: ''): '' {
|
|
|
452
452
|
expect(secondPos).toBeLessThan(afterPos)
|
|
453
453
|
})
|
|
454
454
|
})
|
|
455
|
+
|
|
456
|
+
describe('prettifyTestBody', () => {
|
|
457
|
+
it('translates toBe', () => {
|
|
458
|
+
expect(prettifyTestBody('expect(x).toBe(y)')).toBe('x // → y')
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
it('translates toEqual to ≡', () => {
|
|
462
|
+
expect(prettifyTestBody('expect(a).toEqual(b)')).toBe('a // ≡ b')
|
|
463
|
+
})
|
|
464
|
+
|
|
465
|
+
it('handles balanced parens in expression', () => {
|
|
466
|
+
expect(
|
|
467
|
+
prettifyTestBody('expect(Boolean(new Boolean(false))).toBe(false)')
|
|
468
|
+
).toBe('Boolean(new Boolean(false)) // → false')
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
it('handles balanced parens in expected value', () => {
|
|
472
|
+
expect(prettifyTestBody('expect(x).toBe(f(1, 2))')).toBe('x // → f(1, 2)')
|
|
473
|
+
})
|
|
474
|
+
|
|
475
|
+
it('handles toBeTruthy / toBeFalsy / toBeNull / toBeUndefined', () => {
|
|
476
|
+
expect(prettifyTestBody('expect(x).toBeTruthy()')).toBe('x // → truthy')
|
|
477
|
+
expect(prettifyTestBody('expect(x).toBeFalsy()')).toBe('x // → falsy')
|
|
478
|
+
expect(prettifyTestBody('expect(x).toBeNull()')).toBe('x // → null')
|
|
479
|
+
expect(prettifyTestBody('expect(x).toBeUndefined()')).toBe(
|
|
480
|
+
'x // → undefined'
|
|
481
|
+
)
|
|
482
|
+
})
|
|
483
|
+
|
|
484
|
+
it('translates toContain / toThrow / toBeNaN', () => {
|
|
485
|
+
expect(prettifyTestBody('expect(arr).toContain(3)')).toBe(
|
|
486
|
+
'arr // → contains 3'
|
|
487
|
+
)
|
|
488
|
+
expect(prettifyTestBody('expect(fn).toThrow()')).toBe('fn // → throws')
|
|
489
|
+
expect(prettifyTestBody('expect(x).toBeNaN()')).toBe('x // → NaN')
|
|
490
|
+
})
|
|
491
|
+
|
|
492
|
+
it('translates toBeGreaterThan / toBeLessThan', () => {
|
|
493
|
+
expect(prettifyTestBody('expect(x).toBeGreaterThan(5)')).toBe('x // → > 5')
|
|
494
|
+
expect(prettifyTestBody('expect(y).toBeLessThan(10)')).toBe('y // → < 10')
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
it('preserves non-expect lines', () => {
|
|
498
|
+
expect(prettifyTestBody('console.log("hi")')).toBe('console.log("hi")')
|
|
499
|
+
expect(prettifyTestBody('const x = 5')).toBe('const x = 5')
|
|
500
|
+
})
|
|
501
|
+
|
|
502
|
+
it('handles multiple expects on separate lines', () => {
|
|
503
|
+
const input = ' expect(x).toBe(1)\n expect(y).toBeTruthy()'
|
|
504
|
+
const expected = ' x // → 1\n y // → truthy'
|
|
505
|
+
expect(prettifyTestBody(input)).toBe(expected)
|
|
506
|
+
})
|
|
507
|
+
|
|
508
|
+
it('does not touch parens inside string literals', () => {
|
|
509
|
+
// The `expect(...)` inside the string literal should NOT be transformed
|
|
510
|
+
expect(prettifyTestBody(`const s = "expect(fake).toBe(impossible)"`)).toBe(
|
|
511
|
+
`const s = "expect(fake).toBe(impossible)"`
|
|
512
|
+
)
|
|
513
|
+
})
|
|
514
|
+
|
|
515
|
+
it('falls back gracefully on unknown matchers', () => {
|
|
516
|
+
expect(prettifyTestBody('expect(x).somethingWeird(y)')).toBe(
|
|
517
|
+
'x // .somethingWeird(y)'
|
|
518
|
+
)
|
|
519
|
+
})
|
|
520
|
+
})
|
|
521
|
+
|
|
522
|
+
describe('function extraction', () => {
|
|
523
|
+
it('captures simple function signatures', () => {
|
|
524
|
+
const md = generateDocsMarkdown(
|
|
525
|
+
`function add(a: 0, b: 0): 0 { return a + b }`
|
|
526
|
+
)
|
|
527
|
+
expect(md).toContain('## add')
|
|
528
|
+
expect(md).toContain('function add(a: 0, b: 0): 0')
|
|
529
|
+
})
|
|
530
|
+
|
|
531
|
+
it('handles arrow-function defaults (params with nested parens)', () => {
|
|
532
|
+
// Regression: the old funcPattern used [^)]* and broke on `(x) => x`
|
|
533
|
+
const source = `
|
|
534
|
+
function mapStrings(arr: [''], fn = (x) => x): [''] {
|
|
535
|
+
return arr.map(fn)
|
|
536
|
+
}
|
|
537
|
+
function compose(f = (x) => x, g = (x) => x): 0 {
|
|
538
|
+
return f(g(5))
|
|
539
|
+
}
|
|
540
|
+
`
|
|
541
|
+
const md = generateDocsMarkdown(source)
|
|
542
|
+
expect(md).toContain('## mapStrings')
|
|
543
|
+
expect(md).toContain("function mapStrings(arr: [''], fn = (x) => x): ['']")
|
|
544
|
+
expect(md).toContain('## compose')
|
|
545
|
+
expect(md).toContain('function compose(f = (x) => x, g = (x) => x): 0')
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
it('handles object/array example values in params', () => {
|
|
549
|
+
const md = generateDocsMarkdown(
|
|
550
|
+
`function f(p: { a: 0, b: '' }, q: [0]): {} { return p }`
|
|
551
|
+
)
|
|
552
|
+
expect(md).toContain("function f(p: { a: 0, b: '' }, q: [0]): {}")
|
|
553
|
+
})
|
|
554
|
+
|
|
555
|
+
it('handles return-type annotation with quoted string', () => {
|
|
556
|
+
const md = generateDocsMarkdown(
|
|
557
|
+
`function greet(name: 'World'): 'Hello, World!' { return \`Hello, \${name}!\` }`
|
|
558
|
+
)
|
|
559
|
+
expect(md).toContain("function greet(name: 'World'): 'Hello, World!'")
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
it('does not extract nested function declarations', () => {
|
|
563
|
+
const source = `
|
|
564
|
+
function outer() {
|
|
565
|
+
function nested() { return 1 }
|
|
566
|
+
return nested
|
|
567
|
+
}
|
|
568
|
+
`
|
|
569
|
+
const result = generateDocs(source)
|
|
570
|
+
const fns = result.items.filter((i) => i.type === 'function')
|
|
571
|
+
expect(fns.length).toBe(1)
|
|
572
|
+
expect(fns[0].name).toBe('outer')
|
|
573
|
+
})
|
|
574
|
+
|
|
575
|
+
describe('function param rendering in docs', () => {
|
|
576
|
+
// We need transpiled type metadata for the param-table renderer.
|
|
577
|
+
// Use the lang index's `tjs()` to get types.
|
|
578
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
579
|
+
const { tjs } = require('./index')
|
|
580
|
+
|
|
581
|
+
it('renders an arrow-default param as `(x: any) => any` (not `function`)', () => {
|
|
582
|
+
const r = tjs(`function f(fn = (x) => x): 0 { return 0 }`)
|
|
583
|
+
const md = generateDocsMarkdown(r.code, r.types)
|
|
584
|
+
expect(md).toContain('`fn`: (x: any) => any')
|
|
585
|
+
})
|
|
586
|
+
|
|
587
|
+
it('infers return type from concise arrow body', () => {
|
|
588
|
+
const r = tjs(`function f(make = () => 5): 0 { return 0 }`)
|
|
589
|
+
const md = generateDocsMarkdown(r.code, r.types)
|
|
590
|
+
expect(md).toContain('`make`: () => number')
|
|
591
|
+
})
|
|
592
|
+
|
|
593
|
+
it('infers param types from arrow defaults', () => {
|
|
594
|
+
const r = tjs(
|
|
595
|
+
`function f(reduce = (acc = 0, x = 0) => 0): 0 { return 0 }`
|
|
596
|
+
)
|
|
597
|
+
const md = generateDocsMarkdown(r.code, r.types)
|
|
598
|
+
expect(md).toContain('`reduce`: (acc: number, x: number) => number')
|
|
599
|
+
})
|
|
600
|
+
|
|
601
|
+
it('does not show `e.g. undefined` for function example values', () => {
|
|
602
|
+
const r = tjs(`function f(fn = (x) => x): 0 { return 0 }`)
|
|
603
|
+
const md = generateDocsMarkdown(r.code, r.types)
|
|
604
|
+
expect(md).not.toContain('undefined')
|
|
605
|
+
})
|
|
606
|
+
})
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
describe('class extraction', () => {
|
|
610
|
+
it('extracts a class with constructor and methods', () => {
|
|
611
|
+
const source = `
|
|
612
|
+
class Point {
|
|
613
|
+
constructor(x: 0, y: 0) {
|
|
614
|
+
this.x = x
|
|
615
|
+
this.y = y
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
magnitude() {
|
|
619
|
+
return Math.sqrt(this.x * this.x + this.y * this.y)
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
toString() {
|
|
623
|
+
return \`(\${this.x}, \${this.y})\`
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
`
|
|
627
|
+
const md = generateDocsMarkdown(source)
|
|
628
|
+
expect(md).toContain('## Point')
|
|
629
|
+
expect(md).toContain('class Point {')
|
|
630
|
+
expect(md).toContain('constructor(x: 0, y: 0)')
|
|
631
|
+
expect(md).toContain('magnitude()')
|
|
632
|
+
expect(md).toContain('toString()')
|
|
633
|
+
// Method bodies should NOT appear
|
|
634
|
+
expect(md).not.toContain('this.x = x')
|
|
635
|
+
expect(md).not.toContain('Math.sqrt')
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
it('extracts multiple constructors', () => {
|
|
639
|
+
const source = `
|
|
640
|
+
class Color {
|
|
641
|
+
constructor(r: +0, g: +0, b: +0) { this.r = r; this.g = g; this.b = b }
|
|
642
|
+
constructor(hex: '#000000') { /* parse */ }
|
|
643
|
+
toString() { return 'rgb(...)' }
|
|
644
|
+
}
|
|
645
|
+
`
|
|
646
|
+
const md = generateDocsMarkdown(source)
|
|
647
|
+
expect(md).toContain('constructor(r: +0, g: +0, b: +0)')
|
|
648
|
+
expect(md).toContain("constructor(hex: '#000000')")
|
|
649
|
+
expect(md).toContain('toString()')
|
|
650
|
+
})
|
|
651
|
+
|
|
652
|
+
it('renders extends clause', () => {
|
|
653
|
+
const source = `
|
|
654
|
+
class ColorWithAlpha extends Color {
|
|
655
|
+
constructor(r: 0, g: 0, b: 0, a: 1.0) { super(r, g, b); this.a = a }
|
|
656
|
+
}
|
|
657
|
+
`
|
|
658
|
+
const md = generateDocsMarkdown(source)
|
|
659
|
+
expect(md).toContain('class ColorWithAlpha extends Color {')
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
it('handles static / async / get / set modifiers', () => {
|
|
663
|
+
const source = `
|
|
664
|
+
class Thing {
|
|
665
|
+
static load(path: '') { return path }
|
|
666
|
+
async fetch() { return null }
|
|
667
|
+
get name() { return 'x' }
|
|
668
|
+
set name(v: '') { this._name = v }
|
|
669
|
+
}
|
|
670
|
+
`
|
|
671
|
+
const md = generateDocsMarkdown(source)
|
|
672
|
+
expect(md).toContain("static load(path: '')")
|
|
673
|
+
expect(md).toContain('async fetch()')
|
|
674
|
+
expect(md).toContain('get name()')
|
|
675
|
+
expect(md).toContain("set name(v: '')")
|
|
676
|
+
})
|
|
677
|
+
|
|
678
|
+
it('captures return type annotations', () => {
|
|
679
|
+
const source = `
|
|
680
|
+
class Math2 {
|
|
681
|
+
add(a: 0, b: 0): 0 { return a + b }
|
|
682
|
+
}
|
|
683
|
+
`
|
|
684
|
+
const md = generateDocsMarkdown(source)
|
|
685
|
+
expect(md).toContain('add(a: 0, b: 0): 0')
|
|
686
|
+
})
|
|
687
|
+
|
|
688
|
+
it('does NOT extract `class Foo` text inside /*# */ doc blocks', () => {
|
|
689
|
+
// The doc-block prose is rendered as-is, but we should NOT emit a
|
|
690
|
+
// "## FakeClass" heading from the prose's `class FakeClass` mention.
|
|
691
|
+
const source = `
|
|
692
|
+
/*#
|
|
693
|
+
## Example
|
|
694
|
+
Don't write this:
|
|
695
|
+
|
|
696
|
+
class FakeClass { constructor(x) {} }
|
|
697
|
+
*/
|
|
698
|
+
|
|
699
|
+
class RealClass {
|
|
700
|
+
constructor() {}
|
|
701
|
+
}
|
|
702
|
+
`
|
|
703
|
+
const result = generateDocs(source)
|
|
704
|
+
const classItems = result.items.filter((i) => i.type === 'class')
|
|
705
|
+
expect(classItems.length).toBe(1)
|
|
706
|
+
expect(classItems[0].name).toBe('RealClass')
|
|
707
|
+
})
|
|
708
|
+
|
|
709
|
+
it('does NOT extract `function` text inside /*# */ doc blocks', () => {
|
|
710
|
+
const source = `
|
|
711
|
+
/*#
|
|
712
|
+
Don't write this:
|
|
713
|
+
|
|
714
|
+
function fakeFn() {}
|
|
715
|
+
*/
|
|
716
|
+
|
|
717
|
+
function realFn() { return 1 }
|
|
718
|
+
`
|
|
719
|
+
const result = generateDocs(source)
|
|
720
|
+
const fnItems = result.items.filter((i) => i.type === 'function')
|
|
721
|
+
expect(fnItems.length).toBe(1)
|
|
722
|
+
expect(fnItems[0].name).toBe('realFn')
|
|
723
|
+
})
|
|
724
|
+
|
|
725
|
+
it('handles a class with no members', () => {
|
|
726
|
+
const md = generateDocsMarkdown('class Empty {}')
|
|
727
|
+
expect(md).toContain('## Empty')
|
|
728
|
+
expect(md).toContain('class Empty {')
|
|
729
|
+
expect(md).toContain('}')
|
|
730
|
+
})
|
|
731
|
+
})
|
|
732
|
+
|
|
733
|
+
describe('generateDocsMarkdown — test cases section', () => {
|
|
734
|
+
it('renders each named test as a "### <name> (test cases)" heading with prettified body', () => {
|
|
735
|
+
const source = `
|
|
736
|
+
test 'x is 5' {
|
|
737
|
+
expect(x).toBe(5)
|
|
738
|
+
}
|
|
739
|
+
test 'y is truthy' {
|
|
740
|
+
expect(y).toBeTruthy()
|
|
741
|
+
}
|
|
742
|
+
`
|
|
743
|
+
const md = generateDocsMarkdown(source)
|
|
744
|
+
expect(md).toContain('### x is 5 (test cases)')
|
|
745
|
+
expect(md).toContain('x // → 5')
|
|
746
|
+
expect(md).toContain('### y is truthy (test cases)')
|
|
747
|
+
expect(md).toContain('y // → truthy')
|
|
748
|
+
})
|
|
749
|
+
|
|
750
|
+
it('skips anonymous tests (no description)', () => {
|
|
751
|
+
const source = `
|
|
752
|
+
test {
|
|
753
|
+
expect(x).toBe(5)
|
|
754
|
+
}
|
|
755
|
+
`
|
|
756
|
+
const md = generateDocsMarkdown(source)
|
|
757
|
+
expect(md).not.toContain('(test cases)')
|
|
758
|
+
})
|
|
759
|
+
|
|
760
|
+
it('integrates with doc blocks and functions', () => {
|
|
761
|
+
const source = `
|
|
762
|
+
/*#
|
|
763
|
+
# Module
|
|
764
|
+
Some intro.
|
|
765
|
+
*/
|
|
766
|
+
|
|
767
|
+
function add(a: 0, b: 0): 0 {
|
|
768
|
+
return a + b
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
test 'add(2, 3) is 5' {
|
|
772
|
+
expect(add(2, 3)).toBe(5)
|
|
773
|
+
}
|
|
774
|
+
`
|
|
775
|
+
const md = generateDocsMarkdown(source)
|
|
776
|
+
expect(md).toContain('# Module')
|
|
777
|
+
expect(md).toContain('## add')
|
|
778
|
+
expect(md).toContain('### add(2, 3) is 5 (test cases)')
|
|
779
|
+
expect(md).toContain('add(2, 3) // → 5')
|
|
780
|
+
})
|
|
781
|
+
})
|