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.
- package/CONTEXT.md +594 -0
- package/LICENSE +190 -0
- package/README.md +220 -0
- package/bin/benchmarks.ts +351 -0
- package/bin/dev.ts +205 -0
- package/bin/docs.js +170 -0
- package/bin/install-cursor.sh +71 -0
- package/bin/install-vscode.sh +71 -0
- package/bin/select-local-models.d.ts +1 -0
- package/bin/select-local-models.js +28 -0
- package/bin/select-local-models.ts +31 -0
- package/demo/autocomplete.test.ts +232 -0
- package/demo/docs.json +186 -0
- package/demo/examples.test.ts +598 -0
- package/demo/index.html +91 -0
- package/demo/src/autocomplete.ts +482 -0
- package/demo/src/capabilities.ts +859 -0
- package/demo/src/demo-nav.ts +2097 -0
- package/demo/src/examples.test.ts +161 -0
- package/demo/src/examples.ts +476 -0
- package/demo/src/imports.test.ts +196 -0
- package/demo/src/imports.ts +421 -0
- package/demo/src/index.ts +639 -0
- package/demo/src/module-store.ts +635 -0
- package/demo/src/module-sw.ts +132 -0
- package/demo/src/playground.ts +949 -0
- package/demo/src/service-host.ts +389 -0
- package/demo/src/settings.ts +440 -0
- package/demo/src/style.ts +280 -0
- package/demo/src/tjs-playground.ts +1605 -0
- package/demo/src/ts-examples.ts +478 -0
- package/demo/src/ts-playground.ts +1092 -0
- package/demo/static/favicon.svg +30 -0
- package/demo/static/photo-1.jpg +0 -0
- package/demo/static/photo-2.jpg +0 -0
- package/demo/static/texts/ai-history.txt +9 -0
- package/demo/static/texts/coffee-origins.txt +9 -0
- package/demo/static/texts/renewable-energy.txt +9 -0
- package/dist/index.js +256 -0
- package/dist/index.js.map +37 -0
- package/dist/tjs-batteries.js +4 -0
- package/dist/tjs-batteries.js.map +15 -0
- package/dist/tjs-full.js +256 -0
- package/dist/tjs-full.js.map +37 -0
- package/dist/tjs-transpiler.js +220 -0
- package/dist/tjs-transpiler.js.map +21 -0
- package/dist/tjs-vm.js +4 -0
- package/dist/tjs-vm.js.map +14 -0
- package/docs/CNAME +1 -0
- package/docs/favicon.svg +30 -0
- package/docs/index.html +91 -0
- package/docs/index.js +10468 -0
- package/docs/index.js.map +92 -0
- package/docs/photo-1.jpg +0 -0
- package/docs/photo-1.webp +0 -0
- package/docs/photo-2.jpg +0 -0
- package/docs/photo-2.webp +0 -0
- package/docs/texts/ai-history.txt +9 -0
- package/docs/texts/coffee-origins.txt +9 -0
- package/docs/texts/renewable-energy.txt +9 -0
- package/docs/tjs-lang.svg +31 -0
- package/docs/tosijs-agent.svg +31 -0
- package/editors/README.md +325 -0
- package/editors/ace/ajs-mode.js +328 -0
- package/editors/ace/ajs-mode.ts +269 -0
- package/editors/ajs-syntax.ts +212 -0
- package/editors/build-grammars.ts +510 -0
- package/editors/codemirror/ajs-language.js +287 -0
- package/editors/codemirror/ajs-language.ts +1447 -0
- package/editors/codemirror/autocomplete.test.ts +531 -0
- package/editors/codemirror/component.ts +404 -0
- package/editors/monaco/ajs-monarch.js +243 -0
- package/editors/monaco/ajs-monarch.ts +225 -0
- package/editors/tjs-syntax.ts +115 -0
- package/editors/vscode/language-configuration.json +37 -0
- package/editors/vscode/package.json +65 -0
- package/editors/vscode/syntaxes/ajs-injection.tmLanguage.json +107 -0
- package/editors/vscode/syntaxes/ajs.tmLanguage.json +252 -0
- package/editors/vscode/syntaxes/tjs.tmLanguage.json +333 -0
- package/package.json +83 -0
- package/src/cli/commands/check.ts +41 -0
- package/src/cli/commands/convert.ts +133 -0
- package/src/cli/commands/emit.ts +260 -0
- package/src/cli/commands/run.ts +68 -0
- package/src/cli/commands/test.ts +194 -0
- package/src/cli/commands/types.ts +20 -0
- package/src/cli/create-app.ts +236 -0
- package/src/cli/playground.ts +250 -0
- package/src/cli/tjs.ts +166 -0
- package/src/cli/tjsx.ts +160 -0
- 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
|
+
}
|