tjs-lang 0.8.6 → 0.8.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tjs-lang",
3
- "version": "0.8.6",
3
+ "version": "0.8.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",
@@ -0,0 +1,48 @@
1
+ import { describe, it, expect } from 'bun:test'
2
+ import { tjs } from './index'
3
+
4
+ /**
5
+ * `transformBareAssignments` auto-const's a FIRST bare assignment to an
6
+ * uppercase identifier (native-TJS convenience: `Foo = Type(...)` → `const Foo`).
7
+ * Two invariants it must respect — both were broken and are regression-guarded
8
+ * here (a tosijs live-example crashed because `let B = null; … B = BABYLON`
9
+ * became `const B = BABYLON`, shadowing the outer `B`):
10
+ * 1. It is a TJS feature — must NOT touch plain JS (dialect: 'js'). TJS ⊇ JS.
11
+ * 2. Even in TJS, it must NOT redeclare an already-declared binding.
12
+ */
13
+ const code = (src: string, dialect: 'js' | 'tjs') =>
14
+ tjs(src, { dialect, runTests: false }).code
15
+
16
+ describe('bare assignments (auto-const) — TJS-only, first-assignment-only', () => {
17
+ it('dialect:js — a reassignment of a declared uppercase let is untouched', () => {
18
+ const out = code('let B = null\nfunction f() { B = 1 }', 'js')
19
+ expect(out).not.toContain('const B = 1')
20
+ expect(out).toContain('B = 1')
21
+ })
22
+
23
+ it('dialect:js — even an undeclared uppercase assignment (implicit global) is untouched', () => {
24
+ expect(code('Foo = 1', 'js')).not.toContain('const Foo')
25
+ })
26
+
27
+ it('dialect:js — the reported repro: B = BABYLON inside a method stays an assignment', () => {
28
+ const out = code(
29
+ 'let B = null\nconst s = g({ m(el, BABYLON) { B = BABYLON } })',
30
+ 'js'
31
+ )
32
+ expect(out).not.toContain('const B = BABYLON')
33
+ expect(out).toContain('B = BABYLON')
34
+ })
35
+
36
+ it('dialect:tjs — reassignment of a declared binding is preserved', () => {
37
+ const out = code('let B = null\nconst s = g({ m() { B = 1 } })', 'tjs')
38
+ expect(out).not.toContain('const B = 1')
39
+ })
40
+
41
+ it('dialect:tjs — the feature still fires for a FIRST assignment of an undeclared uppercase name', () => {
42
+ expect(code('Foo = { debug: true }', 'tjs')).toContain('const Foo')
43
+ })
44
+
45
+ it('never rewrites lowercase identifiers (only uppercase convention)', () => {
46
+ expect(code('foo = 1', 'tjs')).not.toContain('const foo')
47
+ })
48
+ })
@@ -0,0 +1,32 @@
1
+ import { describe, it, expect } from 'bun:test'
2
+ import { tjs } from './index'
3
+
4
+ /**
5
+ * A doc comment (`/*#` … or JSDoc `/**` …) is only a doc comment when it starts
6
+ * a line — whitespace-only before the `/`. A `/*#` after code on the line, or
7
+ * inside a string literal, is an ordinary block comment and must be ignored (not
8
+ * extracted as documentation). Enforced by a line-start lookbehind on every
9
+ * doc-comment matcher (docs.ts, from-ts, parser, bin/docs.js).
10
+ */
11
+ const descOf = (src: string) =>
12
+ tjs(src, { dialect: 'js', runTests: false }).metadata?.greet?.description
13
+
14
+ describe('doc comments must start a line (whitespace-only before the slash)', () => {
15
+ const fn = '\nfunction greet(name) { return name }'
16
+
17
+ it('extracts a line-start /*# doc comment', () => {
18
+ expect(descOf('/*#\nMy docs\n*/' + fn)).toContain('My docs')
19
+ })
20
+
21
+ it('extracts an indented (own-line) /*# doc comment', () => {
22
+ expect(descOf(' /*#\n Indented docs\n */' + fn)).toContain('Indented')
23
+ })
24
+
25
+ it('ignores a /*# that follows code on the same line', () => {
26
+ expect(descOf('const x = 1 /*# not a doc */' + fn)).toBeUndefined()
27
+ })
28
+
29
+ it('ignores a /*# inside a string literal', () => {
30
+ expect(descOf('const s = "/*# not a doc */"' + fn)).toBeUndefined()
31
+ })
32
+ })
package/src/lang/docs.ts CHANGED
@@ -169,8 +169,12 @@ export function generateDocs(source: string): DocResult {
169
169
  // Two doc-block flavors are recognized:
170
170
  // /*# ... */ — TJS native: content is markdown verbatim
171
171
  // /** ... */ — JSDoc: each line's leading ` * ` is stripped, then markdown
172
- const docPattern = /\/\*#([\s\S]*?)\*\//g
173
- const jsdocPattern = /\/\*\*([\s\S]*?)\*\//g
172
+ // Only a line-start doc comment (whitespace-only before the `/`) counts; a
173
+ // `/*#` or `/**` after code on the line — or inside a string — is an ordinary
174
+ // block comment, not documentation. The lookbehind keeps match.index on the
175
+ // `/` (no position shift for callers).
176
+ const docPattern = /(?<=^[ \t]*)\/\*#([\s\S]*?)\*\//gm
177
+ const jsdocPattern = /(?<=^[ \t]*)\/\*\*([\s\S]*?)\*\//gm
174
178
  // Match the START of a function declaration. Params (which can contain
175
179
  // nested parens like `fn = (x) => x`) are captured by balanced-paren
176
180
  // scanning below, NOT by this regex.
@@ -2333,7 +2333,10 @@ function extractDocComments(
2333
2333
  source: string
2334
2334
  ): Array<{ content: string; index: number }> {
2335
2335
  const comments: Array<{ content: string; index: number }> = []
2336
- const docRegex = /\/\*#[\s\S]*?\*\//g
2336
+ // Line-start `/*#` only (whitespace-only before it); a mid-line `/*#` (after
2337
+ // code, or inside a string) is an ordinary block comment. Lookbehind is
2338
+ // zero-width, so match.index stays on `/*#` for the brace-depth check below.
2339
+ const docRegex = /(?<=^[ \t]*)\/\*#[\s\S]*?\*\//gm
2337
2340
 
2338
2341
  // Track brace depth to identify top-level comments
2339
2342
  let braceDepth = 0
@@ -3126,12 +3126,19 @@ ${branches.join('\n')}
3126
3126
  * where the identifier starts with uppercase (to avoid breaking normal assignments)
3127
3127
  */
3128
3128
  export function transformBareAssignments(source: string): string {
3129
+ // A bare `Foo = …` is auto-const'd only on FIRST assignment. If the name is
3130
+ // already declared (let/const/var/function/class) anywhere in the source, the
3131
+ // statement is a REASSIGNMENT — leave it alone, or we'd emit a duplicate/
3132
+ // shadowing `const` (e.g. `let B = null; … B = x` → wrongly `const B = x`).
3133
+ const declared = new Set<string>()
3134
+ const declRe = /\b(?:let|const|var|function|class)\s+([A-Z][a-zA-Z0-9_]*)/g
3135
+ for (let m; (m = declRe.exec(source)); ) declared.add(m[1])
3136
+
3129
3137
  // Match: start of line/statement, uppercase identifier, =, not ==
3130
- // Negative lookbehind for const/let/var to avoid double-declaring
3131
3138
  return source.replace(
3132
- /(?<=^|[;\n{])\s*([A-Z][a-zA-Z0-9_]*)\s*=(?!=)/gm,
3133
- (match, name) => {
3134
- // Check if already has const/let/var before it
3139
+ /(?<=^|[;\n{])(\s*)([A-Z][a-zA-Z0-9_]*)\s*=(?!=)/gm,
3140
+ (match, _ws, name) => {
3141
+ if (declared.has(name)) return match // reassignment of a declared binding
3135
3142
  return match.replace(name, `const ${name}`)
3136
3143
  }
3137
3144
  )
@@ -304,9 +304,14 @@ export function preprocess(
304
304
  source = transformUnionDeclarations(source)
305
305
  source = transformEnumDeclarations(source)
306
306
 
307
- // Transform bare assignments to const declarations
308
- // Foo = ... -> const Foo = ...
309
- source = transformBareAssignments(source)
307
+ // Transform bare assignments to const declarations (native-TJS convenience):
308
+ // Foo = ... -> const Foo = ... Gated by TjsSafeAssign — OFF for plain JS
309
+ // (dialect: 'js'), TS-originated, and VM targets, so a JS reassignment like
310
+ // `B = value` (of an already-declared `let B`) is never rewritten. See
311
+ // PRINCIPLES.md (TJS ⊇ JS): plain JS must pass through unchanged.
312
+ if (tjsModes.tjsSafeAssign) {
313
+ source = transformBareAssignments(source)
314
+ }
310
315
 
311
316
  // Phase 3: cross-file wasm-function composition. When a ModuleLoader is
312
317
  // supplied, resolve `import { ... } from '<spec>'` statements at transpile
@@ -737,7 +742,11 @@ export function extractTDoc(
737
742
  // This preserves full markdown content
738
743
  // Find the LAST /*# ... */ block and verify it immediately precedes the function
739
744
  // (only whitespace and line comments allowed between)
740
- const allDocBlocks = [...beforeFunc.matchAll(/\/\*#([\s\S]*?)\*\//g)]
745
+ // Line-start `/*#` only — a `/*#` after code (or in a string) isn't a doc
746
+ // comment. Lookbehind keeps match.index/length on the `/*#…*/` span.
747
+ const allDocBlocks = [
748
+ ...beforeFunc.matchAll(/(?<=^[ \t]*)\/\*#([\s\S]*?)\*\//gm),
749
+ ]
741
750
  if (allDocBlocks.length > 0) {
742
751
  const lastBlock = allDocBlocks[allDocBlocks.length - 1]
743
752
  const afterBlock = beforeFunc.substring(