tjs-lang 0.2.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.
Files changed (91) hide show
  1. package/CONTEXT.md +594 -0
  2. package/LICENSE +190 -0
  3. package/README.md +220 -0
  4. package/bin/benchmarks.ts +351 -0
  5. package/bin/dev.ts +205 -0
  6. package/bin/docs.js +170 -0
  7. package/bin/install-cursor.sh +71 -0
  8. package/bin/install-vscode.sh +71 -0
  9. package/bin/select-local-models.d.ts +1 -0
  10. package/bin/select-local-models.js +28 -0
  11. package/bin/select-local-models.ts +31 -0
  12. package/demo/autocomplete.test.ts +232 -0
  13. package/demo/docs.json +186 -0
  14. package/demo/examples.test.ts +598 -0
  15. package/demo/index.html +91 -0
  16. package/demo/src/autocomplete.ts +482 -0
  17. package/demo/src/capabilities.ts +859 -0
  18. package/demo/src/demo-nav.ts +2097 -0
  19. package/demo/src/examples.test.ts +161 -0
  20. package/demo/src/examples.ts +476 -0
  21. package/demo/src/imports.test.ts +196 -0
  22. package/demo/src/imports.ts +421 -0
  23. package/demo/src/index.ts +639 -0
  24. package/demo/src/module-store.ts +635 -0
  25. package/demo/src/module-sw.ts +132 -0
  26. package/demo/src/playground.ts +949 -0
  27. package/demo/src/service-host.ts +389 -0
  28. package/demo/src/settings.ts +440 -0
  29. package/demo/src/style.ts +280 -0
  30. package/demo/src/tjs-playground.ts +1605 -0
  31. package/demo/src/ts-examples.ts +478 -0
  32. package/demo/src/ts-playground.ts +1092 -0
  33. package/demo/static/favicon.svg +30 -0
  34. package/demo/static/photo-1.jpg +0 -0
  35. package/demo/static/photo-2.jpg +0 -0
  36. package/demo/static/texts/ai-history.txt +9 -0
  37. package/demo/static/texts/coffee-origins.txt +9 -0
  38. package/demo/static/texts/renewable-energy.txt +9 -0
  39. package/dist/index.js +256 -0
  40. package/dist/index.js.map +37 -0
  41. package/dist/tjs-batteries.js +4 -0
  42. package/dist/tjs-batteries.js.map +15 -0
  43. package/dist/tjs-full.js +256 -0
  44. package/dist/tjs-full.js.map +37 -0
  45. package/dist/tjs-transpiler.js +220 -0
  46. package/dist/tjs-transpiler.js.map +21 -0
  47. package/dist/tjs-vm.js +4 -0
  48. package/dist/tjs-vm.js.map +14 -0
  49. package/docs/CNAME +1 -0
  50. package/docs/favicon.svg +30 -0
  51. package/docs/index.html +91 -0
  52. package/docs/index.js +10468 -0
  53. package/docs/index.js.map +92 -0
  54. package/docs/photo-1.jpg +0 -0
  55. package/docs/photo-1.webp +0 -0
  56. package/docs/photo-2.jpg +0 -0
  57. package/docs/photo-2.webp +0 -0
  58. package/docs/texts/ai-history.txt +9 -0
  59. package/docs/texts/coffee-origins.txt +9 -0
  60. package/docs/texts/renewable-energy.txt +9 -0
  61. package/docs/tjs-lang.svg +31 -0
  62. package/docs/tosijs-agent.svg +31 -0
  63. package/editors/README.md +325 -0
  64. package/editors/ace/ajs-mode.js +328 -0
  65. package/editors/ace/ajs-mode.ts +269 -0
  66. package/editors/ajs-syntax.ts +212 -0
  67. package/editors/build-grammars.ts +510 -0
  68. package/editors/codemirror/ajs-language.js +287 -0
  69. package/editors/codemirror/ajs-language.ts +1447 -0
  70. package/editors/codemirror/autocomplete.test.ts +531 -0
  71. package/editors/codemirror/component.ts +404 -0
  72. package/editors/monaco/ajs-monarch.js +243 -0
  73. package/editors/monaco/ajs-monarch.ts +225 -0
  74. package/editors/tjs-syntax.ts +115 -0
  75. package/editors/vscode/language-configuration.json +37 -0
  76. package/editors/vscode/package.json +65 -0
  77. package/editors/vscode/syntaxes/ajs-injection.tmLanguage.json +107 -0
  78. package/editors/vscode/syntaxes/ajs.tmLanguage.json +252 -0
  79. package/editors/vscode/syntaxes/tjs.tmLanguage.json +333 -0
  80. package/package.json +83 -0
  81. package/src/cli/commands/check.ts +41 -0
  82. package/src/cli/commands/convert.ts +133 -0
  83. package/src/cli/commands/emit.ts +260 -0
  84. package/src/cli/commands/run.ts +68 -0
  85. package/src/cli/commands/test.ts +194 -0
  86. package/src/cli/commands/types.ts +20 -0
  87. package/src/cli/create-app.ts +236 -0
  88. package/src/cli/playground.ts +250 -0
  89. package/src/cli/tjs.ts +166 -0
  90. package/src/cli/tjsx.ts +160 -0
  91. package/tjs-lang.svg +31 -0
@@ -0,0 +1,41 @@
1
+ /**
2
+ * tjs check - Parse and type check a TJS file
3
+ */
4
+
5
+ import { readFileSync } from 'fs'
6
+ import { tjs } from '../../lang'
7
+
8
+ export async function check(file: string): Promise<void> {
9
+ const source = readFileSync(file, 'utf-8')
10
+
11
+ try {
12
+ const result = tjs(source)
13
+
14
+ // Report function info from types
15
+ if (result.types) {
16
+ const fn = result.types
17
+ const params = Object.entries(fn.params || {})
18
+ .map(([name, info]: [string, any]) => {
19
+ const opt = info.required ? '' : '?'
20
+ const type = info.type?.kind || 'any'
21
+ return `${name}${opt}: ${type}`
22
+ })
23
+ .join(', ')
24
+ const ret = fn.returns?.kind || 'void'
25
+ console.log(`✓ ${file}`)
26
+ console.log(` ${fn.name}(${params}) -> ${ret}`)
27
+ } else {
28
+ console.log(`✓ ${file} - Parsed successfully`)
29
+ }
30
+ } catch (error: any) {
31
+ console.error(`✗ ${file}`)
32
+ if (error.name === 'SyntaxError' && error.formatWithContext) {
33
+ console.error()
34
+ console.error(error.formatWithContext(2))
35
+ console.error()
36
+ } else {
37
+ console.error(` ${error.message}`)
38
+ }
39
+ process.exit(1)
40
+ }
41
+ }
@@ -0,0 +1,133 @@
1
+ /**
2
+ * tjs convert - Convert TypeScript files to TJS
3
+ *
4
+ * Usage:
5
+ * tjs convert <file.ts> Convert single file, output to stdout
6
+ * tjs convert <file.ts> -o <out.tjs> Convert single file to output file
7
+ * tjs convert <dir> -o <outdir> Convert all .ts files in directory
8
+ */
9
+
10
+ import {
11
+ readFileSync,
12
+ writeFileSync,
13
+ readdirSync,
14
+ statSync,
15
+ mkdirSync,
16
+ existsSync,
17
+ } from 'fs'
18
+ import { join, basename, dirname, extname } from 'path'
19
+ import { fromTS } from '../../lang/emitters/from-ts'
20
+
21
+ export interface ConvertOptions {
22
+ output?: string
23
+ recursive?: boolean
24
+ verbose?: boolean
25
+ }
26
+
27
+ export async function convert(
28
+ input: string,
29
+ options: ConvertOptions = {}
30
+ ): Promise<void> {
31
+ const { output, recursive = true, verbose = false } = options
32
+ const stats = statSync(input)
33
+
34
+ if (stats.isFile()) {
35
+ // Single file conversion
36
+ await convertFile(input, output, verbose)
37
+ } else if (stats.isDirectory()) {
38
+ // Directory conversion
39
+ if (!output) {
40
+ console.error('Error: Output directory required for directory conversion')
41
+ console.error('Usage: tjs convert <dir> -o <outdir>')
42
+ process.exit(1)
43
+ }
44
+ await convertDirectory(input, output, recursive, verbose)
45
+ } else {
46
+ console.error(`Error: ${input} is not a file or directory`)
47
+ process.exit(1)
48
+ }
49
+ }
50
+
51
+ async function convertFile(
52
+ inputPath: string,
53
+ outputPath?: string,
54
+ verbose = false
55
+ ): Promise<void> {
56
+ const source = readFileSync(inputPath, 'utf-8')
57
+ const filename = basename(inputPath)
58
+
59
+ try {
60
+ const result = fromTS(source, { emitTJS: true, filename })
61
+
62
+ if (result.warnings && result.warnings.length > 0 && verbose) {
63
+ console.error(`Warnings for ${inputPath}:`)
64
+ for (const warning of result.warnings) {
65
+ console.error(` - ${warning}`)
66
+ }
67
+ }
68
+
69
+ if (outputPath) {
70
+ // Ensure output directory exists
71
+ const outDir = dirname(outputPath)
72
+ if (!existsSync(outDir)) {
73
+ mkdirSync(outDir, { recursive: true })
74
+ }
75
+ writeFileSync(outputPath, result.code)
76
+ console.log(`✓ ${inputPath} -> ${outputPath}`)
77
+ } else {
78
+ // Output to stdout
79
+ console.log(result.code)
80
+ }
81
+ } catch (error: any) {
82
+ console.error(`✗ ${inputPath}: ${error.message}`)
83
+ if (!outputPath) {
84
+ process.exit(1)
85
+ }
86
+ }
87
+ }
88
+
89
+ async function convertDirectory(
90
+ inputDir: string,
91
+ outputDir: string,
92
+ recursive: boolean,
93
+ verbose: boolean
94
+ ): Promise<void> {
95
+ const entries = readdirSync(inputDir)
96
+ let converted = 0
97
+ let failed = 0
98
+ let skipped = 0
99
+
100
+ for (const entry of entries) {
101
+ const inputPath = join(inputDir, entry)
102
+ const stats = statSync(inputPath)
103
+
104
+ if (stats.isDirectory() && recursive) {
105
+ // Recurse into subdirectory
106
+ const subOutputDir = join(outputDir, entry)
107
+ await convertDirectory(inputPath, subOutputDir, recursive, verbose)
108
+ } else if (stats.isFile() && extname(entry) === '.ts') {
109
+ // Skip test files and declaration files
110
+ if (entry.endsWith('.test.ts') || entry.endsWith('.d.ts')) {
111
+ skipped++
112
+ if (verbose) {
113
+ console.log(`- Skipping ${inputPath}`)
114
+ }
115
+ continue
116
+ }
117
+
118
+ const outputPath = join(outputDir, entry.replace(/\.ts$/, '.tjs'))
119
+ try {
120
+ await convertFile(inputPath, outputPath, verbose)
121
+ converted++
122
+ } catch {
123
+ failed++
124
+ }
125
+ }
126
+ }
127
+
128
+ if (verbose || converted > 0 || failed > 0) {
129
+ console.log(
130
+ `\nDirectory ${inputDir}: ${converted} converted, ${failed} failed, ${skipped} skipped`
131
+ )
132
+ }
133
+ }
@@ -0,0 +1,260 @@
1
+ /**
2
+ * tjs emit - Output transpiled JavaScript
3
+ *
4
+ * Usage:
5
+ * tjs emit <file.tjs> Emit single file to stdout
6
+ * tjs emit <file.tjs> -o <out.js> Emit single file to output
7
+ * tjs emit <dir> -o <outdir> Emit all .tjs files in directory
8
+ * tjs emit --unsafe <file.tjs> Emit without __tjs metadata (production)
9
+ * tjs emit --no-docs <file.tjs> Suppress documentation generation
10
+ * tjs emit --docs-dir <dir> Output docs to separate directory
11
+ * tjs emit --jfdi <file.tjs> Emit even if tests fail (just fucking do it)
12
+ */
13
+
14
+ import {
15
+ readFileSync,
16
+ writeFileSync,
17
+ readdirSync,
18
+ statSync,
19
+ mkdirSync,
20
+ existsSync,
21
+ } from 'fs'
22
+ import { join, basename, dirname, extname } from 'path'
23
+ import { tjs } from '../../lang'
24
+ import { generateDocs } from '../../lang/docs'
25
+
26
+ export interface EmitOptions {
27
+ /** Include source locations in __tjs metadata */
28
+ debug?: boolean
29
+ /** Output path (file or directory) */
30
+ output?: string
31
+ /** Strip __tjs metadata for production builds */
32
+ unsafe?: boolean
33
+ /** Recursive directory processing */
34
+ recursive?: boolean
35
+ /** Verbose output */
36
+ verbose?: boolean
37
+ /** Suppress documentation generation */
38
+ noDocs?: boolean
39
+ /** Output docs to separate directory (default: alongside JS) */
40
+ docsDir?: string
41
+ /** Emit even if tests fail (just fucking do it) */
42
+ jfdi?: boolean
43
+ }
44
+
45
+ export async function emit(
46
+ input: string,
47
+ options: EmitOptions = {}
48
+ ): Promise<void> {
49
+ const { output, recursive = true } = options
50
+
51
+ // Check if input exists
52
+ if (!existsSync(input)) {
53
+ console.error(`Error: ${input} does not exist`)
54
+ process.exit(1)
55
+ }
56
+
57
+ const stats = statSync(input)
58
+
59
+ if (stats.isFile()) {
60
+ await emitFile(input, output, options)
61
+ } else if (stats.isDirectory()) {
62
+ if (!output) {
63
+ console.error('Error: Output directory required for directory emit')
64
+ console.error('Usage: tjs emit <dir> -o <outdir>')
65
+ process.exit(1)
66
+ }
67
+ await emitDirectory(input, output, recursive, options)
68
+ } else {
69
+ console.error(`Error: ${input} is not a file or directory`)
70
+ process.exit(1)
71
+ }
72
+ }
73
+
74
+ async function emitFile(
75
+ inputPath: string,
76
+ outputPath: string | undefined,
77
+ options: EmitOptions
78
+ ): Promise<void> {
79
+ const source = readFileSync(inputPath, 'utf-8')
80
+ const filename = basename(inputPath)
81
+
82
+ try {
83
+ // Use 'report' mode to get test results without throwing
84
+ const result = tjs(source, {
85
+ filename,
86
+ debug: options.debug,
87
+ runTests: 'report',
88
+ })
89
+
90
+ // Check test results
91
+ const testResults = result.testResults || []
92
+ const failures = testResults.filter((r) => !r.passed)
93
+ const hasFailures = failures.length > 0
94
+
95
+ // Report test results
96
+ if (testResults.length > 0) {
97
+ const passed = testResults.filter((r) => r.passed).length
98
+ const failed = failures.length
99
+
100
+ if (options.verbose || hasFailures) {
101
+ if (hasFailures) {
102
+ console.log(`\n${inputPath}: ${passed} passed, ${failed} failed`)
103
+ for (const f of failures) {
104
+ if (f.isSignatureTest) {
105
+ console.log(` ✗ Signature: ${f.error}`)
106
+ } else {
107
+ console.log(` ✗ ${f.description}: ${f.error}`)
108
+ }
109
+ }
110
+ } else if (options.verbose) {
111
+ console.log(` ✓ ${testResults.length} tests passed`)
112
+ }
113
+ }
114
+ }
115
+
116
+ // Don't emit if tests failed (unless --jfdi)
117
+ if (hasFailures && !options.jfdi) {
118
+ if (!options.verbose) {
119
+ // Show failures even in non-verbose mode
120
+ console.error(`✗ ${inputPath}: ${failures.length} test(s) failed`)
121
+ for (const f of failures) {
122
+ if (f.isSignatureTest) {
123
+ console.error(` Signature: ${f.error}`)
124
+ } else {
125
+ console.error(` ${f.description}: ${f.error}`)
126
+ }
127
+ }
128
+ }
129
+ console.error(` (use --jfdi to emit anyway)`)
130
+ return
131
+ }
132
+
133
+ let code = result.code
134
+
135
+ // Strip __tjs metadata if unsafe mode
136
+ if (options.unsafe) {
137
+ code = stripTJSMetadata(code)
138
+ }
139
+
140
+ if (outputPath) {
141
+ // Ensure output directory exists
142
+ const outDir = dirname(outputPath)
143
+ if (outDir && !existsSync(outDir)) {
144
+ mkdirSync(outDir, { recursive: true })
145
+ }
146
+ writeFileSync(outputPath, code)
147
+ if (options.verbose) {
148
+ const suffix = hasFailures ? ' (tests failed, --jfdi)' : ''
149
+ console.log(`✓ ${inputPath} -> ${outputPath}${suffix}`)
150
+ }
151
+
152
+ // Generate docs unless suppressed
153
+ if (!options.noDocs) {
154
+ try {
155
+ const docs = generateDocs(source)
156
+ const docsPath = options.docsDir
157
+ ? join(
158
+ options.docsDir,
159
+ basename(outputPath).replace(/\.js$/, '.md')
160
+ )
161
+ : outputPath.replace(/\.js$/, '.md')
162
+
163
+ // Ensure docs directory exists
164
+ const docsDir = dirname(docsPath)
165
+ if (docsDir && !existsSync(docsDir)) {
166
+ mkdirSync(docsDir, { recursive: true })
167
+ }
168
+
169
+ writeFileSync(docsPath, docs.markdown)
170
+ if (options.verbose) {
171
+ console.log(` 📄 ${docsPath}`)
172
+ }
173
+ } catch (docsError: any) {
174
+ // Don't fail emit if docs generation fails
175
+ if (options.verbose) {
176
+ console.log(` ⚠ docs skipped: ${docsError.message}`)
177
+ }
178
+ }
179
+ }
180
+ } else {
181
+ // Output to stdout
182
+ console.log(code)
183
+ }
184
+ } catch (error: any) {
185
+ // This is a real transpilation error (syntax, parse, etc.)
186
+ console.error(`✗ ${inputPath}: ${error.message}`)
187
+ if (!outputPath) {
188
+ process.exit(1)
189
+ }
190
+ }
191
+ }
192
+
193
+ async function emitDirectory(
194
+ inputDir: string,
195
+ outputDir: string,
196
+ recursive: boolean,
197
+ options: EmitOptions
198
+ ): Promise<void> {
199
+ const entries = readdirSync(inputDir)
200
+ let emitted = 0
201
+ let failed = 0
202
+ let skipped = 0
203
+
204
+ for (const entry of entries) {
205
+ const inputPath = join(inputDir, entry)
206
+ const stats = statSync(inputPath)
207
+
208
+ if (stats.isDirectory()) {
209
+ if (recursive && !entry.startsWith('.') && entry !== 'node_modules') {
210
+ const subOutputDir = join(outputDir, entry)
211
+ await emitDirectory(inputPath, subOutputDir, recursive, options)
212
+ }
213
+ } else if (stats.isFile() && extname(entry) === '.tjs') {
214
+ // Skip test files for production emit
215
+ if (entry.endsWith('.test.tjs')) {
216
+ skipped++
217
+ if (options.verbose) {
218
+ console.log(`- Skipping test: ${inputPath}`)
219
+ }
220
+ continue
221
+ }
222
+
223
+ const outputPath = join(outputDir, entry.replace(/\.tjs$/, '.js'))
224
+ try {
225
+ await emitFile(inputPath, outputPath, { ...options, verbose: true })
226
+ emitted++
227
+ } catch {
228
+ failed++
229
+ }
230
+ }
231
+ }
232
+
233
+ if (options.verbose || emitted > 0 || failed > 0) {
234
+ console.log(
235
+ `\n${inputDir}: ${emitted} emitted, ${failed} failed, ${skipped} skipped`
236
+ )
237
+ }
238
+ }
239
+
240
+ /**
241
+ * Strip __tjs metadata from transpiled code for production builds.
242
+ * This removes runtime validation, giving pure JS performance.
243
+ */
244
+ function stripTJSMetadata(code: string): string {
245
+ // Remove __tjs property assignments from function declarations
246
+ // Pattern: function foo(x) { ... }\nfoo.__tjs = { ... };
247
+ code = code.replace(/\n\w+\.__tjs\s*=\s*\{[^}]*\};?/g, '')
248
+
249
+ // Remove __tjs from object method shorthand and arrow functions
250
+ // Pattern: const foo = (x) => { ... };\nfoo.__tjs = { ... };
251
+ code = code.replace(/;\s*\w+\.__tjs\s*=\s*\{[^}]*\};?/g, ';')
252
+
253
+ // Remove standalone __tjs assignments that might remain
254
+ code = code.replace(/^\w+\.__tjs\s*=\s*\{[^}]*\};?\n?/gm, '')
255
+
256
+ // Clean up any double newlines left behind
257
+ code = code.replace(/\n{3,}/g, '\n\n')
258
+
259
+ return code
260
+ }
@@ -0,0 +1,68 @@
1
+ /**
2
+ * tjs run - Transpile and execute a TJS file
3
+ *
4
+ * Executes the entire file as a script, with full TJS runtime support.
5
+ */
6
+
7
+ import { readFileSync } from 'fs'
8
+ import { resolve } from 'path'
9
+ import { preprocess } from '../../lang/parser'
10
+ import { transpileToJS } from '../../lang/emitters/js'
11
+ import * as runtime from '../../lang/runtime'
12
+
13
+ export async function run(file: string): Promise<void> {
14
+ const absolutePath = resolve(file)
15
+ const source = readFileSync(absolutePath, 'utf-8')
16
+
17
+ try {
18
+ // Preprocess: transforms Type, Generic, Union declarations, runs tests
19
+ const preprocessed = preprocess(source)
20
+
21
+ if (preprocessed.testErrors.length > 0) {
22
+ console.error('Test failures:')
23
+ for (const err of preprocessed.testErrors) {
24
+ console.error(` ${err.description}: ${err.error}`)
25
+ }
26
+ process.exit(1)
27
+ }
28
+
29
+ // Transpile to JS
30
+ const result = transpileToJS(preprocessed.source)
31
+
32
+ if (result.warnings && result.warnings.length > 0) {
33
+ for (const warning of result.warnings) {
34
+ console.warn(`Warning: ${warning.message}`)
35
+ }
36
+ }
37
+
38
+ // Create a module-like execution context with TJS runtime
39
+ const AsyncFunction = Object.getPrototypeOf(
40
+ async function () {}
41
+ ).constructor
42
+
43
+ // Wrap code in an async IIFE to support top-level await
44
+ const wrappedCode = `
45
+ const { Type, Generic, Union, Enum, isRuntimeType, wrap, error, isError } = __runtime__;
46
+ ${result.code}
47
+ `
48
+
49
+ const fn = new AsyncFunction('__runtime__', wrappedCode)
50
+ await fn(runtime)
51
+ } catch (error: any) {
52
+ if (error.name === 'SyntaxError' && error.formatWithContext) {
53
+ // Use enhanced error formatting with source context
54
+ console.error(`Syntax error in ${file}:\n`)
55
+ console.error(error.formatWithContext(2))
56
+ console.error()
57
+ } else if (error.name === 'SyntaxError') {
58
+ console.error(`Syntax error in ${file}:`)
59
+ console.error(` ${error.message}`)
60
+ if (error.line) {
61
+ console.error(` at line ${error.line}, column ${error.column}`)
62
+ }
63
+ } else {
64
+ console.error(`Runtime error: ${error.message}`)
65
+ }
66
+ process.exit(1)
67
+ }
68
+ }
@@ -0,0 +1,194 @@
1
+ /**
2
+ * tjs test - Run TJS test files
3
+ *
4
+ * Usage:
5
+ * tjs test Run all .test.tjs files
6
+ * tjs test <file> Run specific test file
7
+ * tjs test <dir> Run all .test.tjs files in directory
8
+ * tjs test -t <pattern> Run tests matching pattern
9
+ *
10
+ * This command wraps `bun test` with the TJS plugin preloaded,
11
+ * and generates temporary wrapper files for .test.tjs files since
12
+ * Bun's test runner only recognizes standard extensions.
13
+ */
14
+
15
+ import {
16
+ readdirSync,
17
+ statSync,
18
+ writeFileSync,
19
+ unlinkSync,
20
+ existsSync,
21
+ mkdirSync,
22
+ } from 'fs'
23
+ import { join, dirname, resolve, relative } from 'path'
24
+ import { spawn } from 'bun'
25
+
26
+ export interface TestOptions {
27
+ pattern?: string // -t, --test-name-pattern
28
+ timeout?: number // --timeout
29
+ watch?: boolean // --watch
30
+ coverage?: boolean // --coverage
31
+ bail?: number // --bail
32
+ }
33
+
34
+ // Find all .test.tjs files recursively
35
+ function findTestFiles(dir: string, files: string[] = []): string[] {
36
+ const entries = readdirSync(dir)
37
+
38
+ for (const entry of entries) {
39
+ const fullPath = join(dir, entry)
40
+ const stats = statSync(fullPath)
41
+
42
+ if (
43
+ stats.isDirectory() &&
44
+ !entry.startsWith('.') &&
45
+ entry !== 'node_modules'
46
+ ) {
47
+ findTestFiles(fullPath, files)
48
+ } else if (stats.isFile() && entry.endsWith('.test.tjs')) {
49
+ files.push(fullPath)
50
+ }
51
+ }
52
+
53
+ return files
54
+ }
55
+
56
+ // Get the plugin path relative to cwd
57
+ function getPluginPath(): string {
58
+ // Find the tjs-plugin relative to this file
59
+ const pluginPath = resolve(
60
+ dirname(import.meta.path),
61
+ '../../bun-plugin/tjs-plugin.ts'
62
+ )
63
+ return pluginPath
64
+ }
65
+
66
+ // Create temporary wrapper directory
67
+ function getTempDir(): string {
68
+ const tempDir = join(process.cwd(), '.tjs-test-temp')
69
+ if (!existsSync(tempDir)) {
70
+ mkdirSync(tempDir, { recursive: true })
71
+ }
72
+ return tempDir
73
+ }
74
+
75
+ // Generate wrapper .test.ts files that import .test.tjs files
76
+ function generateWrappers(testFiles: string[], tempDir: string): string[] {
77
+ const wrappers: string[] = []
78
+
79
+ for (const testFile of testFiles) {
80
+ const relativePath = relative(tempDir, testFile)
81
+
82
+ // Handle potential name collisions by including directory info
83
+ const uniqueName = testFile
84
+ .replace(process.cwd(), '')
85
+ .replace(/[/\\]/g, '_')
86
+ .replace('.test.tjs', '.test.ts')
87
+ .replace(/^_/, '')
88
+ const uniqueWrapperPath = join(tempDir, uniqueName)
89
+
90
+ const wrapperContent = `// Auto-generated wrapper for TJS test\nimport '${relativePath}';\n`
91
+ writeFileSync(uniqueWrapperPath, wrapperContent)
92
+ wrappers.push(uniqueWrapperPath)
93
+ }
94
+
95
+ return wrappers
96
+ }
97
+
98
+ // Clean up wrapper files
99
+ function cleanupWrappers(wrappers: string[], tempDir: string): void {
100
+ for (const wrapper of wrappers) {
101
+ try {
102
+ unlinkSync(wrapper)
103
+ } catch {
104
+ // Ignore cleanup errors
105
+ }
106
+ }
107
+
108
+ // Try to remove temp directory if empty
109
+ try {
110
+ const remaining = readdirSync(tempDir)
111
+ if (remaining.length === 0) {
112
+ unlinkSync(tempDir)
113
+ }
114
+ } catch {
115
+ // Ignore
116
+ }
117
+ }
118
+
119
+ export async function test(
120
+ input?: string,
121
+ options: TestOptions = {}
122
+ ): Promise<void> {
123
+ const pluginPath = getPluginPath()
124
+
125
+ // Determine what to test
126
+ let testFiles: string[] = []
127
+
128
+ if (!input) {
129
+ // Find all .test.tjs files in current directory
130
+ testFiles = findTestFiles(process.cwd())
131
+ } else if (input.endsWith('.test.tjs')) {
132
+ // Single test file
133
+ testFiles = [resolve(input)]
134
+ } else if (existsSync(input) && statSync(input).isDirectory()) {
135
+ // Directory
136
+ testFiles = findTestFiles(resolve(input))
137
+ } else {
138
+ // Treat as a filter pattern for bun test
139
+ testFiles = findTestFiles(process.cwd())
140
+ }
141
+
142
+ if (testFiles.length === 0) {
143
+ console.log('No .test.tjs files found')
144
+ return
145
+ }
146
+
147
+ console.log(`Found ${testFiles.length} TJS test file(s)`)
148
+
149
+ // Generate wrapper files
150
+ const tempDir = getTempDir()
151
+ const wrappers = generateWrappers(testFiles, tempDir)
152
+
153
+ try {
154
+ // Build bun test command
155
+ const args = ['test', '--preload', pluginPath]
156
+
157
+ if (options.pattern) {
158
+ args.push('--test-name-pattern', options.pattern)
159
+ }
160
+ if (options.timeout) {
161
+ args.push('--timeout', String(options.timeout))
162
+ }
163
+ if (options.coverage) {
164
+ args.push('--coverage')
165
+ }
166
+ if (options.bail !== undefined) {
167
+ args.push('--bail', String(options.bail))
168
+ }
169
+
170
+ // Add wrapper files
171
+ args.push(...wrappers)
172
+
173
+ // Run bun test
174
+ const proc = spawn({
175
+ cmd: ['bun', ...args],
176
+ cwd: process.cwd(),
177
+ stdout: 'inherit',
178
+ stderr: 'inherit',
179
+ })
180
+
181
+ const exitCode = await proc.exited
182
+
183
+ // Cleanup
184
+ cleanupWrappers(wrappers, tempDir)
185
+
186
+ if (exitCode !== 0) {
187
+ process.exit(exitCode)
188
+ }
189
+ } catch (error) {
190
+ // Cleanup on error
191
+ cleanupWrappers(wrappers, tempDir)
192
+ throw error
193
+ }
194
+ }