tjs-lang 0.7.6 → 0.7.7
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 +33 -15
- package/demo/docs.json +3 -3
- package/dist/index.js +110 -106
- package/dist/index.js.map +3 -3
- package/dist/src/lang/parser.d.ts +6 -0
- package/dist/tjs-eval.js +37 -34
- package/dist/tjs-eval.js.map +3 -3
- package/dist/tjs-from-ts.js +1 -1
- package/dist/tjs-from-ts.js.map +1 -1
- package/dist/tjs-lang.js +76 -72
- package/dist/tjs-lang.js.map +3 -3
- package/dist/tjs-vm.js +44 -41
- package/dist/tjs-vm.js.map +3 -3
- package/package.json +1 -1
- package/src/lang/codegen.test.ts +40 -0
- package/src/lang/emitters/js-tests.ts +7 -0
- package/src/lang/emitters/js.ts +26 -0
- package/src/lang/features.test.ts +22 -0
- package/src/lang/parser-transforms.ts +235 -6
- package/src/lang/parser.ts +51 -0
- package/src/lang/tests.ts +21 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tjs-lang",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.7",
|
|
4
4
|
"description": "Type-safe JavaScript dialect with runtime validation, sandboxed VM execution, and AI agent orchestration. Transpiles TypeScript to validated JS with fuel-metered execution for untrusted code.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
package/src/lang/codegen.test.ts
CHANGED
|
@@ -947,6 +947,46 @@ function isEqual(! a: null, b: null):! true { return a == b }`
|
|
|
947
947
|
// No coercion: 0 != '' (unlike JS ==)
|
|
948
948
|
expect(isEqual(0, '')).toBe(false)
|
|
949
949
|
})
|
|
950
|
+
|
|
951
|
+
it('typeof transform skips string/template/comment bodies (regression)', () => {
|
|
952
|
+
// Previously the typeof→TypeOf rewrite was a naive regex that also
|
|
953
|
+
// mangled `'typeof x'` inside string literals. The replacement is now
|
|
954
|
+
// tokenizer-aware.
|
|
955
|
+
const tjsSource = `
|
|
956
|
+
const label = 'typeof null is null'
|
|
957
|
+
const tpl = \`type marker: typeof x\`
|
|
958
|
+
// typeof y
|
|
959
|
+
const t = typeof null
|
|
960
|
+
`
|
|
961
|
+
const { code } = tjs(tjsSource)
|
|
962
|
+
expect(code).toContain("'typeof null is null'")
|
|
963
|
+
expect(code).toContain('`type marker: typeof x`')
|
|
964
|
+
expect(code).toContain('TypeOf(null)')
|
|
965
|
+
})
|
|
966
|
+
|
|
967
|
+
it('applies TjsEquals to test/mock bodies (regression)', () => {
|
|
968
|
+
// Native TJS has TjsEquals on by default. Test bodies are extracted as
|
|
969
|
+
// raw text before parsing, so == inside them must also be transformed.
|
|
970
|
+
const tjsSource = `const foo = '' == false
|
|
971
|
+
|
|
972
|
+
test 'top-level == is transformed' {
|
|
973
|
+
expect(foo).toBe(false)
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
test '== inside test block is transformed' {
|
|
977
|
+
expect('' == false).toBe(false)
|
|
978
|
+
expect('' != false).toBe(true)
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
test '=== identity is preserved' {
|
|
982
|
+
expect('' === false).toBe(false)
|
|
983
|
+
expect(0 === 0).toBe(true)
|
|
984
|
+
}
|
|
985
|
+
`
|
|
986
|
+
const { testResults } = tjs(tjsSource, { runTests: 'report' })
|
|
987
|
+
const failures = (testResults || []).filter((r) => !r.passed)
|
|
988
|
+
expect(failures).toEqual([])
|
|
989
|
+
})
|
|
950
990
|
})
|
|
951
991
|
|
|
952
992
|
describe('TjsStandard (ASI protection)', () => {
|
|
@@ -657,6 +657,12 @@ export function runAllTests(
|
|
|
657
657
|
// Build mock setup
|
|
658
658
|
const mockSetup = mocks.map((m) => m.body).join('\n')
|
|
659
659
|
|
|
660
|
+
// Test bodies may reference Is/IsNot/Eq/NotEq/TypeOf — these come from the
|
|
661
|
+
// == / != / typeof source-level transforms applied to test bodies in js.ts.
|
|
662
|
+
// The module's own destructuring may not include them (if the module never
|
|
663
|
+
// uses ==), so each test block re-destructures into its own block scope.
|
|
664
|
+
const testRuntimeImports = `const { Is, IsNot, Eq, NotEq, TypeOf } = globalThis.__tjs ?? {};`
|
|
665
|
+
|
|
660
666
|
// Build test execution code that runs all tests in sequence
|
|
661
667
|
const testBodies = tests
|
|
662
668
|
.map((t, i) => {
|
|
@@ -668,6 +674,7 @@ export function runAllTests(
|
|
|
668
674
|
return `
|
|
669
675
|
// Test ${i}: ${t.description}
|
|
670
676
|
try {
|
|
677
|
+
${testRuntimeImports}
|
|
671
678
|
${body}
|
|
672
679
|
__testResults.push({ idx: ${i}, passed: true });
|
|
673
680
|
} catch (e) {
|
package/src/lang/emitters/js.ts
CHANGED
|
@@ -53,7 +53,12 @@ import {
|
|
|
53
53
|
extractTDoc,
|
|
54
54
|
preprocess,
|
|
55
55
|
transformExtensionCalls,
|
|
56
|
+
stripLineComments,
|
|
56
57
|
} from '../parser'
|
|
58
|
+
import {
|
|
59
|
+
transformEqualityToStructural,
|
|
60
|
+
transformIsOperators,
|
|
61
|
+
} from '../parser-transforms'
|
|
57
62
|
import type { TypeDescriptor, ParameterDescriptor } from '../types'
|
|
58
63
|
import { inferTypeFromValue, parseParameter } from '../inference'
|
|
59
64
|
import { extractTests } from '../tests'
|
|
@@ -576,6 +581,10 @@ export function transpileToJS(
|
|
|
576
581
|
} = options
|
|
577
582
|
const warnings: string[] = []
|
|
578
583
|
|
|
584
|
+
// Strip single-line comments early — apostrophes in comments (e.g. "don't")
|
|
585
|
+
// confuse brace matching in test extraction and other transforms
|
|
586
|
+
source = stripLineComments(source)
|
|
587
|
+
|
|
579
588
|
// Extract source file annotation if present (from TS transpilation)
|
|
580
589
|
const sourceFileAnnotation = extractSourceFileAnnotation(source)
|
|
581
590
|
const effectiveFilename = sourceFileAnnotation || filename
|
|
@@ -600,6 +609,23 @@ export function transpileToJS(
|
|
|
600
609
|
// Preprocess source (handles TJS syntax transformations)
|
|
601
610
|
const preprocessed = preprocess(cleanSource)
|
|
602
611
|
|
|
612
|
+
// Apply the same source-level equality transforms to extracted test/mock
|
|
613
|
+
// bodies so they observe the module's TJS semantics (e.g. structural ==).
|
|
614
|
+
// Test bodies are extracted as raw text before parse(), so they would
|
|
615
|
+
// otherwise run with native JS == coercion regardless of TjsEquals mode.
|
|
616
|
+
for (const t of tests) {
|
|
617
|
+
t.body = transformIsOperators(t.body)
|
|
618
|
+
if (preprocessed.tjsModes.tjsEquals) {
|
|
619
|
+
t.body = transformEqualityToStructural(t.body)
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
for (const m of mocks) {
|
|
623
|
+
m.body = transformIsOperators(m.body)
|
|
624
|
+
if (preprocessed.tjsModes.tjsEquals) {
|
|
625
|
+
m.body = transformEqualityToStructural(m.body)
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
603
629
|
// Build types map for all functions
|
|
604
630
|
const allTypes: Record<string, TJSTypeInfo> = {}
|
|
605
631
|
|
|
@@ -153,6 +153,28 @@ describe('Inline Tests', () => {
|
|
|
153
153
|
expect(result.tests[0].body).toContain('assert')
|
|
154
154
|
})
|
|
155
155
|
|
|
156
|
+
it('should extract test descriptions containing other quote types', () => {
|
|
157
|
+
// Regression: previously the regex excluded all quote chars from
|
|
158
|
+
// descriptions, so `test 'has "quotes"' { }` was silently dropped.
|
|
159
|
+
const result = extractTests(`
|
|
160
|
+
test 'typeof null is "null"' {
|
|
161
|
+
assert(true)
|
|
162
|
+
}
|
|
163
|
+
test "single 'apostrophe' inside" {
|
|
164
|
+
assert(true)
|
|
165
|
+
}
|
|
166
|
+
test \`backticks with "double" and 'single'\` {
|
|
167
|
+
assert(true)
|
|
168
|
+
}
|
|
169
|
+
`)
|
|
170
|
+
expect(result.tests.length).toBe(3)
|
|
171
|
+
expect(result.tests[0].description).toBe('typeof null is "null"')
|
|
172
|
+
expect(result.tests[1].description).toBe("single 'apostrophe' inside")
|
|
173
|
+
expect(result.tests[2].description).toBe(
|
|
174
|
+
`backticks with "double" and 'single'`
|
|
175
|
+
)
|
|
176
|
+
})
|
|
177
|
+
|
|
156
178
|
it('should remove tests from output code', () => {
|
|
157
179
|
const result = extractTests(`
|
|
158
180
|
function add(a, b) { return a + b }
|
|
@@ -593,6 +593,201 @@ export function insertAsiProtection(source: string): string {
|
|
|
593
593
|
return result.join('\n')
|
|
594
594
|
}
|
|
595
595
|
|
|
596
|
+
/**
|
|
597
|
+
* Replace `typeof X` with `TypeOf(X)` outside string/template/comment/regex bodies.
|
|
598
|
+
* X may be `ident`, `ident.foo`, `ident?.foo`, etc. Anything that doesn't look
|
|
599
|
+
* like a simple identifier expression (e.g. `typeof (x + 1)`) is left alone.
|
|
600
|
+
*/
|
|
601
|
+
function transformTypeofKeyword(source: string): string {
|
|
602
|
+
type Match = { keywordStart: number; operandEnd: number; operand: string }
|
|
603
|
+
const matches: Match[] = []
|
|
604
|
+
let i = 0
|
|
605
|
+
let state: TokenizerState = 'normal'
|
|
606
|
+
const templateStack: number[] = []
|
|
607
|
+
|
|
608
|
+
while (i < source.length) {
|
|
609
|
+
const char = source[i]
|
|
610
|
+
const nextChar = source[i + 1]
|
|
611
|
+
|
|
612
|
+
switch (state) {
|
|
613
|
+
case 'single-string':
|
|
614
|
+
if (char === '\\' && i + 1 < source.length) {
|
|
615
|
+
i += 2
|
|
616
|
+
continue
|
|
617
|
+
}
|
|
618
|
+
if (char === "'") state = 'normal'
|
|
619
|
+
i++
|
|
620
|
+
continue
|
|
621
|
+
case 'double-string':
|
|
622
|
+
if (char === '\\' && i + 1 < source.length) {
|
|
623
|
+
i += 2
|
|
624
|
+
continue
|
|
625
|
+
}
|
|
626
|
+
if (char === '"') state = 'normal'
|
|
627
|
+
i++
|
|
628
|
+
continue
|
|
629
|
+
case 'template-string':
|
|
630
|
+
if (char === '\\' && i + 1 < source.length) {
|
|
631
|
+
i += 2
|
|
632
|
+
continue
|
|
633
|
+
}
|
|
634
|
+
if (char === '$' && nextChar === '{') {
|
|
635
|
+
i += 2
|
|
636
|
+
templateStack.push(1)
|
|
637
|
+
state = 'normal'
|
|
638
|
+
continue
|
|
639
|
+
}
|
|
640
|
+
if (char === '`') state = 'normal'
|
|
641
|
+
i++
|
|
642
|
+
continue
|
|
643
|
+
case 'line-comment':
|
|
644
|
+
if (char === '\n') state = 'normal'
|
|
645
|
+
i++
|
|
646
|
+
continue
|
|
647
|
+
case 'block-comment':
|
|
648
|
+
if (char === '*' && nextChar === '/') {
|
|
649
|
+
i += 2
|
|
650
|
+
state = 'normal'
|
|
651
|
+
continue
|
|
652
|
+
}
|
|
653
|
+
i++
|
|
654
|
+
continue
|
|
655
|
+
case 'regex':
|
|
656
|
+
if (char === '\\' && i + 1 < source.length) {
|
|
657
|
+
i += 2
|
|
658
|
+
continue
|
|
659
|
+
}
|
|
660
|
+
if (char === '[') {
|
|
661
|
+
i++
|
|
662
|
+
while (i < source.length && source[i] !== ']') {
|
|
663
|
+
if (source[i] === '\\' && i + 1 < source.length) i += 2
|
|
664
|
+
else i++
|
|
665
|
+
}
|
|
666
|
+
if (i < source.length) i++
|
|
667
|
+
continue
|
|
668
|
+
}
|
|
669
|
+
if (char === '/') {
|
|
670
|
+
i++
|
|
671
|
+
while (i < source.length && /[gimsuy]/.test(source[i])) i++
|
|
672
|
+
state = 'normal'
|
|
673
|
+
continue
|
|
674
|
+
}
|
|
675
|
+
i++
|
|
676
|
+
continue
|
|
677
|
+
case 'normal':
|
|
678
|
+
if (templateStack.length > 0) {
|
|
679
|
+
if (char === '{') {
|
|
680
|
+
templateStack[templateStack.length - 1]++
|
|
681
|
+
} else if (char === '}') {
|
|
682
|
+
templateStack[templateStack.length - 1]--
|
|
683
|
+
if (templateStack[templateStack.length - 1] === 0) {
|
|
684
|
+
templateStack.pop()
|
|
685
|
+
i++
|
|
686
|
+
state = 'template-string'
|
|
687
|
+
continue
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (char === "'") {
|
|
692
|
+
i++
|
|
693
|
+
state = 'single-string'
|
|
694
|
+
continue
|
|
695
|
+
}
|
|
696
|
+
if (char === '"') {
|
|
697
|
+
i++
|
|
698
|
+
state = 'double-string'
|
|
699
|
+
continue
|
|
700
|
+
}
|
|
701
|
+
if (char === '`') {
|
|
702
|
+
i++
|
|
703
|
+
state = 'template-string'
|
|
704
|
+
continue
|
|
705
|
+
}
|
|
706
|
+
if (char === '/' && nextChar === '/') {
|
|
707
|
+
i += 2
|
|
708
|
+
state = 'line-comment'
|
|
709
|
+
continue
|
|
710
|
+
}
|
|
711
|
+
if (char === '/' && nextChar === '*') {
|
|
712
|
+
i += 2
|
|
713
|
+
state = 'block-comment'
|
|
714
|
+
continue
|
|
715
|
+
}
|
|
716
|
+
if (char === '/') {
|
|
717
|
+
let j = i - 1
|
|
718
|
+
while (j >= 0 && /\s/.test(source[j])) j--
|
|
719
|
+
const beforeChar = j >= 0 ? source[j] : ''
|
|
720
|
+
const isRegexContext =
|
|
721
|
+
!beforeChar ||
|
|
722
|
+
/[=(!,;:{[&|?+\-*%<>~^]/.test(beforeChar) ||
|
|
723
|
+
(j >= 5 &&
|
|
724
|
+
/\b(return|case|throw|in|of|typeof|instanceof|new|delete|void)$/.test(
|
|
725
|
+
source.slice(Math.max(0, j - 10), j + 1)
|
|
726
|
+
))
|
|
727
|
+
if (isRegexContext) {
|
|
728
|
+
i++
|
|
729
|
+
state = 'regex'
|
|
730
|
+
continue
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// Detect `typeof <ident-chain>` — only when preceded by a non-word
|
|
735
|
+
// boundary and followed by whitespace then an identifier.
|
|
736
|
+
if (
|
|
737
|
+
char === 't' &&
|
|
738
|
+
source.slice(i, i + 6) === 'typeof' &&
|
|
739
|
+
(i === 0 || !/[\w$]/.test(source[i - 1])) &&
|
|
740
|
+
/\s/.test(source[i + 6] ?? '')
|
|
741
|
+
) {
|
|
742
|
+
let j = i + 6
|
|
743
|
+
while (j < source.length && /\s/.test(source[j])) j++
|
|
744
|
+
if (j < source.length && /[a-zA-Z_$]/.test(source[j])) {
|
|
745
|
+
const operandStart = j
|
|
746
|
+
while (j < source.length && /[\w$]/.test(source[j])) j++
|
|
747
|
+
// Optional `.name` or `?.name` chains
|
|
748
|
+
while (j < source.length) {
|
|
749
|
+
if (source[j] === '.' && /[a-zA-Z_$]/.test(source[j + 1] ?? '')) {
|
|
750
|
+
j++
|
|
751
|
+
while (j < source.length && /[\w$]/.test(source[j])) j++
|
|
752
|
+
} else if (
|
|
753
|
+
source[j] === '?' &&
|
|
754
|
+
source[j + 1] === '.' &&
|
|
755
|
+
/[a-zA-Z_$]/.test(source[j + 2] ?? '')
|
|
756
|
+
) {
|
|
757
|
+
j += 2
|
|
758
|
+
while (j < source.length && /[\w$]/.test(source[j])) j++
|
|
759
|
+
} else {
|
|
760
|
+
break
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
matches.push({
|
|
764
|
+
keywordStart: i,
|
|
765
|
+
operandEnd: j,
|
|
766
|
+
operand: source.slice(operandStart, j),
|
|
767
|
+
})
|
|
768
|
+
i = j
|
|
769
|
+
continue
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
break
|
|
773
|
+
}
|
|
774
|
+
i++
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
if (matches.length === 0) return source
|
|
778
|
+
|
|
779
|
+
// Apply replacements from end to start so earlier positions remain valid.
|
|
780
|
+
let result = source
|
|
781
|
+
for (let k = matches.length - 1; k >= 0; k--) {
|
|
782
|
+
const m = matches[k]
|
|
783
|
+
result =
|
|
784
|
+
result.slice(0, m.keywordStart) +
|
|
785
|
+
`TypeOf(${m.operand})` +
|
|
786
|
+
result.slice(m.operandEnd)
|
|
787
|
+
}
|
|
788
|
+
return result
|
|
789
|
+
}
|
|
790
|
+
|
|
596
791
|
/**
|
|
597
792
|
* Transform == and != to Is() and IsNot() calls
|
|
598
793
|
*
|
|
@@ -606,11 +801,11 @@ export function insertAsiProtection(source: string): string {
|
|
|
606
801
|
* 2. Transform from end to start (so positions remain valid)
|
|
607
802
|
*/
|
|
608
803
|
export function transformEqualityToStructural(source: string): string {
|
|
609
|
-
// Transform typeof to TypeOf() — fixes typeof null === 'object'
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
)
|
|
804
|
+
// Transform typeof to TypeOf() — fixes typeof null === 'object'.
|
|
805
|
+
// Uses the same tokenizer state machine as the equality pass so it skips
|
|
806
|
+
// string/template/comment/regex bodies (a regex replace would rewrite
|
|
807
|
+
// 'typeof x' inside string literals).
|
|
808
|
+
source = transformTypeofKeyword(source)
|
|
614
809
|
|
|
615
810
|
// First pass: find all == and != positions (outside strings/comments/regex)
|
|
616
811
|
const equalityOps: Array<{ pos: number; op: '==' | '!=' }> = []
|
|
@@ -2642,9 +2837,43 @@ export function extractAndRunTests(
|
|
|
2642
2837
|
let depth = 1
|
|
2643
2838
|
let k = bodyStart
|
|
2644
2839
|
|
|
2645
|
-
// Find matching closing brace
|
|
2840
|
+
// Find matching closing brace (skip strings and comments)
|
|
2841
|
+
let inStr: string | null = null
|
|
2842
|
+
let escaped = false
|
|
2646
2843
|
while (k < source.length && depth > 0) {
|
|
2647
2844
|
const char = source[k]
|
|
2845
|
+
if (escaped) {
|
|
2846
|
+
escaped = false
|
|
2847
|
+
k++
|
|
2848
|
+
continue
|
|
2849
|
+
}
|
|
2850
|
+
if (char === '\\' && inStr) {
|
|
2851
|
+
escaped = true
|
|
2852
|
+
k++
|
|
2853
|
+
continue
|
|
2854
|
+
}
|
|
2855
|
+
if (inStr) {
|
|
2856
|
+
if (char === inStr) inStr = null
|
|
2857
|
+
k++
|
|
2858
|
+
continue
|
|
2859
|
+
}
|
|
2860
|
+
// Line comment — skip to end of line
|
|
2861
|
+
if (char === '/' && source[k + 1] === '/') {
|
|
2862
|
+
const nl = source.indexOf('\n', k)
|
|
2863
|
+
k = nl === -1 ? source.length : nl + 1
|
|
2864
|
+
continue
|
|
2865
|
+
}
|
|
2866
|
+
// Block comment — skip to */
|
|
2867
|
+
if (char === '/' && source[k + 1] === '*') {
|
|
2868
|
+
const end = source.indexOf('*/', k + 2)
|
|
2869
|
+
k = end === -1 ? source.length : end + 2
|
|
2870
|
+
continue
|
|
2871
|
+
}
|
|
2872
|
+
if (char === "'" || char === '"' || char === '`') {
|
|
2873
|
+
inStr = char
|
|
2874
|
+
k++
|
|
2875
|
+
continue
|
|
2876
|
+
}
|
|
2648
2877
|
if (char === '{') depth++
|
|
2649
2878
|
else if (char === '}') depth--
|
|
2650
2879
|
k++
|
package/src/lang/parser.ts
CHANGED
|
@@ -56,6 +56,52 @@ import {
|
|
|
56
56
|
// Re-export transformExtensionCalls for js.ts
|
|
57
57
|
export { transformExtensionCalls } from './parser-transforms'
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Strip single-line comments (//) from source.
|
|
61
|
+
* Replaces comment content with spaces to preserve character offsets.
|
|
62
|
+
* Skips // inside strings and block comments.
|
|
63
|
+
*/
|
|
64
|
+
export function stripLineComments(source: string): string {
|
|
65
|
+
let result = ''
|
|
66
|
+
let i = 0
|
|
67
|
+
while (i < source.length) {
|
|
68
|
+
const ch = source[i]
|
|
69
|
+
// String literals — skip to closing quote
|
|
70
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
71
|
+
const quote = ch
|
|
72
|
+
result += ch
|
|
73
|
+
i++
|
|
74
|
+
while (i < source.length && source[i] !== quote) {
|
|
75
|
+
if (source[i] === '\\') {
|
|
76
|
+
result += source[i++]
|
|
77
|
+
}
|
|
78
|
+
if (i < source.length) result += source[i++]
|
|
79
|
+
}
|
|
80
|
+
if (i < source.length) result += source[i++] // closing quote
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
// Block comment — pass through (may contain //)
|
|
84
|
+
if (ch === '/' && source[i + 1] === '*') {
|
|
85
|
+
const end = source.indexOf('*/', i + 2)
|
|
86
|
+
const slice = end === -1 ? source.slice(i) : source.slice(i, end + 2)
|
|
87
|
+
result += slice
|
|
88
|
+
i += slice.length
|
|
89
|
+
continue
|
|
90
|
+
}
|
|
91
|
+
// Line comment — replace with spaces to preserve offsets
|
|
92
|
+
if (ch === '/' && source[i + 1] === '/') {
|
|
93
|
+
const nl = source.indexOf('\n', i)
|
|
94
|
+
const end = nl === -1 ? source.length : nl
|
|
95
|
+
result += ' '.repeat(end - i)
|
|
96
|
+
i = end // leave \n for next iteration
|
|
97
|
+
continue
|
|
98
|
+
}
|
|
99
|
+
result += ch
|
|
100
|
+
i++
|
|
101
|
+
}
|
|
102
|
+
return result
|
|
103
|
+
}
|
|
104
|
+
|
|
59
105
|
export function preprocess(
|
|
60
106
|
source: string,
|
|
61
107
|
options: PreprocessOptions = {}
|
|
@@ -182,6 +228,11 @@ export function preprocess(
|
|
|
182
228
|
)
|
|
183
229
|
}
|
|
184
230
|
|
|
231
|
+
// Strip single-line comments early — they confuse brace matching,
|
|
232
|
+
// ASI protection, and test extraction (e.g. apostrophes in comments)
|
|
233
|
+
// Preserves line structure by keeping the newline
|
|
234
|
+
source = stripLineComments(source)
|
|
235
|
+
|
|
185
236
|
// TjsStandard mode: insert semicolons to prevent ASI footguns
|
|
186
237
|
// Must happen early before other transformations modify line structure
|
|
187
238
|
if (tjsModes.tjsStandard) {
|
package/src/lang/tests.ts
CHANGED
|
@@ -106,15 +106,17 @@ function extractEmbeddedTests(source: string): ExtractedTest[] {
|
|
|
106
106
|
const tests: ExtractedTest[] = []
|
|
107
107
|
|
|
108
108
|
// Match: /*test 'description' { ... }*/ or /*test { ... }*/
|
|
109
|
+
// Each quote type gets its own alternative so the description can contain
|
|
110
|
+
// the other quote types (e.g. `test 'typeof null is "null"' {`).
|
|
109
111
|
const embeddedRegex =
|
|
110
|
-
/\/\*test\s+(['
|
|
112
|
+
/\/\*test\s+'([^']*)'\s*\{([\s\S]*?)\}\s*\*\/|\/\*test\s+"([^"]*)"\s*\{([\s\S]*?)\}\s*\*\/|\/\*test\s+`([^`]*)`\s*\{([\s\S]*?)\}\s*\*\/|\/\*test\s*\{([\s\S]*?)\}\s*\*\//g
|
|
111
113
|
|
|
112
114
|
let match
|
|
113
115
|
while ((match = embeddedRegex.exec(source)) !== null) {
|
|
114
|
-
//
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const body = (match[
|
|
116
|
+
// Groups: 1/3/5 = description for ' " ` ; 2/4/6 = body for ' " ` ; 7 = body for anonymous
|
|
117
|
+
const desc =
|
|
118
|
+
match[1] || match[3] || match[5] || `embedded test ${tests.length + 1}`
|
|
119
|
+
const body = (match[2] || match[4] || match[6] || match[7] || '').trim()
|
|
118
120
|
|
|
119
121
|
tests.push({
|
|
120
122
|
description: desc,
|
|
@@ -145,8 +147,10 @@ export function extractTests(source: string): TestExtractionResult {
|
|
|
145
147
|
// test { ... } (anonymous test)
|
|
146
148
|
// test 'description' { ... } (canonical TJS)
|
|
147
149
|
// test('description') { ... } (also valid - parenthesized string is still a string)
|
|
150
|
+
// Each quote type has its own alternative so the description can contain
|
|
151
|
+
// the other quote types (e.g. `test 'typeof null is "null"' {`).
|
|
148
152
|
const testRegex =
|
|
149
|
-
/test\s+(['
|
|
153
|
+
/test\s+'([^']*)'\s*\{|test\s+"([^"]*)"\s*\{|test\s+`([^`]*)`\s*\{|test\s*\(\s*'([^']*)'\s*\)\s*\{|test\s*\(\s*"([^"]*)"\s*\)\s*\{|test\s*\(\s*`([^`]*)`\s*\)\s*\{|test\s*\{/g
|
|
150
154
|
const mockRegex = /mock\s*\{/g
|
|
151
155
|
|
|
152
156
|
let cleanCode = source
|
|
@@ -164,8 +168,17 @@ export function extractTests(source: string): TestExtractionResult {
|
|
|
164
168
|
continue
|
|
165
169
|
}
|
|
166
170
|
|
|
167
|
-
//
|
|
168
|
-
|
|
171
|
+
// Groups 1/2/3 = `test 'desc'` / `test "desc"` / `test \`desc\``
|
|
172
|
+
// Groups 4/5/6 = parenthesized variants
|
|
173
|
+
// No group when description is omitted
|
|
174
|
+
const desc =
|
|
175
|
+
match[1] ||
|
|
176
|
+
match[2] ||
|
|
177
|
+
match[3] ||
|
|
178
|
+
match[4] ||
|
|
179
|
+
match[5] ||
|
|
180
|
+
match[6] ||
|
|
181
|
+
`test ${tests.length + 1}`
|
|
169
182
|
const bodyStart = match.index + match[0].length
|
|
170
183
|
|
|
171
184
|
// Find matching closing brace
|