tjs-lang 0.5.4 → 0.6.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tjs-lang",
3
- "version": "0.5.4",
3
+ "version": "0.6.0",
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",
@@ -6,6 +6,7 @@
6
6
  * tjs emit <file.tjs> -o <out.js> Emit single file to output
7
7
  * tjs emit <dir> -o <outdir> Emit all .tjs files in directory
8
8
  * tjs emit --unsafe <file.tjs> Emit without __tjs metadata (production)
9
+ * tjs emit --dts <file.tjs> Also generate .d.ts declaration file
9
10
  * tjs emit --no-docs <file.tjs> Suppress documentation generation
10
11
  * tjs emit --docs-dir <dir> Output docs to separate directory
11
12
  * tjs emit --jfdi <file.tjs> Emit even if tests fail (just fucking do it)
@@ -22,6 +23,7 @@ import {
22
23
  import { join, basename, dirname, extname } from 'path'
23
24
  import { tjs } from '../../lang'
24
25
  import { generateDocs } from '../../lang/docs'
26
+ import { generateDTS } from '../../lang/emitters/dts'
25
27
 
26
28
  export interface EmitOptions {
27
29
  /** Include source locations in __tjs metadata */
@@ -40,6 +42,8 @@ export interface EmitOptions {
40
42
  docsDir?: string
41
43
  /** Emit even if tests fail (just fucking do it) */
42
44
  jfdi?: boolean
45
+ /** Generate .d.ts TypeScript declaration file alongside JS */
46
+ dts?: boolean
43
47
  }
44
48
 
45
49
  export async function emit(
@@ -149,6 +153,28 @@ async function emitFile(
149
153
  console.log(`✓ ${inputPath} -> ${outputPath}${suffix}`)
150
154
  }
151
155
 
156
+ // Generate .d.ts if requested
157
+ if (options.dts) {
158
+ try {
159
+ const dtsContent = generateDTS(result, source)
160
+ const dtsPath = outputPath.replace(/\.js$/, '.d.ts')
161
+
162
+ const dtsDir = dirname(dtsPath)
163
+ if (dtsDir && !existsSync(dtsDir)) {
164
+ mkdirSync(dtsDir, { recursive: true })
165
+ }
166
+
167
+ writeFileSync(dtsPath, dtsContent)
168
+ if (options.verbose) {
169
+ console.log(` ${dtsPath}`)
170
+ }
171
+ } catch (dtsError: any) {
172
+ if (options.verbose) {
173
+ console.log(` .d.ts skipped: ${dtsError.message}`)
174
+ }
175
+ }
176
+ }
177
+
152
178
  // Generate docs unless suppressed
153
179
  if (!options.noDocs) {
154
180
  try {
package/src/cli/tjs.ts CHANGED
@@ -20,7 +20,7 @@ import { emit } from './commands/emit'
20
20
  import { convert } from './commands/convert'
21
21
  import { test } from './commands/test'
22
22
 
23
- const VERSION = '0.1.0'
23
+ const VERSION = '0.6.0'
24
24
 
25
25
  const HELP = `
26
26
  tjs - Typed JavaScript CLI
@@ -41,6 +41,7 @@ Options:
41
41
  -v, --version Show version
42
42
  --debug Include source locations in __tjs metadata (emit command)
43
43
  --unsafe Strip __tjs metadata for production builds (emit command)
44
+ --dts Generate .d.ts declaration file (emit command)
44
45
  --no-docs Suppress documentation generation (emit command)
45
46
  --docs-dir <d> Output docs to separate directory (emit command)
46
47
  --jfdi Emit even if tests fail (just fucking do it)
@@ -82,6 +83,7 @@ async function main() {
82
83
  const verbose = args.includes('--verbose') || args.includes('-V')
83
84
  const unsafe = args.includes('--unsafe')
84
85
  const noDocs = args.includes('--no-docs')
86
+ const dts = args.includes('--dts')
85
87
  const jfdi = args.includes('--jfdi')
86
88
  const emitTJS = args.includes('--emit-tjs')
87
89
 
@@ -145,6 +147,7 @@ async function main() {
145
147
  output,
146
148
  verbose,
147
149
  noDocs,
150
+ dts,
148
151
  docsDir,
149
152
  jfdi,
150
153
  })
@@ -1988,3 +1988,58 @@ function divide(a: 10, b: 2) -? { value: 0, error = '' } {
1988
1988
  })
1989
1989
  })
1990
1990
  })
1991
+
1992
+ describe('TS overloads → TJS → JS full pipeline', () => {
1993
+ it('dispatches by arity at runtime', () => {
1994
+ const tsSource = `
1995
+ function greet(name: string): string;
1996
+ function greet(name: string, greeting: string): string;
1997
+ function greet(name: any, greeting?: any): string {
1998
+ return greeting ? greeting + ', ' + name : 'Hello, ' + name;
1999
+ }
2000
+ `
2001
+ const tjsResult = fromTS(tsSource, { emitTJS: true })
2002
+ const jsResult = tjs(tjsResult.code)
2003
+
2004
+ const savedTjs = globalThis.__tjs
2005
+ const { createRuntime } = require('./runtime')
2006
+ try {
2007
+ globalThis.__tjs = createRuntime()
2008
+ const code = jsResult.code.replace(/^const __tjs =.*\n/m, '')
2009
+ const fn = new Function('__tjs', code + '; return greet')(
2010
+ globalThis.__tjs
2011
+ )
2012
+ expect(fn('World')).toBe('Hello, World')
2013
+ expect(fn('World', 'Hi')).toBe('Hi, World')
2014
+ } finally {
2015
+ globalThis.__tjs = savedTjs
2016
+ }
2017
+ })
2018
+
2019
+ it('dispatches by type at same arity', () => {
2020
+ const tsSource = `
2021
+ function process(x: string): string;
2022
+ function process(x: number): number;
2023
+ function process(x: any): any {
2024
+ if (typeof x === 'string') return x.toUpperCase();
2025
+ return x * 2;
2026
+ }
2027
+ `
2028
+ const tjsResult = fromTS(tsSource, { emitTJS: true })
2029
+ const jsResult = tjs(tjsResult.code)
2030
+
2031
+ const savedTjs = globalThis.__tjs
2032
+ const { createRuntime } = require('./runtime')
2033
+ try {
2034
+ globalThis.__tjs = createRuntime()
2035
+ const code = jsResult.code.replace(/^const __tjs =.*\n/m, '')
2036
+ const fn = new Function('__tjs', code + '; return process')(
2037
+ globalThis.__tjs
2038
+ )
2039
+ expect(fn('hello')).toBe('HELLO')
2040
+ expect(fn(42)).toBe(84)
2041
+ } finally {
2042
+ globalThis.__tjs = savedTjs
2043
+ }
2044
+ })
2045
+ })
@@ -0,0 +1,406 @@
1
+ import { describe, it, expect } from 'bun:test'
2
+ import { typeDescriptorToTS, generateDTS } from './dts'
3
+ import type { TypeDescriptor } from '../types'
4
+ import { transpileToJS } from './js'
5
+
6
+ describe('typeDescriptorToTS', () => {
7
+ it('should convert primitive kinds', () => {
8
+ expect(typeDescriptorToTS({ kind: 'string' })).toBe('string')
9
+ expect(typeDescriptorToTS({ kind: 'number' })).toBe('number')
10
+ expect(typeDescriptorToTS({ kind: 'boolean' })).toBe('boolean')
11
+ expect(typeDescriptorToTS({ kind: 'null' })).toBe('null')
12
+ expect(typeDescriptorToTS({ kind: 'undefined' })).toBe('undefined')
13
+ expect(typeDescriptorToTS({ kind: 'any' })).toBe('any')
14
+ })
15
+
16
+ it('should map integer and non-negative-integer to number', () => {
17
+ expect(typeDescriptorToTS({ kind: 'integer' })).toBe('number')
18
+ expect(typeDescriptorToTS({ kind: 'non-negative-integer' })).toBe('number')
19
+ })
20
+
21
+ it('should handle nullable types', () => {
22
+ expect(typeDescriptorToTS({ kind: 'string', nullable: true })).toBe(
23
+ 'string | null'
24
+ )
25
+ expect(typeDescriptorToTS({ kind: 'integer', nullable: true })).toBe(
26
+ 'number | null'
27
+ )
28
+ })
29
+
30
+ it('should handle arrays', () => {
31
+ expect(
32
+ typeDescriptorToTS({ kind: 'array', items: { kind: 'string' } })
33
+ ).toBe('string[]')
34
+ expect(
35
+ typeDescriptorToTS({ kind: 'array', items: { kind: 'number' } })
36
+ ).toBe('number[]')
37
+ expect(typeDescriptorToTS({ kind: 'array' })).toBe('any[]')
38
+ })
39
+
40
+ it('should wrap union items in parens for arrays', () => {
41
+ const td: TypeDescriptor = {
42
+ kind: 'array',
43
+ items: {
44
+ kind: 'union',
45
+ members: [{ kind: 'string' }, { kind: 'number' }],
46
+ },
47
+ }
48
+ expect(typeDescriptorToTS(td)).toBe('(string | number)[]')
49
+ })
50
+
51
+ it('should handle object shapes', () => {
52
+ const td: TypeDescriptor = {
53
+ kind: 'object',
54
+ shape: {
55
+ name: { kind: 'string' },
56
+ age: { kind: 'integer' },
57
+ },
58
+ }
59
+ expect(typeDescriptorToTS(td)).toBe('{ name: string; age: number }')
60
+ })
61
+
62
+ it('should handle empty objects', () => {
63
+ expect(typeDescriptorToTS({ kind: 'object' })).toBe('Record<string, any>')
64
+ expect(typeDescriptorToTS({ kind: 'object', shape: {} })).toBe(
65
+ 'Record<string, any>'
66
+ )
67
+ })
68
+
69
+ it('should handle unions', () => {
70
+ const td: TypeDescriptor = {
71
+ kind: 'union',
72
+ members: [{ kind: 'string' }, { kind: 'integer' }],
73
+ }
74
+ expect(typeDescriptorToTS(td)).toBe('string | number')
75
+ })
76
+
77
+ it('should handle nested object in array', () => {
78
+ const td: TypeDescriptor = {
79
+ kind: 'array',
80
+ items: {
81
+ kind: 'object',
82
+ shape: {
83
+ id: { kind: 'integer' },
84
+ label: { kind: 'string' },
85
+ },
86
+ },
87
+ }
88
+ expect(typeDescriptorToTS(td)).toBe('{ id: number; label: string }[]')
89
+ })
90
+
91
+ it('should handle nullable object', () => {
92
+ const td: TypeDescriptor = {
93
+ kind: 'object',
94
+ nullable: true,
95
+ shape: { x: { kind: 'number' } },
96
+ }
97
+ expect(typeDescriptorToTS(td)).toBe('{ x: number } | null')
98
+ })
99
+ })
100
+
101
+ describe('generateDTS', () => {
102
+ it('should generate declarations for exported functions', () => {
103
+ const source = `
104
+ export function greet(name: 'Alice') -> '' {
105
+ return \`Hello, \${name}!\`
106
+ }
107
+ `
108
+ const result = transpileToJS(source, { runTests: false })
109
+ const dts = generateDTS(result, source)
110
+
111
+ expect(dts).toContain('export declare function greet(')
112
+ expect(dts).toContain('name: string')
113
+ expect(dts).toContain('): string;')
114
+ })
115
+
116
+ it('should handle optional parameters', () => {
117
+ const source = `
118
+ export function greet(name = 'world') -> '' {
119
+ return \`Hello, \${name}!\`
120
+ }
121
+ `
122
+ const result = transpileToJS(source, { runTests: false })
123
+ const dts = generateDTS(result, source)
124
+
125
+ expect(dts).toContain('name?: string')
126
+ })
127
+
128
+ it('should handle multiple parameters and return types', () => {
129
+ const source = `
130
+ export function add(a: 0, b: 0) -> 0 {
131
+ return a + b
132
+ }
133
+ `
134
+ const result = transpileToJS(source, { runTests: false })
135
+ const dts = generateDTS(result, source)
136
+
137
+ expect(dts).toContain('a: number')
138
+ expect(dts).toContain('b: number')
139
+ expect(dts).toContain('): number;')
140
+ })
141
+
142
+ it('should skip non-exported functions when exports exist', () => {
143
+ const source = `
144
+ function helper(x: '') -> '' {
145
+ return x.toUpperCase()
146
+ }
147
+
148
+ export function greet(name: 'Alice') -> '' {
149
+ return helper(name)
150
+ }
151
+ `
152
+ const result = transpileToJS(source, { runTests: false })
153
+ const dts = generateDTS(result, source)
154
+
155
+ expect(dts).toContain('export declare function greet(')
156
+ expect(dts).not.toContain('helper')
157
+ })
158
+
159
+ it('should treat all functions as exported when no exports exist', () => {
160
+ const source = `
161
+ function add(a: 0, b: 0) -> 0 {
162
+ return a + b
163
+ }
164
+ `
165
+ const result = transpileToJS(source, { runTests: false })
166
+ const dts = generateDTS(result, source)
167
+
168
+ expect(dts).toContain('declare function add(')
169
+ })
170
+
171
+ it('should handle object parameter shapes', () => {
172
+ const source = `
173
+ export function createUser(user: { name: '', age: 0 }) -> { id: 0, name: '' } {
174
+ return { id: 1, name: user.name }
175
+ }
176
+ `
177
+ const result = transpileToJS(source, { runTests: false })
178
+ const dts = generateDTS(result, source)
179
+
180
+ expect(dts).toContain('user: { name: string; age: number }')
181
+ expect(dts).toContain('): { id: number; name: string };')
182
+ })
183
+
184
+ it('should handle nullable parameters', () => {
185
+ const source = `
186
+ export function find(id: 0 | null) -> '' {
187
+ return id ? 'found' : 'not found'
188
+ }
189
+ `
190
+ const result = transpileToJS(source, { runTests: false })
191
+ const dts = generateDTS(result, source)
192
+
193
+ expect(dts).toContain('id: number | null')
194
+ })
195
+
196
+ it('should skip polymorphic variant functions', () => {
197
+ const source = `
198
+ export function area(radius: 3.14) {
199
+ return Math.PI * radius * radius
200
+ }
201
+ export function area(w: 0.0, h: 0.0) {
202
+ return w * h
203
+ }
204
+ `
205
+ const result = transpileToJS(source, { runTests: false })
206
+ const dts = generateDTS(result, source)
207
+
208
+ const areaCount = (dts.match(/function area/g) || []).length
209
+ expect(areaCount).toBe(1)
210
+ expect(dts).not.toContain('area$')
211
+ })
212
+
213
+ it('should include JSDoc from TDoc comments', () => {
214
+ const source = `
215
+ /*# Greet a person by name */
216
+ function greet(name: 'Alice') -> '' {
217
+ return \`Hello, \${name}!\`
218
+ }
219
+ `
220
+ const result = transpileToJS(source, { runTests: false })
221
+ const dts = generateDTS(result, source)
222
+
223
+ expect(dts).toContain('/** Greet a person by name */')
224
+ })
225
+
226
+ it('should wrap in module declaration when moduleName is given', () => {
227
+ const source = `
228
+ export function add(a: 0, b: 0) -> 0 {
229
+ return a + b
230
+ }
231
+ `
232
+ const result = transpileToJS(source, { runTests: false })
233
+ const dts = generateDTS(result, source, { moduleName: 'my-lib' })
234
+
235
+ expect(dts).toContain("declare module 'my-lib'")
236
+ expect(dts).toContain(' export declare function add(')
237
+ expect(dts).toContain('}')
238
+ })
239
+
240
+ it('should handle array return types', () => {
241
+ const source = `
242
+ export function getNames(count: 0) -> [''] {
243
+ return Array(count).fill('test')
244
+ }
245
+ `
246
+ const result = transpileToJS(source, { runTests: false })
247
+ const dts = generateDTS(result, source)
248
+
249
+ expect(dts).toContain('): string[];')
250
+ })
251
+ })
252
+
253
+ describe('generateDTS — classes', () => {
254
+ it('should emit exported class as callable function returning any', () => {
255
+ const source = `
256
+ export class Point {
257
+ constructor(x: 0.0, y: 0.0) {
258
+ this.x = x
259
+ this.y = y
260
+ }
261
+ }
262
+ `
263
+ const result = transpileToJS(source, { runTests: false })
264
+ const dts = generateDTS(result, source)
265
+
266
+ // Callable form (matches TJS wrapClass behavior)
267
+ expect(dts).toContain(
268
+ 'export declare function Point(x: number, y: number): any;'
269
+ )
270
+ // Also class form for `new` usage
271
+ expect(dts).toContain('export declare class Point {')
272
+ expect(dts).toContain('constructor(x: number, y: number);')
273
+ })
274
+
275
+ it('should emit class with methods', () => {
276
+ const source = `
277
+ export class Vec {
278
+ constructor(x: 0.0, y: 0.0) {
279
+ this.x = x
280
+ this.y = y
281
+ }
282
+
283
+ add(other: { x: 0.0, y: 0.0 }) {
284
+ return { x: this.x + other.x, y: this.y + other.y }
285
+ }
286
+ }
287
+ `
288
+ const result = transpileToJS(source, { runTests: false })
289
+ const dts = generateDTS(result, source)
290
+
291
+ expect(dts).toContain('add(other: Record<string, any>): any;')
292
+ })
293
+
294
+ it('should handle class exported via export { Name }', () => {
295
+ const source = `
296
+ class DateTime {
297
+ constructor(initial: '' | 0 | null) {
298
+ this.value = initial
299
+ }
300
+ }
301
+
302
+ export { DateTime }
303
+ `
304
+ const result = transpileToJS(source, { runTests: false })
305
+ const dts = generateDTS(result, source)
306
+
307
+ expect(dts).toContain('export declare function DateTime(')
308
+ expect(dts).toContain('initial: string | number | null')
309
+ })
310
+
311
+ it('should not emit non-exported class when exports exist', () => {
312
+ const source = `
313
+ class Internal {
314
+ constructor(x: 0) {
315
+ this.x = x
316
+ }
317
+ }
318
+
319
+ export function make(n: 0) {
320
+ return new Internal(n)
321
+ }
322
+ `
323
+ const result = transpileToJS(source, { runTests: false })
324
+ const dts = generateDTS(result, source)
325
+
326
+ expect(dts).not.toContain('Internal')
327
+ expect(dts).toContain('export declare function make(')
328
+ })
329
+ })
330
+
331
+ describe('generateDTS — Type declarations', () => {
332
+ it('should emit Type as type guard object', () => {
333
+ const source = `
334
+ export Type Name = 'World'
335
+
336
+ export function greet(name: Name) -> '' {
337
+ return \`Hello, \${name}!\`
338
+ }
339
+ `
340
+ const result = transpileToJS(source, { runTests: false })
341
+ const dts = generateDTS(result, source)
342
+
343
+ expect(dts).toContain('export declare const Name:')
344
+ expect(dts).toContain('check(value: any): boolean')
345
+ expect(dts).toContain('default: string')
346
+ })
347
+
348
+ it('should emit Type with simple value example', () => {
349
+ const source = `
350
+ export Type Count = 0
351
+ `
352
+ const result = transpileToJS(source, { runTests: false })
353
+ const dts = generateDTS(result, source)
354
+
355
+ expect(dts).toContain('default: number')
356
+ })
357
+ })
358
+
359
+ describe('generateDTS — Generic declarations', () => {
360
+ it('should emit Generic as factory function', () => {
361
+ const source = `
362
+ export Generic Box<T> {
363
+ description: 'a boxed value'
364
+ predicate(obj, T) { return typeof obj === 'object' && T(obj.value) }
365
+ }
366
+ `
367
+ const result = transpileToJS(source, { runTests: false })
368
+ const dts = generateDTS(result, source)
369
+
370
+ expect(dts).toContain('export declare function Box(')
371
+ expect(dts).toContain('...args: any[]')
372
+ expect(dts).toContain('check(value: any): boolean')
373
+ })
374
+ })
375
+
376
+ describe('generateDTS — mixed declarations', () => {
377
+ it('should handle file with functions, classes, types, and generics', () => {
378
+ const source = `
379
+ export Type Name = 'World'
380
+
381
+ export Generic Box<T> {
382
+ description: 'a boxed value'
383
+ predicate(obj, T) { return true }
384
+ }
385
+
386
+ export class Point {
387
+ constructor(x: 0.0, y: 0.0) {
388
+ this.x = x
389
+ this.y = y
390
+ }
391
+ }
392
+
393
+ export function greet(name: '') -> '' {
394
+ return \`Hello, \${name}!\`
395
+ }
396
+ `
397
+ const result = transpileToJS(source, { runTests: false })
398
+ const dts = generateDTS(result, source)
399
+
400
+ // All four kinds present
401
+ expect(dts).toContain('export declare function greet(')
402
+ expect(dts).toContain('export declare function Point(')
403
+ expect(dts).toContain('export declare const Name:')
404
+ expect(dts).toContain('export declare function Box(')
405
+ })
406
+ })