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,2097 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Demo Navigation Component
|
|
3
|
+
*
|
|
4
|
+
* Sidebar with 4 accordion details blocks:
|
|
5
|
+
* - AJS Examples (examples that open AJS playground)
|
|
6
|
+
* - TJS Examples (examples that open TJS playground)
|
|
7
|
+
* - AJS Docs (documentation that opens in floating viewer)
|
|
8
|
+
* - TJS Docs (documentation that opens in floating viewer)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { Component, elements, ElementCreator, vars } from 'tosijs'
|
|
12
|
+
import {
|
|
13
|
+
xinFloat,
|
|
14
|
+
XinFloat,
|
|
15
|
+
markdownViewer,
|
|
16
|
+
MarkdownViewer,
|
|
17
|
+
icons,
|
|
18
|
+
} from 'tosijs-ui'
|
|
19
|
+
import { examples as ajsExamples } from './examples'
|
|
20
|
+
import { tsExamples, type TSExample } from './ts-examples'
|
|
21
|
+
|
|
22
|
+
const { div, details, summary, span, button } = elements
|
|
23
|
+
|
|
24
|
+
// TJS example interface
|
|
25
|
+
interface TjsExample {
|
|
26
|
+
name: string
|
|
27
|
+
description: string
|
|
28
|
+
code: string
|
|
29
|
+
group?: 'featured' | 'basics' | 'patterns' | 'fullstack' | 'advanced'
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// TJS examples - demonstrating typed JavaScript features
|
|
33
|
+
export const tjsExamples: TjsExample[] = [
|
|
34
|
+
{
|
|
35
|
+
name: 'TJS Grammar Demo',
|
|
36
|
+
description: 'Comprehensive example exercising all TJS syntax features',
|
|
37
|
+
group: 'featured',
|
|
38
|
+
code: `/*#
|
|
39
|
+
# TJS Grammar Reference
|
|
40
|
+
|
|
41
|
+
This example exercises **every TJS feature**. Run it to see
|
|
42
|
+
tests pass and signature validation in action.
|
|
43
|
+
|
|
44
|
+
## Parameter Syntax
|
|
45
|
+
| Syntax | Meaning |
|
|
46
|
+
|--------|---------|
|
|
47
|
+
| \`x: 0\` | Required number |
|
|
48
|
+
| \`x = 0\` | Optional, defaults to 0 |
|
|
49
|
+
| \`(? x: 0)\` | Force input validation |
|
|
50
|
+
| \`(! x: 0)\` | Skip input validation |
|
|
51
|
+
|
|
52
|
+
## Return Type Syntax
|
|
53
|
+
| Syntax | Meaning |
|
|
54
|
+
|--------|---------|
|
|
55
|
+
| \`-> 10\` | Signature test runs at transpile |
|
|
56
|
+
| \`-? 10\` | + runtime output validation |
|
|
57
|
+
| \`-! 10\` | Skip signature test |
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
// ─────────────────────────────────────────────────────────
|
|
61
|
+
// SIGNATURE TESTS: -> runs at transpile time
|
|
62
|
+
// ─────────────────────────────────────────────────────────
|
|
63
|
+
|
|
64
|
+
/*#
|
|
65
|
+
Double a number. The \`-> 10\` means: double(5) must return 10.
|
|
66
|
+
This is verified when you save/transpile!
|
|
67
|
+
*/
|
|
68
|
+
function double(x: 5) -> 10 {
|
|
69
|
+
return x * 2
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/*#
|
|
73
|
+
Concatenate first and last name.
|
|
74
|
+
*/
|
|
75
|
+
function fullName(first: 'Jane', last: 'Doe') -> 'Jane Doe' {
|
|
76
|
+
return first + ' ' + last
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─────────────────────────────────────────────────────────
|
|
80
|
+
// SKIP SIGNATURE TEST: -! when return varies
|
|
81
|
+
// ─────────────────────────────────────────────────────────
|
|
82
|
+
|
|
83
|
+
/*#
|
|
84
|
+
Division with error handling. Uses \`-!\` because the error
|
|
85
|
+
path returns a different shape than success.
|
|
86
|
+
*/
|
|
87
|
+
function divide(a: 10, b: 2) -! { ok: true, value: 5 } {
|
|
88
|
+
if (b === 0) {
|
|
89
|
+
return { ok: false, value: 0, error: 'div by zero' }
|
|
90
|
+
}
|
|
91
|
+
return { ok: true, value: a / b }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ─────────────────────────────────────────────────────────
|
|
95
|
+
// EXPLICIT TESTS: test 'description' { }
|
|
96
|
+
// ─────────────────────────────────────────────────────────
|
|
97
|
+
|
|
98
|
+
test 'double works' {
|
|
99
|
+
expect(double(7)).toBe(14)
|
|
100
|
+
expect(double(0)).toBe(0)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
test 'fullName concatenates' {
|
|
104
|
+
expect(fullName('John', 'Smith')).toBe('John Smith')
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
test 'divide handles zero' {
|
|
108
|
+
const result = divide(10, 0)
|
|
109
|
+
expect(result.ok).toBe(false)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
test 'divide works normally' {
|
|
113
|
+
const result = divide(20, 4)
|
|
114
|
+
expect(result.ok).toBe(true)
|
|
115
|
+
expect(result.value).toBe(5)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ─────────────────────────────────────────────────────────
|
|
119
|
+
// UNSAFE FUNCTIONS: (!) skips input validation
|
|
120
|
+
// ─────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
/*#
|
|
123
|
+
Fast path - no runtime type checks on inputs.
|
|
124
|
+
Use when you trust the caller (internal code).
|
|
125
|
+
*/
|
|
126
|
+
function fastAdd(! a: 0, b: 0) -> 0 {
|
|
127
|
+
return a + b
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// ─────────────────────────────────────────────────────────
|
|
131
|
+
// SAFE FUNCTIONS: (?) forces input validation
|
|
132
|
+
// ─────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
/*#
|
|
135
|
+
Critical path - always validate inputs even in unsafe blocks.
|
|
136
|
+
*/
|
|
137
|
+
function safeAdd(? a: 0, b: 0) -> 0 {
|
|
138
|
+
return a + b
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// ─────────────────────────────────────────────────────────
|
|
142
|
+
// COMPLEX TYPES
|
|
143
|
+
// ─────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
/*#
|
|
146
|
+
Object types are defined by example shape.
|
|
147
|
+
*/
|
|
148
|
+
function createPoint(x: 3, y: 4) -> { x: 3, y: 4 } {
|
|
149
|
+
return { x, y }
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/*#
|
|
153
|
+
Array types use single-element example.
|
|
154
|
+
*/
|
|
155
|
+
function sum(nums: [1, 2, 3]) -> 6 {
|
|
156
|
+
return nums.reduce((a, b) => a + b, 0)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
test 'createPoint returns structure' {
|
|
160
|
+
const p = createPoint(10, 20)
|
|
161
|
+
expect(p.x).toBe(10)
|
|
162
|
+
expect(p.y).toBe(20)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
test 'sum adds array' {
|
|
166
|
+
expect(sum([1, 2, 3, 4])).toBe(10)
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// ─────────────────────────────────────────────────────────
|
|
170
|
+
// OUTPUT
|
|
171
|
+
// ─────────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
console.log('All signature tests passed at transpile time!')
|
|
174
|
+
console.log('double.__tjs:', double.__tjs)
|
|
175
|
+
console.log('Result:', double(21))
|
|
176
|
+
`,
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
name: 'Hello TJS',
|
|
180
|
+
description: 'Simple typed greeting function with docs and tests',
|
|
181
|
+
group: 'basics',
|
|
182
|
+
code: `/*#
|
|
183
|
+
The classic first function in any language.
|
|
184
|
+
|
|
185
|
+
Demonstrates:
|
|
186
|
+
- Type annotations via examples (\`name: 'World'\`)
|
|
187
|
+
- Return type example (\`-> 'Hello, World'\`) - tests the signature!
|
|
188
|
+
- Inline tests with \`test\` blocks
|
|
189
|
+
- Markdown documentation via \`/*#\` comments
|
|
190
|
+
*/
|
|
191
|
+
test 'greet says hello' {
|
|
192
|
+
expect(greet('TJS')).toBe('Hello, TJS!')
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function greet(name: 'World') -> 'Hello, World!' {
|
|
196
|
+
return \`Hello, \${name}!\`
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// The type metadata includes the doc comment
|
|
200
|
+
console.log('Type info:', greet.__tjs)
|
|
201
|
+
|
|
202
|
+
// The ->! means: greet('World') MUST return 'Hello, World'
|
|
203
|
+
// This is verified at transpile time!
|
|
204
|
+
greet('TJS')`,
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
name: 'Required vs Optional',
|
|
208
|
+
description: 'Difference between : and = in parameters',
|
|
209
|
+
group: 'basics',
|
|
210
|
+
code: `/*#
|
|
211
|
+
## Required vs Optional Parameters
|
|
212
|
+
|
|
213
|
+
In TJS, the punctuation tells you everything:
|
|
214
|
+
|
|
215
|
+
| Syntax | Meaning |
|
|
216
|
+
|--------|---------|
|
|
217
|
+
| \`param: 'value'\` | **Required** - must be provided |
|
|
218
|
+
| \`param = 'value'\` | **Optional** - defaults to value |
|
|
219
|
+
|
|
220
|
+
The example value after \`:\` or \`=\` defines the type.
|
|
221
|
+
*/
|
|
222
|
+
test 'requires name and email' {
|
|
223
|
+
const user = createUser('Alice', 'alice@test.com')
|
|
224
|
+
expect(user.name).toBe('Alice')
|
|
225
|
+
expect(user.age).toBe(0) // default
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function createUser(
|
|
229
|
+
name: 'anonymous',
|
|
230
|
+
email: 'user@example.com',
|
|
231
|
+
age = 0,
|
|
232
|
+
admin = false
|
|
233
|
+
) -> { name: '', email: '', age: 0, admin: false } {
|
|
234
|
+
return { name, email, age, admin }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Check the metadata
|
|
238
|
+
console.log('Params:', createUser.__tjs.params)
|
|
239
|
+
createUser('Alice', 'alice@example.com')`,
|
|
240
|
+
},
|
|
241
|
+
{
|
|
242
|
+
name: 'Object Types',
|
|
243
|
+
description: 'Typed object parameters and returns',
|
|
244
|
+
group: 'basics',
|
|
245
|
+
code: `/*#
|
|
246
|
+
## Object Types
|
|
247
|
+
|
|
248
|
+
Object shapes are defined by example:
|
|
249
|
+
\`{ first: '', last: '' }\` means an object with string properties.
|
|
250
|
+
|
|
251
|
+
The return type \`-> { x: 0, y: 0 }\` is tested at transpile time!
|
|
252
|
+
*/
|
|
253
|
+
test 'createPoint returns correct structure' {
|
|
254
|
+
const p = createPoint(5, 10)
|
|
255
|
+
expect(p.x).toBe(5)
|
|
256
|
+
expect(p.y).toBe(10)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function getFullName(person: { first: '', last: '' }) -> 'Jane Doe' {
|
|
260
|
+
return person.first + ' ' + person.last
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function createPoint(x: 0, y: 0) -> { x: 0, y: 0 } {
|
|
264
|
+
return { x, y }
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function distance(p1: { x: 0, y: 0 }, p2: { x: 0, y: 0 }) -> 5 {
|
|
268
|
+
const dx = p2.x - p1.x
|
|
269
|
+
const dy = p2.y - p1.y
|
|
270
|
+
return Math.sqrt(dx * dx + dy * dy)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Usage - signature tests verify these at transpile time
|
|
274
|
+
const name = getFullName({ first: 'Jane', last: 'Doe' }) // -> 'Jane Doe'
|
|
275
|
+
const dist = distance({ x: 0, y: 0 }, { x: 3, y: 4 }) // -> 5
|
|
276
|
+
|
|
277
|
+
console.log('Name:', name)
|
|
278
|
+
console.log('Distance:', dist)`,
|
|
279
|
+
},
|
|
280
|
+
{
|
|
281
|
+
name: 'Array Types',
|
|
282
|
+
description: 'Working with typed arrays',
|
|
283
|
+
group: 'basics',
|
|
284
|
+
code: `/*#
|
|
285
|
+
## Array Types
|
|
286
|
+
|
|
287
|
+
Array types use a single-element example:
|
|
288
|
+
- \`[0]\` = array of numbers
|
|
289
|
+
- \`['']\` = array of strings
|
|
290
|
+
- \`[{ x: 0 }]\` = array of objects with shape { x: number }
|
|
291
|
+
*/
|
|
292
|
+
test 'sum adds numbers' {
|
|
293
|
+
expect(sum([1, 2, 3, 4])).toBe(10)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
test 'stats calculates correctly' {
|
|
297
|
+
const s = stats([10, 20, 30])
|
|
298
|
+
expect(s.min).toBe(10)
|
|
299
|
+
expect(s.max).toBe(30)
|
|
300
|
+
expect(s.avg).toBe(20)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function sum(numbers: [0]) -> 10 {
|
|
304
|
+
return numbers.reduce((a, b) => a + b, 0)
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function average(numbers: [0]) -> 20 {
|
|
308
|
+
if (numbers.length === 0) return 0
|
|
309
|
+
return sum(numbers) / numbers.length
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function stats(data: [0]) -> { min: 10, max: 30, avg: 20 } {
|
|
313
|
+
if (data.length === 0) {
|
|
314
|
+
return { min: 0, max: 0, avg: 0 }
|
|
315
|
+
}
|
|
316
|
+
return {
|
|
317
|
+
min: Math.min(...data),
|
|
318
|
+
max: Math.max(...data),
|
|
319
|
+
avg: average(data)
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Signature test: stats([10, 20, 30]) -> { min: 10, max: 30, avg: 20 }
|
|
324
|
+
stats([10, 20, 30])`,
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
name: 'Higher-Order Functions',
|
|
328
|
+
description: 'Functions that take or return functions',
|
|
329
|
+
group: 'patterns',
|
|
330
|
+
code: `// TJS handles higher-order functions
|
|
331
|
+
// Note: Function type annotations use simple syntax
|
|
332
|
+
|
|
333
|
+
function mapStrings(arr: [''], fn = (x) => x) -> [''] {
|
|
334
|
+
return arr.map(fn)
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function filterNumbers(arr: [0], predicate = (x) => true) -> [0] {
|
|
338
|
+
return arr.filter(predicate)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function compose(f = (x) => x, g = (x) => x) -> 0 {
|
|
342
|
+
// Returns a composed function, demo returns result
|
|
343
|
+
const composed = (x) => f(g(x))
|
|
344
|
+
return composed(5)
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Usage examples
|
|
348
|
+
const double = (x) => x * 2
|
|
349
|
+
const addOne = (x) => x + 1
|
|
350
|
+
|
|
351
|
+
// Map strings to uppercase
|
|
352
|
+
const words = mapStrings(['hello', 'world'], s => s.toUpperCase())
|
|
353
|
+
|
|
354
|
+
// Filter even numbers
|
|
355
|
+
const evens = filterNumbers([1, 2, 3, 4, 5, 6], x => x % 2 === 0)
|
|
356
|
+
|
|
357
|
+
// Compose functions: (5 * 2) + 1 = 11
|
|
358
|
+
const result = compose(addOne, double)
|
|
359
|
+
|
|
360
|
+
console.log('Mapped:', words)
|
|
361
|
+
console.log('Filtered:', evens)
|
|
362
|
+
console.log('Composed result:', result)
|
|
363
|
+
|
|
364
|
+
result`,
|
|
365
|
+
},
|
|
366
|
+
{
|
|
367
|
+
name: 'Async Functions',
|
|
368
|
+
description: 'Typed async/await patterns',
|
|
369
|
+
group: 'patterns',
|
|
370
|
+
code: `// Async functions work naturally
|
|
371
|
+
|
|
372
|
+
async function fetchUser(id: 'user-1') -> { name: '', email: '' } {
|
|
373
|
+
// Simulated API call
|
|
374
|
+
await new Promise(resolve => setTimeout(resolve, 100))
|
|
375
|
+
return {
|
|
376
|
+
name: 'User ' + id,
|
|
377
|
+
email: id + '@example.com'
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
async function fetchUsers(ids: ['']) -> [{ name: '', email: '' }] {
|
|
382
|
+
return Promise.all(ids.map(id => fetchUser(id)))
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// Run it
|
|
386
|
+
await fetchUsers(['alice', 'bob', 'charlie'])`,
|
|
387
|
+
},
|
|
388
|
+
{
|
|
389
|
+
name: 'Error Handling',
|
|
390
|
+
description: 'Type-safe error handling patterns',
|
|
391
|
+
group: 'patterns',
|
|
392
|
+
code: `/*#
|
|
393
|
+
## Monadic Error Handling
|
|
394
|
+
|
|
395
|
+
TJS uses the Result pattern - errors are values, not exceptions.
|
|
396
|
+
This makes error handling explicit and type-safe.
|
|
397
|
+
|
|
398
|
+
Note: Using \`-!\` to skip signature test since error paths
|
|
399
|
+
return different shapes.
|
|
400
|
+
*/
|
|
401
|
+
test 'divide handles zero' {
|
|
402
|
+
const result = divide(10, 0)
|
|
403
|
+
expect(result.ok).toBe(false)
|
|
404
|
+
expect(result.error).toBe('Division by zero')
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
test 'divide works normally' {
|
|
408
|
+
const result = divide(10, 2)
|
|
409
|
+
expect(result.ok).toBe(true)
|
|
410
|
+
expect(result.value).toBe(5)
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function divide(a: 10, b: 2) -! { ok: true, value: 5, error: '' } {
|
|
414
|
+
if (b === 0) {
|
|
415
|
+
return { ok: false, value: 0, error: 'Division by zero' }
|
|
416
|
+
}
|
|
417
|
+
return { ok: true, value: a / b, error: '' }
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function safeParse(json: '{"x":1}') -! { ok: true, data: null, error: '' } {
|
|
421
|
+
try {
|
|
422
|
+
return { ok: true, data: JSON.parse(json), error: '' }
|
|
423
|
+
} catch (e) {
|
|
424
|
+
return { ok: false, data: null, error: e.message }
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
// Usage - errors are values you can inspect
|
|
429
|
+
const result = divide(10, 0)
|
|
430
|
+
if (result.ok) {
|
|
431
|
+
console.log('Result:', result.value)
|
|
432
|
+
} else {
|
|
433
|
+
console.log('Error:', result.error)
|
|
434
|
+
}`,
|
|
435
|
+
},
|
|
436
|
+
{
|
|
437
|
+
name: 'Schema Validation',
|
|
438
|
+
description: 'Using Schema for runtime type checking',
|
|
439
|
+
group: 'patterns',
|
|
440
|
+
code: `// TJS integrates with Schema for validation
|
|
441
|
+
import { Schema } from 'tosijs-schema'
|
|
442
|
+
|
|
443
|
+
// Define a schema
|
|
444
|
+
const UserSchema = Schema({
|
|
445
|
+
name: 'anonymous',
|
|
446
|
+
email: 'user@example.com',
|
|
447
|
+
age: 0
|
|
448
|
+
})
|
|
449
|
+
|
|
450
|
+
// Validate data
|
|
451
|
+
function validateUser(data: { name: '', email: '', age: 0 }) -> { valid: true, errors: [''] } {
|
|
452
|
+
const errors = []
|
|
453
|
+
|
|
454
|
+
if (!UserSchema.validate(data)) {
|
|
455
|
+
errors.push('Invalid user structure')
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
return {
|
|
459
|
+
valid: errors.length === 0,
|
|
460
|
+
errors
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
validateUser({ name: 'Alice', email: 'alice@test.com', age: 30 })`,
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
name: 'Date Formatting (with import)',
|
|
468
|
+
description: 'Uses date-fns for date formatting via ESM import',
|
|
469
|
+
group: 'patterns',
|
|
470
|
+
code: `/**
|
|
471
|
+
* # Date Formatting with Imports
|
|
472
|
+
*
|
|
473
|
+
* This example demonstrates importing an external ESM module
|
|
474
|
+
* (date-fns) and using it with TJS type safety.
|
|
475
|
+
*/
|
|
476
|
+
|
|
477
|
+
import { format, formatDistance, addDays, parseISO } from 'date-fns'
|
|
478
|
+
|
|
479
|
+
// Format a date with various patterns
|
|
480
|
+
function formatDate(date: '2024-01-15', pattern: 'yyyy-MM-dd') -> '' {
|
|
481
|
+
const parsed = parseISO(date)
|
|
482
|
+
return format(parsed, pattern)
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// Get human-readable relative time
|
|
486
|
+
function timeAgo(date: '2024-01-15') -> '' {
|
|
487
|
+
const parsed = parseISO(date)
|
|
488
|
+
return formatDistance(parsed, new Date(), { addSuffix: true })
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Add days to a date
|
|
492
|
+
function addWorkdays(date: '2024-01-15', days: 5) -> '' {
|
|
493
|
+
const parsed = parseISO(date)
|
|
494
|
+
const result = addDays(parsed, days)
|
|
495
|
+
return format(result, 'yyyy-MM-dd')
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Complex date operation with validation
|
|
499
|
+
function createEvent(input: {
|
|
500
|
+
title: 'Meeting',
|
|
501
|
+
startDate: '2024-01-15',
|
|
502
|
+
durationDays: 1
|
|
503
|
+
}) -> { title: '', start: '', end: '', formatted: '' } {
|
|
504
|
+
const start = parseISO(input.startDate)
|
|
505
|
+
const end = addDays(start, input.durationDays)
|
|
506
|
+
|
|
507
|
+
return {
|
|
508
|
+
title: input.title,
|
|
509
|
+
start: format(start, 'yyyy-MM-dd'),
|
|
510
|
+
end: format(end, 'yyyy-MM-dd'),
|
|
511
|
+
formatted: \`\${input.title}: \${format(start, 'MMM d')} - \${format(end, 'MMM d, yyyy')}\`
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
test('formatDate works with different patterns') {
|
|
516
|
+
expect(formatDate('2024-01-15', 'yyyy-MM-dd')).toBe('2024-01-15')
|
|
517
|
+
expect(formatDate('2024-01-15', 'MMMM d, yyyy')).toBe('January 15, 2024')
|
|
518
|
+
expect(formatDate('2024-01-15', 'EEE')).toBe('Mon')
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
test('addWorkdays calculates correctly') {
|
|
522
|
+
expect(addWorkdays('2024-01-15', 5)).toBe('2024-01-20')
|
|
523
|
+
expect(addWorkdays('2024-01-15', 0)).toBe('2024-01-15')
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
test('createEvent formats event correctly') {
|
|
527
|
+
const event = createEvent({
|
|
528
|
+
title: 'Conference',
|
|
529
|
+
startDate: '2024-06-10',
|
|
530
|
+
durationDays: 3
|
|
531
|
+
})
|
|
532
|
+
expect(event.title).toBe('Conference')
|
|
533
|
+
expect(event.start).toBe('2024-06-10')
|
|
534
|
+
expect(event.end).toBe('2024-06-13')
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Run example
|
|
538
|
+
console.log('Format examples:')
|
|
539
|
+
console.log(' ISO:', formatDate('2024-01-15', 'yyyy-MM-dd'))
|
|
540
|
+
console.log(' Long:', formatDate('2024-01-15', 'MMMM d, yyyy'))
|
|
541
|
+
console.log(' Day:', formatDate('2024-01-15', 'EEEE'))
|
|
542
|
+
|
|
543
|
+
console.log('\\nRelative time:', timeAgo('2024-01-01'))
|
|
544
|
+
|
|
545
|
+
const event = createEvent({
|
|
546
|
+
title: 'Launch Party',
|
|
547
|
+
startDate: '2024-03-15',
|
|
548
|
+
durationDays: 2
|
|
549
|
+
})
|
|
550
|
+
console.log('\\nEvent:', event.formatted)`,
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
name: 'Local Module Imports',
|
|
554
|
+
description: 'Import from modules you save in the playground',
|
|
555
|
+
group: 'patterns',
|
|
556
|
+
code: `/*#
|
|
557
|
+
# Local Module Imports
|
|
558
|
+
|
|
559
|
+
You can import from modules saved in the playground!
|
|
560
|
+
|
|
561
|
+
## How it works:
|
|
562
|
+
1. Save a module (use the Save button, give it a name like "math")
|
|
563
|
+
2. Import it by name from another file
|
|
564
|
+
|
|
565
|
+
## Try it:
|
|
566
|
+
1. First, create and save a module named "mymath":
|
|
567
|
+
|
|
568
|
+
\`\`\`javascript
|
|
569
|
+
export function add(a: 0, b: 0) -> 0 {
|
|
570
|
+
return a + b
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
export function multiply(a: 0, b: 0) -> 0 {
|
|
574
|
+
return a * b
|
|
575
|
+
}
|
|
576
|
+
\`\`\`
|
|
577
|
+
|
|
578
|
+
2. Then run this code (it imports from your saved module)
|
|
579
|
+
*/
|
|
580
|
+
|
|
581
|
+
// This imports from a module you saved in the playground
|
|
582
|
+
// Change 'mymath' to match whatever name you used when saving
|
|
583
|
+
import { add, multiply } from 'mymath'
|
|
584
|
+
|
|
585
|
+
function calculate(x: 0, y: 0) -> 0 {
|
|
586
|
+
// (x + y) * 2
|
|
587
|
+
return multiply(add(x, y), 2)
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
test 'calculate combines add and multiply' {
|
|
591
|
+
expect(calculate(3, 4)).toBe(14) // (3 + 4) * 2 = 14
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
console.log('calculate(3, 4) =', calculate(3, 4))
|
|
595
|
+
console.log('calculate(10, 5) =', calculate(10, 5))`,
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
name: 'Lodash Utilities (with import)',
|
|
599
|
+
description: 'Uses lodash-es for utility functions via ESM import',
|
|
600
|
+
group: 'patterns',
|
|
601
|
+
code: `/**
|
|
602
|
+
* # Lodash Utilities with Type Safety
|
|
603
|
+
*
|
|
604
|
+
* Demonstrates using lodash-es with TJS runtime validation.
|
|
605
|
+
*/
|
|
606
|
+
|
|
607
|
+
import { groupBy, sortBy, uniqBy, debounce, chunk } from 'lodash-es'
|
|
608
|
+
|
|
609
|
+
// Group items by a key
|
|
610
|
+
function groupUsers(users: [{ name: '', dept: '' }], key: 'dept')
|
|
611
|
+
-> { [key: '']: [{ name: '', dept: '' }] } {
|
|
612
|
+
return groupBy(users, key)
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Sort items by property
|
|
616
|
+
function sortByAge(users: [{ name: '', age: 0 }]) -> [{ name: '', age: 0 }] {
|
|
617
|
+
return sortBy(users, ['age'])
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
// Remove duplicates by property
|
|
621
|
+
function uniqueByEmail(users: [{ email: '', name: '' }]) -> [{ email: '', name: '' }] {
|
|
622
|
+
return uniqBy(users, 'email')
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// Chunk array into smaller arrays
|
|
626
|
+
// Note: nested array types like [['']] aren't supported yet, so we omit return type
|
|
627
|
+
function paginate(items: [''], pageSize: 10) {
|
|
628
|
+
return chunk(items, pageSize)
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Process data pipeline
|
|
632
|
+
function processUserData(input: {
|
|
633
|
+
users: [{ id: 0, name: '', email: '', dept: '' }]
|
|
634
|
+
}) -> {
|
|
635
|
+
byDept: { [key: '']: [{ id: 0, name: '', email: '', dept: '' }] },
|
|
636
|
+
unique: [{ id: 0, name: '', email: '', dept: '' }],
|
|
637
|
+
count: 0
|
|
638
|
+
} {
|
|
639
|
+
const unique = uniqBy(input.users, 'email')
|
|
640
|
+
const byDept = groupBy(unique, 'dept')
|
|
641
|
+
|
|
642
|
+
return {
|
|
643
|
+
byDept,
|
|
644
|
+
unique: sortBy(unique, ['name']),
|
|
645
|
+
count: unique.length
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
test('groupUsers groups by department') {
|
|
650
|
+
const users = [
|
|
651
|
+
{ name: 'Alice', dept: 'eng' },
|
|
652
|
+
{ name: 'Bob', dept: 'sales' },
|
|
653
|
+
{ name: 'Carol', dept: 'eng' }
|
|
654
|
+
]
|
|
655
|
+
const grouped = groupUsers(users, 'dept')
|
|
656
|
+
expect(grouped.eng.length).toBe(2)
|
|
657
|
+
expect(grouped.sales.length).toBe(1)
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
test('sortByAge sorts correctly') {
|
|
661
|
+
const users = [
|
|
662
|
+
{ name: 'Alice', age: 30 },
|
|
663
|
+
{ name: 'Bob', age: 25 },
|
|
664
|
+
{ name: 'Carol', age: 35 }
|
|
665
|
+
]
|
|
666
|
+
const sorted = sortByAge(users)
|
|
667
|
+
expect(sorted[0].name).toBe('Bob')
|
|
668
|
+
expect(sorted[2].name).toBe('Carol')
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
test('uniqueByEmail deduplicates') {
|
|
672
|
+
const users = [
|
|
673
|
+
{ email: 'a@b.com', name: 'Alice' },
|
|
674
|
+
{ email: 'a@b.com', name: 'Alice2' },
|
|
675
|
+
{ email: 'c@d.com', name: 'Carol' }
|
|
676
|
+
]
|
|
677
|
+
const unique = uniqueByEmail(users)
|
|
678
|
+
expect(unique.length).toBe(2)
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
test('paginate chunks correctly') {
|
|
682
|
+
const items = ['a', 'b', 'c', 'd', 'e']
|
|
683
|
+
const pages = paginate(items, 2)
|
|
684
|
+
expect(pages.length).toBe(3)
|
|
685
|
+
expect(pages[0]).toEqual(['a', 'b'])
|
|
686
|
+
expect(pages[2]).toEqual(['e'])
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// Run example
|
|
690
|
+
const users = [
|
|
691
|
+
{ id: 1, name: 'Alice', email: 'alice@co.com', dept: 'Engineering' },
|
|
692
|
+
{ id: 2, name: 'Bob', email: 'bob@co.com', dept: 'Sales' },
|
|
693
|
+
{ id: 3, name: 'Carol', email: 'carol@co.com', dept: 'Engineering' },
|
|
694
|
+
{ id: 4, name: 'Dave', email: 'alice@co.com', dept: 'Marketing' }, // dupe email
|
|
695
|
+
]
|
|
696
|
+
|
|
697
|
+
const result = processUserData({ users })
|
|
698
|
+
console.log('Unique users:', result.count)
|
|
699
|
+
console.log('Departments:', Object.keys(result.byDept))
|
|
700
|
+
console.log('Engineering team:', result.byDept['Engineering']?.map(u => u.name))`,
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
name: 'Full-Stack Demo: User Service',
|
|
704
|
+
description:
|
|
705
|
+
'A complete backend service with typed endpoints - save this first!',
|
|
706
|
+
group: 'fullstack',
|
|
707
|
+
code: `/**
|
|
708
|
+
* # User Service
|
|
709
|
+
*
|
|
710
|
+
* A complete backend service running in the browser.
|
|
711
|
+
* Save this module as "user-service", then run the client example.
|
|
712
|
+
*
|
|
713
|
+
* Features:
|
|
714
|
+
* - Type-safe endpoints with validation
|
|
715
|
+
* - In-memory data store
|
|
716
|
+
* - Full CRUD operations
|
|
717
|
+
*/
|
|
718
|
+
|
|
719
|
+
// In-memory store (would be a real DB in production)
|
|
720
|
+
const users = new Map()
|
|
721
|
+
let nextId = 1
|
|
722
|
+
|
|
723
|
+
// Create a new user
|
|
724
|
+
export function createUser(input: {
|
|
725
|
+
name: 'Alice',
|
|
726
|
+
email: 'alice@example.com'
|
|
727
|
+
}) -> { id: 0, name: '', email: '', createdAt: '' } {
|
|
728
|
+
const user = {
|
|
729
|
+
id: nextId++,
|
|
730
|
+
name: input.name,
|
|
731
|
+
email: input.email,
|
|
732
|
+
createdAt: new Date().toISOString()
|
|
733
|
+
}
|
|
734
|
+
users.set(user.id, user)
|
|
735
|
+
return user
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// Get user by ID (returns empty object if not found - union types not yet supported)
|
|
739
|
+
export function getUser(input: { id: 1 }) -> { id: 0, name: '', email: '', createdAt: '' } {
|
|
740
|
+
return users.get(input.id) || { id: 0, name: '', email: '', createdAt: '' }
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
// Update a user (returns empty object if not found - union types not yet supported)
|
|
744
|
+
export function updateUser(input: {
|
|
745
|
+
id: 1,
|
|
746
|
+
name: 'Alice',
|
|
747
|
+
email: 'alice@example.com'
|
|
748
|
+
}) -> { id: 0, name: '', email: '', createdAt: '' } {
|
|
749
|
+
const existing = users.get(input.id)
|
|
750
|
+
if (!existing) return { id: 0, name: '', email: '', createdAt: '' }
|
|
751
|
+
|
|
752
|
+
const updated = { ...existing, name: input.name, email: input.email }
|
|
753
|
+
users.set(input.id, updated)
|
|
754
|
+
return updated
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// Delete a user
|
|
758
|
+
export function deleteUser(input: { id: 1 }) -> { success: true, deleted: 0 } {
|
|
759
|
+
const existed = users.has(input.id)
|
|
760
|
+
users.delete(input.id)
|
|
761
|
+
return { success: existed, deleted: existed ? input.id : 0 }
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
// List all users
|
|
765
|
+
export function listUsers(input: { limit: 10, offset: 0 })
|
|
766
|
+
-> { users: [{ id: 0, name: '', email: '', createdAt: '' }], total: 0 } {
|
|
767
|
+
const all = [...users.values()]
|
|
768
|
+
const slice = all.slice(input.offset, input.offset + input.limit)
|
|
769
|
+
return { users: slice, total: all.length }
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Search users by name
|
|
773
|
+
export function searchUsers(input: { query: '' })
|
|
774
|
+
-> { users: [{ id: 0, name: '', email: '', createdAt: '' }] } {
|
|
775
|
+
const query = input.query.toLowerCase()
|
|
776
|
+
const matches = [...users.values()].filter(u =>
|
|
777
|
+
u.name.toLowerCase().includes(query)
|
|
778
|
+
)
|
|
779
|
+
return { users: matches }
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// Test the service
|
|
783
|
+
test('createUser creates user with ID') {
|
|
784
|
+
const user = createUser({ name: 'Test', email: 'test@test.com' })
|
|
785
|
+
expect(user.id).toBeGreaterThan(0)
|
|
786
|
+
expect(user.name).toBe('Test')
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
test('getUser returns created user') {
|
|
790
|
+
const created = createUser({ name: 'Bob', email: 'bob@test.com' })
|
|
791
|
+
const fetched = getUser({ id: created.id })
|
|
792
|
+
expect(fetched?.name).toBe('Bob')
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
test('updateUser modifies user') {
|
|
796
|
+
const user = createUser({ name: 'Original', email: 'orig@test.com' })
|
|
797
|
+
const updated = updateUser({ id: user.id, name: 'Updated', email: 'new@test.com' })
|
|
798
|
+
expect(updated?.name).toBe('Updated')
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
test('deleteUser removes user') {
|
|
802
|
+
const user = createUser({ name: 'ToDelete', email: 'del@test.com' })
|
|
803
|
+
const result = deleteUser({ id: user.id })
|
|
804
|
+
expect(result.success).toBe(true)
|
|
805
|
+
expect(getUser({ id: user.id })).toBe(null)
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
// Demo
|
|
809
|
+
console.log('=== User Service Demo ===\\n')
|
|
810
|
+
|
|
811
|
+
const alice = createUser({ name: 'Alice', email: 'alice@company.com' })
|
|
812
|
+
console.log('Created:', alice)
|
|
813
|
+
|
|
814
|
+
const bob = createUser({ name: 'Bob', email: 'bob@company.com' })
|
|
815
|
+
console.log('Created:', bob)
|
|
816
|
+
|
|
817
|
+
const carol = createUser({ name: 'Carol', email: 'carol@company.com' })
|
|
818
|
+
console.log('Created:', carol)
|
|
819
|
+
|
|
820
|
+
console.log('\\nAll users:', listUsers({ limit: 10, offset: 0 }))
|
|
821
|
+
console.log('\\nSearch "ob":', searchUsers({ query: 'ob' }))
|
|
822
|
+
|
|
823
|
+
// Type validation in action
|
|
824
|
+
console.log('\\nType validation test:')
|
|
825
|
+
const badResult = createUser({ name: 123 }) // Wrong type
|
|
826
|
+
console.log('Bad input result:', badResult) // Returns $error object`,
|
|
827
|
+
},
|
|
828
|
+
{
|
|
829
|
+
name: 'Full-Stack Demo: Client App',
|
|
830
|
+
description:
|
|
831
|
+
'Frontend that calls the User Service - run after saving user-service!',
|
|
832
|
+
group: 'fullstack',
|
|
833
|
+
code: `/**
|
|
834
|
+
* # Client Application
|
|
835
|
+
*
|
|
836
|
+
* A frontend that calls the User Service.
|
|
837
|
+
*
|
|
838
|
+
* **First:** Run the "User Service" example and save it as "user-service"
|
|
839
|
+
* **Then:** Run this client to see full-stack in action
|
|
840
|
+
*
|
|
841
|
+
* This demonstrates:
|
|
842
|
+
* - Importing local TJS modules
|
|
843
|
+
* - Type-safe service calls
|
|
844
|
+
* - Error handling
|
|
845
|
+
*/
|
|
846
|
+
|
|
847
|
+
// Import from local module (saved in playground)
|
|
848
|
+
import { createUser, getUser, listUsers, searchUsers } from 'user-service'
|
|
849
|
+
|
|
850
|
+
// Helper to display results
|
|
851
|
+
function display(label: '', data: {}) {
|
|
852
|
+
console.log(\`\\n\${label}:\`)
|
|
853
|
+
console.log(JSON.stringify(data, null, 2))
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
// Main app
|
|
857
|
+
async function main() {
|
|
858
|
+
console.log('=== Client App ===')
|
|
859
|
+
console.log('Connecting to user-service...\\n')
|
|
860
|
+
|
|
861
|
+
// Create some users
|
|
862
|
+
const user1 = createUser({ name: 'Dave', email: 'dave@startup.io' })
|
|
863
|
+
display('Created user', user1)
|
|
864
|
+
|
|
865
|
+
const user2 = createUser({ name: 'Eve', email: 'eve@startup.io' })
|
|
866
|
+
display('Created user', user2)
|
|
867
|
+
|
|
868
|
+
// Fetch a user
|
|
869
|
+
const fetched = getUser({ id: user1.id })
|
|
870
|
+
display('Fetched user', fetched)
|
|
871
|
+
|
|
872
|
+
// List all
|
|
873
|
+
const all = listUsers({ limit: 100, offset: 0 })
|
|
874
|
+
display('All users', all)
|
|
875
|
+
|
|
876
|
+
// Search
|
|
877
|
+
const results = searchUsers({ query: 'eve' })
|
|
878
|
+
display('Search results for "eve"', results)
|
|
879
|
+
|
|
880
|
+
// Type error handling
|
|
881
|
+
console.log('\\n--- Type Validation Demo ---')
|
|
882
|
+
const badResult = createUser({ name: 999 })
|
|
883
|
+
if (badResult.$error) {
|
|
884
|
+
console.log('Caught type error:', badResult.message)
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
console.log('\\n=== Full-Stack Demo Complete ===')
|
|
888
|
+
console.log('Everything ran in the browser. No server. No build step.')
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
main()`,
|
|
892
|
+
},
|
|
893
|
+
{
|
|
894
|
+
name: 'Full-Stack Demo: Todo API',
|
|
895
|
+
description: 'Complete REST-style Todo API with persistence',
|
|
896
|
+
group: 'fullstack',
|
|
897
|
+
code: `/**
|
|
898
|
+
* # Todo API Service
|
|
899
|
+
*
|
|
900
|
+
* A REST-style API for todo management.
|
|
901
|
+
* Demonstrates a more complete service pattern.
|
|
902
|
+
*/
|
|
903
|
+
|
|
904
|
+
// Simulated persistence layer
|
|
905
|
+
const todos = new Map()
|
|
906
|
+
let nextId = 1
|
|
907
|
+
|
|
908
|
+
// Types
|
|
909
|
+
type Todo = { id: number, title: string, completed: boolean, createdAt: string }
|
|
910
|
+
type CreateInput = { title: 'Buy milk' }
|
|
911
|
+
type UpdateInput = { id: 1, title: 'Buy milk', completed: false }
|
|
912
|
+
type FilterInput = { completed: true } | { completed: false } | {}
|
|
913
|
+
|
|
914
|
+
// POST /todos - Create
|
|
915
|
+
export function createTodo(input: { title: 'New todo' })
|
|
916
|
+
-> { id: 0, title: '', completed: false, createdAt: '' } {
|
|
917
|
+
const todo = {
|
|
918
|
+
id: nextId++,
|
|
919
|
+
title: input.title,
|
|
920
|
+
completed: false,
|
|
921
|
+
createdAt: new Date().toISOString()
|
|
922
|
+
}
|
|
923
|
+
todos.set(todo.id, todo)
|
|
924
|
+
return todo
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// GET /todos/:id - Read one (returns empty if not found)
|
|
928
|
+
export function getTodo(input: { id: 1 })
|
|
929
|
+
-> { id: 0, title: '', completed: false, createdAt: '' } {
|
|
930
|
+
return todos.get(input.id) || { id: 0, title: '', completed: false, createdAt: '' }
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
// GET /todos - Read all (with optional filter)
|
|
934
|
+
export function listTodos(input: { completed: true } | {})
|
|
935
|
+
-> { todos: [{ id: 0, title: '', completed: false, createdAt: '' }] } {
|
|
936
|
+
let items = [...todos.values()]
|
|
937
|
+
|
|
938
|
+
if ('completed' in input) {
|
|
939
|
+
items = items.filter(t => t.completed === input.completed)
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
return { todos: items }
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
// PUT /todos/:id - Update (returns empty if not found)
|
|
946
|
+
export function updateTodo(input: { id: 1, title: '', completed: false })
|
|
947
|
+
-> { id: 0, title: '', completed: false, createdAt: '' } {
|
|
948
|
+
const existing = todos.get(input.id)
|
|
949
|
+
if (!existing) return { id: 0, title: '', completed: false, createdAt: '' }
|
|
950
|
+
|
|
951
|
+
const updated = {
|
|
952
|
+
...existing,
|
|
953
|
+
title: input.title ?? existing.title,
|
|
954
|
+
completed: input.completed ?? existing.completed
|
|
955
|
+
}
|
|
956
|
+
todos.set(input.id, updated)
|
|
957
|
+
return updated
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// DELETE /todos/:id - Delete
|
|
961
|
+
export function deleteTodo(input: { id: 1 }) -> { deleted: true } {
|
|
962
|
+
const existed = todos.has(input.id)
|
|
963
|
+
todos.delete(input.id)
|
|
964
|
+
return { deleted: existed }
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// PATCH /todos/:id/toggle - Toggle completion (returns empty if not found)
|
|
968
|
+
export function toggleTodo(input: { id: 1 })
|
|
969
|
+
-> { id: 0, title: '', completed: false, createdAt: '' } {
|
|
970
|
+
const todo = todos.get(input.id)
|
|
971
|
+
if (!todo) return { id: 0, title: '', completed: false, createdAt: '' }
|
|
972
|
+
|
|
973
|
+
todo.completed = !todo.completed
|
|
974
|
+
return todo
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
// DELETE /todos/completed - Clear completed
|
|
978
|
+
export function clearCompleted(input: {}) -> { cleared: 0 } {
|
|
979
|
+
let cleared = 0
|
|
980
|
+
for (const [id, todo] of todos) {
|
|
981
|
+
if (todo.completed) {
|
|
982
|
+
todos.delete(id)
|
|
983
|
+
cleared++
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
return { cleared }
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
// Tests
|
|
990
|
+
test('CRUD operations work') {
|
|
991
|
+
const todo = createTodo({ title: 'Test todo' })
|
|
992
|
+
expect(todo.id).toBeGreaterThan(0)
|
|
993
|
+
expect(todo.completed).toBe(false)
|
|
994
|
+
|
|
995
|
+
const fetched = getTodo({ id: todo.id })
|
|
996
|
+
expect(fetched?.title).toBe('Test todo')
|
|
997
|
+
|
|
998
|
+
const toggled = toggleTodo({ id: todo.id })
|
|
999
|
+
expect(toggled?.completed).toBe(true)
|
|
1000
|
+
|
|
1001
|
+
const deleted = deleteTodo({ id: todo.id })
|
|
1002
|
+
expect(deleted.deleted).toBe(true)
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// Demo
|
|
1006
|
+
console.log('=== Todo API Demo ===\\n')
|
|
1007
|
+
|
|
1008
|
+
// Create todos
|
|
1009
|
+
createTodo({ title: 'Learn TJS' })
|
|
1010
|
+
createTodo({ title: 'Build something cool' })
|
|
1011
|
+
createTodo({ title: 'Ship it' })
|
|
1012
|
+
|
|
1013
|
+
console.log('Created 3 todos')
|
|
1014
|
+
console.log('All:', listTodos({}))
|
|
1015
|
+
|
|
1016
|
+
// Complete first one
|
|
1017
|
+
const first = listTodos({}).todos[0]
|
|
1018
|
+
toggleTodo({ id: first.id })
|
|
1019
|
+
console.log('\\nToggled first todo')
|
|
1020
|
+
console.log('Completed:', listTodos({ completed: true }))
|
|
1021
|
+
console.log('Pending:', listTodos({ completed: false }))
|
|
1022
|
+
|
|
1023
|
+
// Clear completed
|
|
1024
|
+
console.log('\\nClearing completed...')
|
|
1025
|
+
console.log(clearCompleted({}))
|
|
1026
|
+
console.log('Remaining:', listTodos({}))`,
|
|
1027
|
+
},
|
|
1028
|
+
{
|
|
1029
|
+
name: 'The Universal Endpoint',
|
|
1030
|
+
description:
|
|
1031
|
+
'One endpoint. Any logic. Zero deployment. This is the whole thing.',
|
|
1032
|
+
group: 'advanced',
|
|
1033
|
+
code: `/**
|
|
1034
|
+
* # The Universal Endpoint
|
|
1035
|
+
*
|
|
1036
|
+
* This is the entire backend industry in 50 lines.
|
|
1037
|
+
*
|
|
1038
|
+
* What this replaces:
|
|
1039
|
+
* - GraphQL servers
|
|
1040
|
+
* - REST API forests
|
|
1041
|
+
* - Firebase/Lambda/Vercel Functions
|
|
1042
|
+
* - Kubernetes deployments
|
|
1043
|
+
* - The backend priesthood
|
|
1044
|
+
*
|
|
1045
|
+
* How it works:
|
|
1046
|
+
* 1. Client sends logic (not just data)
|
|
1047
|
+
* 2. Server executes it with bounded resources
|
|
1048
|
+
* 3. That's it. That's the whole thing.
|
|
1049
|
+
*/
|
|
1050
|
+
|
|
1051
|
+
import { AgentVM, ajs, coreAtoms } from 'tjs-lang'
|
|
1052
|
+
|
|
1053
|
+
// ============================================================
|
|
1054
|
+
// THE UNIVERSAL ENDPOINT (This is the entire backend)
|
|
1055
|
+
// ============================================================
|
|
1056
|
+
|
|
1057
|
+
export async function post(req: {
|
|
1058
|
+
body: {
|
|
1059
|
+
agent: '', // The logic to execute (AJS source)
|
|
1060
|
+
args: {}, // Input data
|
|
1061
|
+
fuel: 1000 // Max compute units (like gas)
|
|
1062
|
+
},
|
|
1063
|
+
headers: { authorization: '' }
|
|
1064
|
+
}) -> { result: {}, fuelUsed: 0, status: '' } | { error: '', fuelUsed: 0 } {
|
|
1065
|
+
|
|
1066
|
+
// 1. Parse the agent (it's just code as data)
|
|
1067
|
+
let ast
|
|
1068
|
+
try {
|
|
1069
|
+
ast = ajs(req.body.agent)
|
|
1070
|
+
} catch (e) {
|
|
1071
|
+
return { error: \`Parse error: \${e.message}\`, fuelUsed: 0 }
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
// 2. Create VM with capabilities (this is what you monetize)
|
|
1075
|
+
const vm = new AgentVM({
|
|
1076
|
+
// Your database, your auth, your AI - exposed as capabilities
|
|
1077
|
+
// Agents can only do what you allow
|
|
1078
|
+
})
|
|
1079
|
+
|
|
1080
|
+
// 3. Execute with bounded resources
|
|
1081
|
+
const result = await vm.run(ast, req.body.args, {
|
|
1082
|
+
fuel: Math.min(req.body.fuel, 10000), // Cap fuel
|
|
1083
|
+
timeoutMs: 5000 // Cap time
|
|
1084
|
+
})
|
|
1085
|
+
|
|
1086
|
+
// 4. Return result (or error - but we didn't crash)
|
|
1087
|
+
if (result.error) {
|
|
1088
|
+
return {
|
|
1089
|
+
error: result.error.message,
|
|
1090
|
+
fuelUsed: result.fuelUsed,
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
return {
|
|
1095
|
+
result: result.result,
|
|
1096
|
+
fuelUsed: result.fuelUsed,
|
|
1097
|
+
status: 'success'
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
// ============================================================
|
|
1102
|
+
// DEMO: Let's use it
|
|
1103
|
+
// ============================================================
|
|
1104
|
+
|
|
1105
|
+
// Simulate the endpoint
|
|
1106
|
+
const endpoint = post
|
|
1107
|
+
|
|
1108
|
+
// --- TEST 1: Simple computation (Success) ---
|
|
1109
|
+
console.log('═══════════════════════════════════════════')
|
|
1110
|
+
console.log('TEST 1: Simple Agent')
|
|
1111
|
+
console.log('═══════════════════════════════════════════')
|
|
1112
|
+
|
|
1113
|
+
const simpleAgent = \`
|
|
1114
|
+
function compute({ x, y }) {
|
|
1115
|
+
let sum = x + y
|
|
1116
|
+
let product = x * y
|
|
1117
|
+
return { sum, product, message: 'Math is easy' }
|
|
1118
|
+
}
|
|
1119
|
+
\`
|
|
1120
|
+
|
|
1121
|
+
const result1 = await endpoint({
|
|
1122
|
+
body: { agent: simpleAgent, args: { x: 7, y: 6 }, fuel: 100 },
|
|
1123
|
+
headers: { authorization: 'token_123' }
|
|
1124
|
+
})
|
|
1125
|
+
|
|
1126
|
+
console.log('Agent: compute({ x: 7, y: 6 })')
|
|
1127
|
+
console.log('Result:', result1.result)
|
|
1128
|
+
console.log('Fuel used:', result1.fuelUsed)
|
|
1129
|
+
console.log('Status: ✓ Success')
|
|
1130
|
+
|
|
1131
|
+
|
|
1132
|
+
// --- TEST 2: Infinite loop (Fuel Exhausted) ---
|
|
1133
|
+
console.log('\\n═══════════════════════════════════════════')
|
|
1134
|
+
console.log('TEST 2: Malicious Agent (Infinite Loop)')
|
|
1135
|
+
console.log('═══════════════════════════════════════════')
|
|
1136
|
+
|
|
1137
|
+
const maliciousAgent = \`
|
|
1138
|
+
function attack({ }) {
|
|
1139
|
+
let i = 0
|
|
1140
|
+
while (true) {
|
|
1141
|
+
i = i + 1
|
|
1142
|
+
// This would hang your Express server forever
|
|
1143
|
+
// This would cost you $10,000 on Lambda
|
|
1144
|
+
// This would crash your Kubernetes pod
|
|
1145
|
+
}
|
|
1146
|
+
return { i }
|
|
1147
|
+
}
|
|
1148
|
+
\`
|
|
1149
|
+
|
|
1150
|
+
const result2 = await endpoint({
|
|
1151
|
+
body: { agent: maliciousAgent, args: {}, fuel: 50 },
|
|
1152
|
+
headers: { authorization: 'token_123' }
|
|
1153
|
+
})
|
|
1154
|
+
|
|
1155
|
+
console.log('Agent: while (true) { ... }')
|
|
1156
|
+
console.log('Error:', result2.error)
|
|
1157
|
+
console.log('Fuel used:', result2.fuelUsed, '(exhausted at limit)')
|
|
1158
|
+
console.log('Status: ✗ Safely terminated')
|
|
1159
|
+
|
|
1160
|
+
|
|
1161
|
+
// --- TEST 3: Complex computation (Metered) ---
|
|
1162
|
+
console.log('\\n═══════════════════════════════════════════')
|
|
1163
|
+
console.log('TEST 3: Complex Agent (Metered)')
|
|
1164
|
+
console.log('═══════════════════════════════════════════')
|
|
1165
|
+
|
|
1166
|
+
const complexAgent = \`
|
|
1167
|
+
function fibonacci({ n }) {
|
|
1168
|
+
if (n <= 1) return { result: n }
|
|
1169
|
+
|
|
1170
|
+
let a = 0
|
|
1171
|
+
let b = 1
|
|
1172
|
+
let i = 2
|
|
1173
|
+
while (i <= n) {
|
|
1174
|
+
let temp = a + b
|
|
1175
|
+
a = b
|
|
1176
|
+
b = temp
|
|
1177
|
+
i = i + 1
|
|
1178
|
+
}
|
|
1179
|
+
return { result: b, iterations: n }
|
|
1180
|
+
}
|
|
1181
|
+
\`
|
|
1182
|
+
|
|
1183
|
+
const result3 = await endpoint({
|
|
1184
|
+
body: { agent: complexAgent, args: { n: 20 }, fuel: 500 },
|
|
1185
|
+
headers: { authorization: 'token_123' }
|
|
1186
|
+
})
|
|
1187
|
+
|
|
1188
|
+
console.log('Agent: fibonacci({ n: 20 })')
|
|
1189
|
+
console.log('Result:', result3.result)
|
|
1190
|
+
console.log('Fuel used:', result3.fuelUsed)
|
|
1191
|
+
console.log('Status: ✓ Success (metered)')
|
|
1192
|
+
|
|
1193
|
+
|
|
1194
|
+
// --- THE PUNCHLINE ---
|
|
1195
|
+
console.log('\\n═══════════════════════════════════════════')
|
|
1196
|
+
console.log('THE PUNCHLINE')
|
|
1197
|
+
console.log('═══════════════════════════════════════════')
|
|
1198
|
+
console.log(\`
|
|
1199
|
+
Your current backend?
|
|
1200
|
+
- The infinite loop would have HUNG your server
|
|
1201
|
+
- Or cost you THOUSANDS on Lambda
|
|
1202
|
+
- Or crashed your Kubernetes pod
|
|
1203
|
+
- Or required a "senior engineer" to add timeout logic
|
|
1204
|
+
|
|
1205
|
+
Tosi?
|
|
1206
|
+
- Charged 50 fuel units
|
|
1207
|
+
- Returned an error
|
|
1208
|
+
- Kept running
|
|
1209
|
+
- Total code: 50 lines
|
|
1210
|
+
|
|
1211
|
+
This is the entire backend industry.
|
|
1212
|
+
|
|
1213
|
+
One endpoint.
|
|
1214
|
+
Any logic.
|
|
1215
|
+
Zero deployment.
|
|
1216
|
+
Everyone is full stack now.
|
|
1217
|
+
\`)`,
|
|
1218
|
+
},
|
|
1219
|
+
{
|
|
1220
|
+
name: 'Inline Tests: Test Private Functions',
|
|
1221
|
+
description: 'Test internals without exporting them - the killer feature',
|
|
1222
|
+
group: 'advanced',
|
|
1223
|
+
code: `/**
|
|
1224
|
+
* # Testing Private Functions
|
|
1225
|
+
*
|
|
1226
|
+
* This is the killer feature of inline tests:
|
|
1227
|
+
* You can test functions WITHOUT exporting them.
|
|
1228
|
+
*
|
|
1229
|
+
* Traditional testing requires you to either:
|
|
1230
|
+
* - Export internal helpers (pollutes your API)
|
|
1231
|
+
* - Test only through public interface (incomplete coverage)
|
|
1232
|
+
* - Use hacks like rewire/proxyquire (brittle)
|
|
1233
|
+
*
|
|
1234
|
+
* TJS inline tests have full access to the module scope.
|
|
1235
|
+
* Test everything. Export only what you need.
|
|
1236
|
+
*/
|
|
1237
|
+
|
|
1238
|
+
// ============================================================
|
|
1239
|
+
// PRIVATE HELPERS (not exported, but fully testable!)
|
|
1240
|
+
// ============================================================
|
|
1241
|
+
|
|
1242
|
+
// Private: Email validation regex
|
|
1243
|
+
const EMAIL_REGEX = /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/
|
|
1244
|
+
|
|
1245
|
+
// Private: Validate email format
|
|
1246
|
+
function isValidEmail(email: '') -> true {
|
|
1247
|
+
return EMAIL_REGEX.test(email)
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
// Private: Sanitize user input
|
|
1251
|
+
function sanitize(input: '') -> '' {
|
|
1252
|
+
return input.trim().toLowerCase()
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
// Private: Generate a unique ID
|
|
1256
|
+
function generateId(prefix: 'user') -> '' {
|
|
1257
|
+
return prefix + '_' + Math.random().toString(36).slice(2, 10)
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// Private: Hash password (simplified for demo)
|
|
1261
|
+
function hashPassword(password: '') -> '' {
|
|
1262
|
+
let hash = 0
|
|
1263
|
+
for (let i = 0; i < password.length; i++) {
|
|
1264
|
+
hash = ((hash << 5) - hash) + password.charCodeAt(i)
|
|
1265
|
+
hash = hash & hash
|
|
1266
|
+
}
|
|
1267
|
+
return 'hashed_' + Math.abs(hash).toString(16)
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// Private: Check password strength
|
|
1271
|
+
function isStrongPassword(password: '') -> { strong: true, issues: [''] } {
|
|
1272
|
+
const issues = []
|
|
1273
|
+
if (password.length < 8) issues.push('Must be at least 8 characters')
|
|
1274
|
+
if (!/[A-Z]/.test(password)) issues.push('Must contain uppercase letter')
|
|
1275
|
+
if (!/[a-z]/.test(password)) issues.push('Must contain lowercase letter')
|
|
1276
|
+
if (!/[0-9]/.test(password)) issues.push('Must contain a number')
|
|
1277
|
+
return { strong: issues.length === 0, issues }
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// ============================================================
|
|
1281
|
+
// PUBLIC API (this is all that gets exported)
|
|
1282
|
+
// ============================================================
|
|
1283
|
+
|
|
1284
|
+
export function createUser(input: { email: '', password: '' })
|
|
1285
|
+
-> { id: '', email: '', passwordHash: '' } | { error: '', code: 0 } {
|
|
1286
|
+
|
|
1287
|
+
// Validate email (using private helper)
|
|
1288
|
+
const cleanEmail = sanitize(input.email)
|
|
1289
|
+
if (!isValidEmail(cleanEmail)) {
|
|
1290
|
+
return { error: 'Invalid email format', code: 400 }
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// Validate password (using private helper)
|
|
1294
|
+
const strength = isStrongPassword(input.password)
|
|
1295
|
+
if (!strength.strong) {
|
|
1296
|
+
return { error: strength.issues.join(', '), code: 400 }
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
// Create user (using private helpers)
|
|
1300
|
+
return {
|
|
1301
|
+
id: generateId('user'),
|
|
1302
|
+
email: cleanEmail,
|
|
1303
|
+
passwordHash: hashPassword(input.password)
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// ============================================================
|
|
1308
|
+
// TESTS - Full access to private functions!
|
|
1309
|
+
// ============================================================
|
|
1310
|
+
|
|
1311
|
+
// --- Test private email validation ---
|
|
1312
|
+
test('isValidEmail accepts valid emails') {
|
|
1313
|
+
expect(isValidEmail('test@example.com')).toBe(true)
|
|
1314
|
+
expect(isValidEmail('user.name+tag@domain.co.uk')).toBe(true)
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
test('isValidEmail rejects invalid emails') {
|
|
1318
|
+
expect(isValidEmail('not-an-email')).toBe(false)
|
|
1319
|
+
expect(isValidEmail('@nodomain.com')).toBe(false)
|
|
1320
|
+
expect(isValidEmail('spaces in@email.com')).toBe(false)
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
// --- Test private sanitization ---
|
|
1324
|
+
test('sanitize trims and lowercases') {
|
|
1325
|
+
expect(sanitize(' HELLO ')).toBe('hello')
|
|
1326
|
+
expect(sanitize(' Test@Email.COM ')).toBe('test@email.com')
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// --- Test private ID generation ---
|
|
1330
|
+
test('generateId creates prefixed unique IDs') {
|
|
1331
|
+
const id1 = generateId('user')
|
|
1332
|
+
const id2 = generateId('user')
|
|
1333
|
+
expect(id1.startsWith('user_')).toBe(true)
|
|
1334
|
+
expect(id1).not.toBe(id2) // unique each time
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
test('generateId respects prefix') {
|
|
1338
|
+
expect(generateId('post').startsWith('post_')).toBe(true)
|
|
1339
|
+
expect(generateId('comment').startsWith('comment_')).toBe(true)
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// --- Test private password hashing ---
|
|
1343
|
+
test('hashPassword is deterministic') {
|
|
1344
|
+
const hash1 = hashPassword('secret123')
|
|
1345
|
+
const hash2 = hashPassword('secret123')
|
|
1346
|
+
expect(hash1).toBe(hash2)
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
test('hashPassword produces different hashes for different inputs') {
|
|
1350
|
+
const hash1 = hashPassword('password1')
|
|
1351
|
+
const hash2 = hashPassword('password2')
|
|
1352
|
+
expect(hash1).not.toBe(hash2)
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
// --- Test private password strength checker ---
|
|
1356
|
+
test('isStrongPassword rejects weak passwords') {
|
|
1357
|
+
const result = isStrongPassword('weak')
|
|
1358
|
+
expect(result.strong).toBe(false)
|
|
1359
|
+
expect(result.issues.length).toBeGreaterThan(0)
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
test('isStrongPassword accepts strong passwords') {
|
|
1363
|
+
const result = isStrongPassword('MyStr0ngP@ss!')
|
|
1364
|
+
expect(result.strong).toBe(true)
|
|
1365
|
+
expect(result.issues.length).toBe(0)
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
test('isStrongPassword lists specific issues') {
|
|
1369
|
+
const noUpper = isStrongPassword('lowercase123')
|
|
1370
|
+
expect(noUpper.issues).toContain('Must contain uppercase letter')
|
|
1371
|
+
|
|
1372
|
+
const noLower = isStrongPassword('UPPERCASE123')
|
|
1373
|
+
expect(noLower.issues).toContain('Must contain lowercase letter')
|
|
1374
|
+
|
|
1375
|
+
const noNumber = isStrongPassword('NoNumbers!')
|
|
1376
|
+
expect(noNumber.issues).toContain('Must contain a number')
|
|
1377
|
+
|
|
1378
|
+
const tooShort = isStrongPassword('Ab1!')
|
|
1379
|
+
expect(tooShort.issues).toContain('Must be at least 8 characters')
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// --- Test the public API (integration) ---
|
|
1383
|
+
test('createUser validates email') {
|
|
1384
|
+
const result = createUser({ email: 'invalid', password: 'StrongPass1!' })
|
|
1385
|
+
expect(result.error).toBe('Invalid email format')
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
test('createUser validates password strength') {
|
|
1389
|
+
const result = createUser({ email: 'test@test.com', password: 'weak' })
|
|
1390
|
+
expect(result.error).toBeTruthy()
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
test('createUser succeeds with valid input') {
|
|
1394
|
+
const result = createUser({
|
|
1395
|
+
email: ' Test@Example.COM ',
|
|
1396
|
+
password: 'MyStr0ngPass!'
|
|
1397
|
+
})
|
|
1398
|
+
expect(result.id).toBeTruthy()
|
|
1399
|
+
expect(result.email).toBe('test@example.com') // sanitized
|
|
1400
|
+
expect(result.passwordHash.startsWith('hashed_')).toBe(true)
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
// ============================================================
|
|
1404
|
+
// DEMO OUTPUT
|
|
1405
|
+
// ============================================================
|
|
1406
|
+
|
|
1407
|
+
console.log('=== Testing Private Functions Demo ===\\n')
|
|
1408
|
+
console.log('The functions isValidEmail, sanitize, generateId,')
|
|
1409
|
+
console.log('hashPassword, and isStrongPassword are all PRIVATE.')
|
|
1410
|
+
console.log('They are NOT exported. But we tested them all!\\n')
|
|
1411
|
+
|
|
1412
|
+
console.log('Try this in Jest/Vitest without exporting them. You can\\'t.')
|
|
1413
|
+
console.log('You\\'d have to either pollute your API or leave them untested.\\n')
|
|
1414
|
+
|
|
1415
|
+
console.log('TJS inline tests: Full coverage. Clean exports.\\n')
|
|
1416
|
+
|
|
1417
|
+
// Show the public API working
|
|
1418
|
+
const user = createUser({ email: 'demo@example.com', password: 'SecurePass123!' })
|
|
1419
|
+
console.log('Created user:', user)`,
|
|
1420
|
+
},
|
|
1421
|
+
]
|
|
1422
|
+
|
|
1423
|
+
// Types for docs
|
|
1424
|
+
interface DocItem {
|
|
1425
|
+
title: string
|
|
1426
|
+
filename: string
|
|
1427
|
+
text: string
|
|
1428
|
+
category?: 'ajs' | 'tjs' | 'general'
|
|
1429
|
+
hidden?: boolean
|
|
1430
|
+
}
|
|
1431
|
+
|
|
1432
|
+
interface DemoNavEvents {
|
|
1433
|
+
'select-ajs-example': { example: (typeof ajsExamples)[0] }
|
|
1434
|
+
'select-tjs-example': { example: (typeof tjsExamples)[0] }
|
|
1435
|
+
'select-ts-example': { example: TSExample }
|
|
1436
|
+
'select-doc': { doc: DocItem }
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
export class DemoNav extends Component {
|
|
1440
|
+
private _docs: DocItem[] = []
|
|
1441
|
+
private openSection: string | null = null
|
|
1442
|
+
private floatViewer: XinFloat | null = null
|
|
1443
|
+
private mdViewer: MarkdownViewer | null = null
|
|
1444
|
+
|
|
1445
|
+
// Track current selection for highlighting
|
|
1446
|
+
private _currentView: 'home' | 'ajs' | 'tjs' = 'home'
|
|
1447
|
+
private _currentExample: string | null = null
|
|
1448
|
+
|
|
1449
|
+
constructor() {
|
|
1450
|
+
super()
|
|
1451
|
+
// Initialize from URL hash
|
|
1452
|
+
this.loadStateFromURL()
|
|
1453
|
+
// Listen for hash changes
|
|
1454
|
+
window.addEventListener('hashchange', () => this.loadStateFromURL())
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
get currentView() {
|
|
1458
|
+
return this._currentView
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
set currentView(value: 'home' | 'ajs' | 'tjs') {
|
|
1462
|
+
this._currentView = value
|
|
1463
|
+
// Auto-open the appropriate section
|
|
1464
|
+
if (value === 'ajs') {
|
|
1465
|
+
this.openSection = 'ajs-demos'
|
|
1466
|
+
} else if (value === 'tjs') {
|
|
1467
|
+
this.openSection = 'tjs-demos'
|
|
1468
|
+
}
|
|
1469
|
+
this.rebuildNav()
|
|
1470
|
+
// Update indicator after rebuild (DOM now exists)
|
|
1471
|
+
this.updateCurrentIndicator()
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1474
|
+
get currentExample() {
|
|
1475
|
+
return this._currentExample
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
set currentExample(value: string | null) {
|
|
1479
|
+
this._currentExample = value
|
|
1480
|
+
this.updateCurrentIndicator()
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
private updateCurrentIndicator() {
|
|
1484
|
+
// Update .current class on nav items
|
|
1485
|
+
const items = this.querySelectorAll('.nav-item')
|
|
1486
|
+
items.forEach((item) => {
|
|
1487
|
+
const itemName = item.textContent?.trim()
|
|
1488
|
+
const isCurrent = itemName === this._currentExample
|
|
1489
|
+
item.classList.toggle('current', isCurrent)
|
|
1490
|
+
})
|
|
1491
|
+
// Update home link
|
|
1492
|
+
const homeLink = this.querySelector('.home-link')
|
|
1493
|
+
homeLink?.classList.toggle('current', this._currentView === 'home')
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
private loadStateFromURL() {
|
|
1497
|
+
const hash = window.location.hash.slice(1) // Remove '#'
|
|
1498
|
+
if (!hash) return
|
|
1499
|
+
|
|
1500
|
+
const params = new URLSearchParams(hash)
|
|
1501
|
+
const section = params.get('section')
|
|
1502
|
+
const view = params.get('view')
|
|
1503
|
+
const example = params.get('example')
|
|
1504
|
+
|
|
1505
|
+
// Set view and open appropriate section
|
|
1506
|
+
if (view === 'ajs') {
|
|
1507
|
+
this._currentView = 'ajs'
|
|
1508
|
+
this.openSection = 'ajs-demos'
|
|
1509
|
+
} else if (view === 'tjs') {
|
|
1510
|
+
this._currentView = 'tjs'
|
|
1511
|
+
this.openSection = 'tjs-demos'
|
|
1512
|
+
} else if (view === 'home') {
|
|
1513
|
+
this._currentView = 'home'
|
|
1514
|
+
} else if (
|
|
1515
|
+
section &&
|
|
1516
|
+
['ajs-demos', 'tjs-demos', 'ajs-docs', 'tjs-docs'].includes(section)
|
|
1517
|
+
) {
|
|
1518
|
+
this.openSection = section
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
// Set current example for highlighting
|
|
1522
|
+
if (example) {
|
|
1523
|
+
this._currentExample = example
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
this.rebuildNav()
|
|
1527
|
+
this.updateCurrentIndicator()
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
private saveStateToURL() {
|
|
1531
|
+
const params = new URLSearchParams(window.location.hash.slice(1))
|
|
1532
|
+
if (this.openSection) {
|
|
1533
|
+
params.set('section', this.openSection)
|
|
1534
|
+
}
|
|
1535
|
+
const newHash = params.toString()
|
|
1536
|
+
if (newHash !== window.location.hash.slice(1)) {
|
|
1537
|
+
window.history.replaceState(null, '', `#${newHash}`)
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
get docs(): DocItem[] {
|
|
1542
|
+
return this._docs
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
set docs(value: DocItem[]) {
|
|
1546
|
+
this._docs = value
|
|
1547
|
+
// Re-render when docs are set
|
|
1548
|
+
this.rebuildNav()
|
|
1549
|
+
}
|
|
1550
|
+
|
|
1551
|
+
// Light DOM styles (no static styleSpec)
|
|
1552
|
+
static lightDOMStyles = {
|
|
1553
|
+
':host': {
|
|
1554
|
+
display: 'flex',
|
|
1555
|
+
flexDirection: 'column',
|
|
1556
|
+
height: '100%',
|
|
1557
|
+
overflow: 'hidden',
|
|
1558
|
+
},
|
|
1559
|
+
|
|
1560
|
+
'.nav-sections': {
|
|
1561
|
+
flex: '1 1 auto',
|
|
1562
|
+
overflowY: 'auto',
|
|
1563
|
+
padding: '8px',
|
|
1564
|
+
},
|
|
1565
|
+
|
|
1566
|
+
details: {
|
|
1567
|
+
marginBottom: '4px',
|
|
1568
|
+
borderRadius: '6px',
|
|
1569
|
+
overflow: 'hidden',
|
|
1570
|
+
},
|
|
1571
|
+
|
|
1572
|
+
summary: {
|
|
1573
|
+
padding: '8px 12px',
|
|
1574
|
+
background: vars.codeBackground,
|
|
1575
|
+
color: vars.textColor,
|
|
1576
|
+
cursor: 'pointer',
|
|
1577
|
+
fontWeight: '500',
|
|
1578
|
+
fontSize: '14px',
|
|
1579
|
+
display: 'flex',
|
|
1580
|
+
alignItems: 'center',
|
|
1581
|
+
gap: '8px',
|
|
1582
|
+
userSelect: 'none',
|
|
1583
|
+
listStyle: 'none',
|
|
1584
|
+
},
|
|
1585
|
+
|
|
1586
|
+
'summary::-webkit-details-marker': {
|
|
1587
|
+
display: 'none',
|
|
1588
|
+
},
|
|
1589
|
+
|
|
1590
|
+
'summary::before': {
|
|
1591
|
+
content: '"▶"',
|
|
1592
|
+
fontSize: '10px',
|
|
1593
|
+
transition: 'transform 0.2s',
|
|
1594
|
+
},
|
|
1595
|
+
|
|
1596
|
+
'details[open] summary::before': {
|
|
1597
|
+
transform: 'rotate(90deg)',
|
|
1598
|
+
},
|
|
1599
|
+
|
|
1600
|
+
'summary:hover': {
|
|
1601
|
+
background: vars.codeBorder,
|
|
1602
|
+
},
|
|
1603
|
+
|
|
1604
|
+
'.section-content': {
|
|
1605
|
+
padding: '4px 0',
|
|
1606
|
+
},
|
|
1607
|
+
|
|
1608
|
+
'.nav-item': {
|
|
1609
|
+
display: 'block',
|
|
1610
|
+
padding: '6px 12px 6px 24px',
|
|
1611
|
+
cursor: 'pointer',
|
|
1612
|
+
fontSize: '13px',
|
|
1613
|
+
color: vars.textColor,
|
|
1614
|
+
textDecoration: 'none',
|
|
1615
|
+
borderRadius: '4px',
|
|
1616
|
+
transition: 'background 0.15s',
|
|
1617
|
+
},
|
|
1618
|
+
|
|
1619
|
+
'.nav-item:hover': {
|
|
1620
|
+
background: vars.codeBackground,
|
|
1621
|
+
},
|
|
1622
|
+
|
|
1623
|
+
'.nav-item.requires-api::after': {
|
|
1624
|
+
content: '"🔑"',
|
|
1625
|
+
marginLeft: '4px',
|
|
1626
|
+
fontSize: '11px',
|
|
1627
|
+
},
|
|
1628
|
+
|
|
1629
|
+
'.nav-item.current': {
|
|
1630
|
+
background: vars.brandColor,
|
|
1631
|
+
fontWeight: '500',
|
|
1632
|
+
color: '#fff',
|
|
1633
|
+
},
|
|
1634
|
+
|
|
1635
|
+
'.group-header': {
|
|
1636
|
+
padding: '8px 12px 4px 16px',
|
|
1637
|
+
fontSize: '11px',
|
|
1638
|
+
fontWeight: '600',
|
|
1639
|
+
color: vars.textColorLight,
|
|
1640
|
+
textTransform: 'uppercase',
|
|
1641
|
+
letterSpacing: '0.5px',
|
|
1642
|
+
},
|
|
1643
|
+
|
|
1644
|
+
'.group-header:not(:first-child)': {
|
|
1645
|
+
marginTop: '8px',
|
|
1646
|
+
borderTop: `1px solid ${vars.codeBorder}`,
|
|
1647
|
+
paddingTop: '12px',
|
|
1648
|
+
},
|
|
1649
|
+
|
|
1650
|
+
'.section-icon': {
|
|
1651
|
+
width: '16px',
|
|
1652
|
+
height: '16px',
|
|
1653
|
+
},
|
|
1654
|
+
|
|
1655
|
+
'.home-link': {
|
|
1656
|
+
display: 'flex',
|
|
1657
|
+
alignItems: 'center',
|
|
1658
|
+
gap: '8px',
|
|
1659
|
+
padding: '10px 12px',
|
|
1660
|
+
marginBottom: '8px',
|
|
1661
|
+
cursor: 'pointer',
|
|
1662
|
+
fontSize: '14px',
|
|
1663
|
+
fontWeight: '500',
|
|
1664
|
+
color: '#374151',
|
|
1665
|
+
borderRadius: '6px',
|
|
1666
|
+
transition: 'background 0.15s',
|
|
1667
|
+
},
|
|
1668
|
+
|
|
1669
|
+
'.home-link:hover': {
|
|
1670
|
+
background: '#f3f4f6',
|
|
1671
|
+
},
|
|
1672
|
+
|
|
1673
|
+
'.home-link.current': {
|
|
1674
|
+
background: '#e0e7ff',
|
|
1675
|
+
color: '#3730a3',
|
|
1676
|
+
},
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
content = () => [div({ class: 'nav-sections', part: 'sections' })]
|
|
1680
|
+
|
|
1681
|
+
connectedCallback() {
|
|
1682
|
+
super.connectedCallback()
|
|
1683
|
+
this.rebuildNav()
|
|
1684
|
+
// Update indicator after DOM is ready
|
|
1685
|
+
this.updateCurrentIndicator()
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
// Group labels for display
|
|
1689
|
+
private static readonly GROUP_LABELS: Record<string, string> = {
|
|
1690
|
+
// TypeScript example groups
|
|
1691
|
+
intro: 'Introduction',
|
|
1692
|
+
validation: 'Runtime Validation',
|
|
1693
|
+
// TJS example groups
|
|
1694
|
+
|
|
1695
|
+
featured: 'Featured',
|
|
1696
|
+
basics: 'Basics',
|
|
1697
|
+
patterns: 'Patterns',
|
|
1698
|
+
api: 'API',
|
|
1699
|
+
llm: 'LLM',
|
|
1700
|
+
fullstack: 'Full Stack',
|
|
1701
|
+
advanced: 'Advanced',
|
|
1702
|
+
}
|
|
1703
|
+
|
|
1704
|
+
// Group ordering (featured first, then alphabetical-ish)
|
|
1705
|
+
private static readonly GROUP_ORDER = [
|
|
1706
|
+
// TypeScript groups
|
|
1707
|
+
'intro',
|
|
1708
|
+
'validation',
|
|
1709
|
+
// TJS groups
|
|
1710
|
+
'featured',
|
|
1711
|
+
'basics',
|
|
1712
|
+
'patterns',
|
|
1713
|
+
'api',
|
|
1714
|
+
'llm',
|
|
1715
|
+
'fullstack',
|
|
1716
|
+
'advanced',
|
|
1717
|
+
]
|
|
1718
|
+
|
|
1719
|
+
// Helper to render examples grouped by their group field
|
|
1720
|
+
private renderGroupedExamples<T extends { name: string; group?: string }>(
|
|
1721
|
+
examples: T[],
|
|
1722
|
+
renderItem: (ex: T) => HTMLElement
|
|
1723
|
+
): HTMLElement[] {
|
|
1724
|
+
const grouped = new Map<string, T[]>()
|
|
1725
|
+
|
|
1726
|
+
// Group examples
|
|
1727
|
+
for (const ex of examples) {
|
|
1728
|
+
const group = ex.group || 'other'
|
|
1729
|
+
if (!grouped.has(group)) {
|
|
1730
|
+
grouped.set(group, [])
|
|
1731
|
+
}
|
|
1732
|
+
grouped.get(group)!.push(ex)
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
// Sort groups by GROUP_ORDER
|
|
1736
|
+
const sortedGroups = Array.from(grouped.keys()).sort((a, b) => {
|
|
1737
|
+
const orderA = DemoNav.GROUP_ORDER.indexOf(a)
|
|
1738
|
+
const orderB = DemoNav.GROUP_ORDER.indexOf(b)
|
|
1739
|
+
return (orderA === -1 ? 99 : orderA) - (orderB === -1 ? 99 : orderB)
|
|
1740
|
+
})
|
|
1741
|
+
|
|
1742
|
+
// Render groups with headers
|
|
1743
|
+
const elements: HTMLElement[] = []
|
|
1744
|
+
for (const group of sortedGroups) {
|
|
1745
|
+
const items = grouped.get(group)!
|
|
1746
|
+
const label = DemoNav.GROUP_LABELS[group] || group
|
|
1747
|
+
|
|
1748
|
+
// Add group header
|
|
1749
|
+
elements.push(div({ class: 'group-header' }, label))
|
|
1750
|
+
|
|
1751
|
+
// Add items in this group
|
|
1752
|
+
for (const ex of items) {
|
|
1753
|
+
elements.push(renderItem(ex))
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
return elements
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
rebuildNav() {
|
|
1761
|
+
const container = this.querySelector('.nav-sections')
|
|
1762
|
+
if (!container) return
|
|
1763
|
+
|
|
1764
|
+
container.innerHTML = ''
|
|
1765
|
+
container.append(
|
|
1766
|
+
// Home link
|
|
1767
|
+
div(
|
|
1768
|
+
{
|
|
1769
|
+
class:
|
|
1770
|
+
this._currentView === 'home' ? 'home-link current' : 'home-link',
|
|
1771
|
+
onClick: () => this.selectHome(),
|
|
1772
|
+
},
|
|
1773
|
+
span({ class: 'section-icon' }, icons.home({ size: 16 })),
|
|
1774
|
+
'Home'
|
|
1775
|
+
),
|
|
1776
|
+
|
|
1777
|
+
// TypeScript Examples (TS -> TJS -> JS pipeline)
|
|
1778
|
+
details(
|
|
1779
|
+
{
|
|
1780
|
+
open: this.openSection === 'ts-demos',
|
|
1781
|
+
'data-section': 'ts-demos',
|
|
1782
|
+
onToggle: this.handleToggle,
|
|
1783
|
+
},
|
|
1784
|
+
summary(
|
|
1785
|
+
span({ class: 'section-icon' }, icons.code({ size: 16 })),
|
|
1786
|
+
'TypeScript Examples'
|
|
1787
|
+
),
|
|
1788
|
+
div(
|
|
1789
|
+
{ class: 'section-content' },
|
|
1790
|
+
...this.renderGroupedExamples(tsExamples, (ex) =>
|
|
1791
|
+
div(
|
|
1792
|
+
{
|
|
1793
|
+
class: 'nav-item',
|
|
1794
|
+
title: ex.description,
|
|
1795
|
+
onClick: () => this.selectTsExample(ex),
|
|
1796
|
+
},
|
|
1797
|
+
ex.name
|
|
1798
|
+
)
|
|
1799
|
+
)
|
|
1800
|
+
)
|
|
1801
|
+
),
|
|
1802
|
+
|
|
1803
|
+
// TJS Examples
|
|
1804
|
+
details(
|
|
1805
|
+
{
|
|
1806
|
+
open: this.openSection === 'tjs-demos',
|
|
1807
|
+
'data-section': 'tjs-demos',
|
|
1808
|
+
onToggle: this.handleToggle,
|
|
1809
|
+
},
|
|
1810
|
+
summary(
|
|
1811
|
+
span({ class: 'section-icon' }, icons.code({ size: 16 })),
|
|
1812
|
+
'TJS Examples'
|
|
1813
|
+
),
|
|
1814
|
+
div(
|
|
1815
|
+
{ class: 'section-content' },
|
|
1816
|
+
...this.renderGroupedExamples(tjsExamples, (ex) =>
|
|
1817
|
+
div(
|
|
1818
|
+
{
|
|
1819
|
+
class: 'nav-item',
|
|
1820
|
+
title: ex.description,
|
|
1821
|
+
onClick: () => this.selectTjsExample(ex),
|
|
1822
|
+
},
|
|
1823
|
+
ex.name
|
|
1824
|
+
)
|
|
1825
|
+
)
|
|
1826
|
+
)
|
|
1827
|
+
),
|
|
1828
|
+
|
|
1829
|
+
// AJS Examples
|
|
1830
|
+
details(
|
|
1831
|
+
{
|
|
1832
|
+
open: this.openSection === 'ajs-demos',
|
|
1833
|
+
'data-section': 'ajs-demos',
|
|
1834
|
+
onToggle: this.handleToggle,
|
|
1835
|
+
},
|
|
1836
|
+
summary(
|
|
1837
|
+
span({ class: 'section-icon' }, icons.code({ size: 16 })),
|
|
1838
|
+
'AJS Examples'
|
|
1839
|
+
),
|
|
1840
|
+
div(
|
|
1841
|
+
{ class: 'section-content' },
|
|
1842
|
+
...this.renderGroupedExamples(ajsExamples, (ex) =>
|
|
1843
|
+
div(
|
|
1844
|
+
{
|
|
1845
|
+
class: ex.requiresApi ? 'nav-item requires-api' : 'nav-item',
|
|
1846
|
+
title: ex.description,
|
|
1847
|
+
onClick: () => this.selectAjsExample(ex),
|
|
1848
|
+
},
|
|
1849
|
+
ex.name
|
|
1850
|
+
)
|
|
1851
|
+
)
|
|
1852
|
+
)
|
|
1853
|
+
),
|
|
1854
|
+
|
|
1855
|
+
// TJS Docs
|
|
1856
|
+
details(
|
|
1857
|
+
{
|
|
1858
|
+
open: this.openSection === 'tjs-docs',
|
|
1859
|
+
'data-section': 'tjs-docs',
|
|
1860
|
+
onToggle: this.handleToggle,
|
|
1861
|
+
},
|
|
1862
|
+
summary(
|
|
1863
|
+
span({ class: 'section-icon' }, icons.book({ size: 16 })),
|
|
1864
|
+
'TJS Docs'
|
|
1865
|
+
),
|
|
1866
|
+
div(
|
|
1867
|
+
{ class: 'section-content' },
|
|
1868
|
+
...this.getTjsDocs().map((doc) =>
|
|
1869
|
+
div(
|
|
1870
|
+
{
|
|
1871
|
+
class: 'nav-item',
|
|
1872
|
+
onClick: () => this.selectDoc(doc),
|
|
1873
|
+
},
|
|
1874
|
+
doc.title
|
|
1875
|
+
)
|
|
1876
|
+
)
|
|
1877
|
+
)
|
|
1878
|
+
),
|
|
1879
|
+
|
|
1880
|
+
// AJS Docs
|
|
1881
|
+
details(
|
|
1882
|
+
{
|
|
1883
|
+
open: this.openSection === 'ajs-docs',
|
|
1884
|
+
'data-section': 'ajs-docs',
|
|
1885
|
+
onToggle: this.handleToggle,
|
|
1886
|
+
},
|
|
1887
|
+
summary(
|
|
1888
|
+
span({ class: 'section-icon' }, icons.book({ size: 16 })),
|
|
1889
|
+
'AJS Docs'
|
|
1890
|
+
),
|
|
1891
|
+
div(
|
|
1892
|
+
{ class: 'section-content' },
|
|
1893
|
+
...this.getAjsDocs().map((doc) =>
|
|
1894
|
+
div(
|
|
1895
|
+
{
|
|
1896
|
+
class: 'nav-item',
|
|
1897
|
+
onClick: () => this.selectDoc(doc),
|
|
1898
|
+
},
|
|
1899
|
+
doc.title
|
|
1900
|
+
)
|
|
1901
|
+
)
|
|
1902
|
+
)
|
|
1903
|
+
)
|
|
1904
|
+
)
|
|
1905
|
+
}
|
|
1906
|
+
|
|
1907
|
+
handleToggle = (event: Event) => {
|
|
1908
|
+
const details = event.target as HTMLDetailsElement
|
|
1909
|
+
const section = details.getAttribute('data-section')
|
|
1910
|
+
|
|
1911
|
+
if (details.open) {
|
|
1912
|
+
// Close other sections (accordion behavior)
|
|
1913
|
+
this.openSection = section
|
|
1914
|
+
const allDetails = this.querySelectorAll('details')
|
|
1915
|
+
allDetails.forEach((d) => {
|
|
1916
|
+
if (d !== details && d.open) {
|
|
1917
|
+
d.open = false
|
|
1918
|
+
}
|
|
1919
|
+
})
|
|
1920
|
+
// Save to URL
|
|
1921
|
+
this.saveStateToURL()
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
getAjsDocs(): DocItem[] {
|
|
1926
|
+
return this.docs.filter(
|
|
1927
|
+
(d) =>
|
|
1928
|
+
!d.hidden &&
|
|
1929
|
+
(d.filename.includes('ASYNCJS') ||
|
|
1930
|
+
d.filename.includes('PATTERNS') ||
|
|
1931
|
+
d.filename === 'runtime.ts')
|
|
1932
|
+
)
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
getTjsDocs(): DocItem[] {
|
|
1936
|
+
return this.docs.filter(
|
|
1937
|
+
(d) =>
|
|
1938
|
+
!d.hidden &&
|
|
1939
|
+
(d.filename.includes('TJS') ||
|
|
1940
|
+
d.filename === 'CONTEXT.md' ||
|
|
1941
|
+
d.filename === 'PLAN.md')
|
|
1942
|
+
)
|
|
1943
|
+
}
|
|
1944
|
+
|
|
1945
|
+
selectHome() {
|
|
1946
|
+
this._currentView = 'home'
|
|
1947
|
+
this._currentExample = null
|
|
1948
|
+
this.updateCurrentIndicator()
|
|
1949
|
+
this.dispatchEvent(
|
|
1950
|
+
new CustomEvent('select-home', {
|
|
1951
|
+
bubbles: true,
|
|
1952
|
+
})
|
|
1953
|
+
)
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
selectAjsExample(example: (typeof ajsExamples)[0]) {
|
|
1957
|
+
this._currentView = 'ajs'
|
|
1958
|
+
this._currentExample = example.name
|
|
1959
|
+
this.updateCurrentIndicator()
|
|
1960
|
+
this.dispatchEvent(
|
|
1961
|
+
new CustomEvent('select-ajs-example', {
|
|
1962
|
+
detail: { example },
|
|
1963
|
+
bubbles: true,
|
|
1964
|
+
})
|
|
1965
|
+
)
|
|
1966
|
+
}
|
|
1967
|
+
|
|
1968
|
+
selectTjsExample(example: (typeof tjsExamples)[0]) {
|
|
1969
|
+
this._currentView = 'tjs'
|
|
1970
|
+
this._currentExample = example.name
|
|
1971
|
+
this.updateCurrentIndicator()
|
|
1972
|
+
this.dispatchEvent(
|
|
1973
|
+
new CustomEvent('select-tjs-example', {
|
|
1974
|
+
detail: { example },
|
|
1975
|
+
bubbles: true,
|
|
1976
|
+
})
|
|
1977
|
+
)
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1980
|
+
selectTsExample(example: TSExample) {
|
|
1981
|
+
this._currentView = 'tjs' // Will switch to 'ts' when TS playground is wired up
|
|
1982
|
+
this._currentExample = example.name
|
|
1983
|
+
this.updateCurrentIndicator()
|
|
1984
|
+
this.dispatchEvent(
|
|
1985
|
+
new CustomEvent('select-ts-example', {
|
|
1986
|
+
detail: { example },
|
|
1987
|
+
bubbles: true,
|
|
1988
|
+
})
|
|
1989
|
+
)
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
selectDoc(doc: DocItem) {
|
|
1993
|
+
// Open or update floating doc viewer
|
|
1994
|
+
if (!this.floatViewer || !document.body.contains(this.floatViewer)) {
|
|
1995
|
+
this.createFloatViewer(doc)
|
|
1996
|
+
} else {
|
|
1997
|
+
// Update existing viewer
|
|
1998
|
+
if (this.mdViewer) {
|
|
1999
|
+
this.mdViewer.value = doc.text
|
|
2000
|
+
}
|
|
2001
|
+
// Update title
|
|
2002
|
+
const title = this.floatViewer.querySelector('.float-title')
|
|
2003
|
+
if (title) {
|
|
2004
|
+
title.textContent = doc.title
|
|
2005
|
+
}
|
|
2006
|
+
}
|
|
2007
|
+
|
|
2008
|
+
this.dispatchEvent(
|
|
2009
|
+
new CustomEvent('select-doc', {
|
|
2010
|
+
detail: { doc },
|
|
2011
|
+
bubbles: true,
|
|
2012
|
+
})
|
|
2013
|
+
)
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
createFloatViewer(doc: DocItem) {
|
|
2017
|
+
this.mdViewer = markdownViewer({
|
|
2018
|
+
class: 'no-drag markdown-content',
|
|
2019
|
+
value: doc.text,
|
|
2020
|
+
style: {
|
|
2021
|
+
display: 'block',
|
|
2022
|
+
padding: '4px 20px 12px',
|
|
2023
|
+
overflow: 'auto',
|
|
2024
|
+
maxHeight: 'calc(80vh - 40px)',
|
|
2025
|
+
},
|
|
2026
|
+
})
|
|
2027
|
+
|
|
2028
|
+
const closeBtn = button(
|
|
2029
|
+
{
|
|
2030
|
+
class: 'iconic no-drag',
|
|
2031
|
+
style: {
|
|
2032
|
+
padding: '4px',
|
|
2033
|
+
border: 'none',
|
|
2034
|
+
background: 'transparent',
|
|
2035
|
+
cursor: 'pointer',
|
|
2036
|
+
},
|
|
2037
|
+
},
|
|
2038
|
+
icons.x({ size: 16 })
|
|
2039
|
+
)
|
|
2040
|
+
|
|
2041
|
+
this.floatViewer = xinFloat(
|
|
2042
|
+
{
|
|
2043
|
+
drag: true,
|
|
2044
|
+
remainOnResize: 'remain',
|
|
2045
|
+
remainOnScroll: 'remain',
|
|
2046
|
+
style: {
|
|
2047
|
+
position: 'fixed',
|
|
2048
|
+
top: '60px',
|
|
2049
|
+
right: '20px',
|
|
2050
|
+
width: '500px',
|
|
2051
|
+
maxWidth: 'calc(100vw - 40px)',
|
|
2052
|
+
maxHeight: '80vh',
|
|
2053
|
+
background: 'white',
|
|
2054
|
+
borderRadius: '8px',
|
|
2055
|
+
boxShadow: '0 4px 20px rgba(0,0,0,0.15)',
|
|
2056
|
+
overflow: 'hidden',
|
|
2057
|
+
zIndex: '1000',
|
|
2058
|
+
},
|
|
2059
|
+
},
|
|
2060
|
+
// Header
|
|
2061
|
+
div(
|
|
2062
|
+
{
|
|
2063
|
+
style: {
|
|
2064
|
+
display: 'flex',
|
|
2065
|
+
alignItems: 'center',
|
|
2066
|
+
padding: '6px 12px',
|
|
2067
|
+
background: '#f3f4f6',
|
|
2068
|
+
borderBottom: '1px solid #e5e7eb',
|
|
2069
|
+
cursor: 'move',
|
|
2070
|
+
},
|
|
2071
|
+
},
|
|
2072
|
+
span(
|
|
2073
|
+
{ class: 'float-title', style: { flex: '1', fontWeight: '500' } },
|
|
2074
|
+
doc.title
|
|
2075
|
+
),
|
|
2076
|
+
closeBtn
|
|
2077
|
+
),
|
|
2078
|
+
// Content
|
|
2079
|
+
this.mdViewer
|
|
2080
|
+
)
|
|
2081
|
+
|
|
2082
|
+
// Add click handler after element is created
|
|
2083
|
+
closeBtn.addEventListener('click', (e) => {
|
|
2084
|
+
e.stopPropagation()
|
|
2085
|
+
this.floatViewer?.remove()
|
|
2086
|
+
this.floatViewer = null
|
|
2087
|
+
this.mdViewer = null
|
|
2088
|
+
})
|
|
2089
|
+
|
|
2090
|
+
document.body.appendChild(this.floatViewer)
|
|
2091
|
+
}
|
|
2092
|
+
}
|
|
2093
|
+
|
|
2094
|
+
export const demoNav: ElementCreator<DemoNav> = DemoNav.elementCreator({
|
|
2095
|
+
tag: 'demo-nav',
|
|
2096
|
+
styleSpec: DemoNav.lightDOMStyles,
|
|
2097
|
+
})
|