tjs-lang 0.5.4 → 0.5.5
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/demo/docs.json +6 -0
- package/demo/src/demo-nav.ts +137 -203
- package/demo/src/imports.ts +43 -9
- package/demo/src/playground-shared.ts +7 -0
- package/demo/src/tjs-playground.ts +1 -0
- package/demo/src/ts-playground.ts +1 -0
- package/dist/index.js +123 -121
- package/dist/index.js.map +3 -3
- package/dist/src/lang/emitters/from-ts.d.ts +2 -0
- package/dist/tjs-batteries.js +3 -3
- package/dist/tjs-batteries.js.map +2 -2
- package/dist/tjs-full.js +123 -121
- package/dist/tjs-full.js.map +3 -3
- package/dist/tjs-transpiler.js +2 -349
- package/dist/tjs-transpiler.js.map +4 -19
- package/package.json +1 -1
- package/src/lang/codegen.test.ts +55 -0
- package/src/lang/emitters/from-ts.ts +244 -20
- package/src/lang/typescript-syntax.test.ts +358 -0
package/demo/docs.json
CHANGED
|
@@ -681,6 +681,12 @@
|
|
|
681
681
|
"filename": "from-ts.ts",
|
|
682
682
|
"path": "src/lang/emitters/from-ts.ts"
|
|
683
683
|
},
|
|
684
|
+
{
|
|
685
|
+
"title": "GEMINI.md - Project Context & Instructions",
|
|
686
|
+
"filename": "GEMINI.md",
|
|
687
|
+
"path": "GEMINI.md",
|
|
688
|
+
"text": "# GEMINI.md - Project Context & Instructions\n\nThis file provides foundational mandates and contextual information for Gemini CLI when working in the `tjs-lang` repository.\n\n## Project Overview\n\n**tjs-lang** is a platform for type-safe JavaScript execution and AI agent orchestration. It bridges the gap between static typing and runtime safety by preserving type information and providing a secure execution environment.\n\n### Core Languages\n\n1. **TJS (Typed JavaScript):** A TypeScript-like dialect that transpiles to JavaScript with embedded `__tjs` metadata. This metadata enables runtime type validation, auto-documentation, and introspection.\n2. **AJS (Agent JavaScript):** A safe, sandboxed language for AI agents. It compiles to a JSON-serializable AST and executes in a gas-limited VM with capability-based security.\n\n### Architecture\n\n- **Builder Layer** (`src/builder.ts`): Fluent API for constructing AST nodes.\n- **Runtime Layer** (`src/vm/runtime.ts`): Security-critical core that executes AST nodes and manages fuel/capabilities.\n- **Lang Layer** (`src/lang/`): Contains the parser (based on Acorn), linter, and emitters for TJS and AJS.\n- **Batteries** (`src/batteries/`): Built-in capabilities like LLM integration (LM Studio), vector search, and persistence.\n\n## Development Lifecycle\n\n### Common Commands\n\n- **Install:** `bun install`\n- **Build:** `npm run make` (Format, build grammars, compile TS, and bundle)\n- **Fast Test:** `npm run test:fast` (Runs core tests, skips LLM and benchmarks)\n- **Full Test:** `bun test`\n- **Type Check:** `npm run typecheck`\n- **Lint:** `npm run lint`\n- **Format:** `npm run format` (ESLint fix + Prettier)\n- **Dev Server:** `npm run dev`\n- **CLI Tool:** `bun src/cli/tjs.ts <command>` (Commands: `check`, `run`, `types`, `emit`, `convert`, `test`)\n\n### Testing Strategy\n\n- **Unit Tests:** Located alongside source files (e.g., `src/lang/parser.test.ts`).\n- **Integration Tests:** Located in `src/use-cases/`.\n- **Security Tests:** Critical for the VM, found in `src/use-cases/malicious-actor.test.ts`.\n- **Reproduction:** Always create a reproduction test case before fixing a bug.\n\n## Engineering Standards & Conventions\n\n### Coding Style\n\n- **Formatting:** Prettier (Single quotes, no semicolons, 2-space indentation, 80 char width).\n- **Naming:** Follow existing conventions; prefix unused variables with `_`.\n- **Type Safety:** Prioritize TypeScript for all core logic. `any` is allowed but should be used judiciously.\n- **Security:** Rigorously protect the `src/vm/runtime.ts` file as it is security-critical. Avoid prototype access (`__proto__`, `constructor`, `prototype`) in expression evaluation.\n\n### Language Specifics\n\n- **TJS Types:** In TJS, colons indicate _examples_, not just type annotations (e.g., `name: 'Alice'` means a required string).\n- **AJS Expressions:** Be aware that AJS expressions have differences from JS (e.g., safe null member access, limited computed member access).\n- **Monadic Errors:** Prefer returning error values (Monadic errors) over throwing exceptions to ensure execution stability and security.\n\n### Documentation\n\n- Maintain `DOCS-TJS.md` and `DOCS-AJS.md` for language-specific documentation.\n- Update `CLAUDE.md` for tool-specific guidance if architectural patterns change.\n\n## Contextual Precedence\n\n- **Directives:** Explicit requests for implementation take priority.\n- **Inquiries:** Requests for analysis or advice should not result in code changes without a subsequent Directive.\n- **Security First:** Never compromise the sandboxing or fuel-metering integrity of the VM.\n"
|
|
689
|
+
},
|
|
684
690
|
{
|
|
685
691
|
"title": "icebox",
|
|
686
692
|
"filename": "tasks-f6d70f.md",
|
package/demo/src/demo-nav.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* - TJS Docs (documentation that opens in floating viewer)
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { Component, elements, ElementCreator, vars,
|
|
11
|
+
import { Component, elements, ElementCreator, vars, bind } from 'tosijs'
|
|
12
12
|
import {
|
|
13
13
|
xinFloat,
|
|
14
14
|
XinFloat,
|
|
@@ -75,6 +75,7 @@ interface DocItem {
|
|
|
75
75
|
export class DemoNav extends Component {
|
|
76
76
|
private _docs: DocItem[] = []
|
|
77
77
|
private _appState: any = null // boxed proxy from index.ts
|
|
78
|
+
private _built = false
|
|
78
79
|
private floatViewer: XinFloat | null = null
|
|
79
80
|
private mdViewer: MarkdownViewer | null = null
|
|
80
81
|
|
|
@@ -93,42 +94,7 @@ export class DemoNav extends Component {
|
|
|
93
94
|
set appState(state: any) {
|
|
94
95
|
this._appState = state
|
|
95
96
|
if (!state) return
|
|
96
|
-
|
|
97
|
-
observe(/^app\.(currentView|currentExample)/, () => {
|
|
98
|
-
this.rebuildNav()
|
|
99
|
-
this.updateCurrentIndicator()
|
|
100
|
-
})
|
|
101
|
-
observe('app.openSection', () => {
|
|
102
|
-
this.rebuildNav()
|
|
103
|
-
})
|
|
104
|
-
// Initial sync
|
|
105
|
-
this.rebuildNav()
|
|
106
|
-
this.updateCurrentIndicator()
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
private get _currentView(): string {
|
|
110
|
-
return this._appState?.currentView?.valueOf() ?? 'home'
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
private get _currentExampleName(): string | null {
|
|
114
|
-
const ex = this._appState?.currentExample?.valueOf()
|
|
115
|
-
if (!ex) return null
|
|
116
|
-
return ex.name || ex.title || null
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
private get _openSection(): string | null {
|
|
120
|
-
return this._appState?.openSection?.valueOf() ?? null
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
private updateCurrentIndicator() {
|
|
124
|
-
const exName = this._currentExampleName
|
|
125
|
-
const items = this.querySelectorAll('.nav-item')
|
|
126
|
-
items.forEach((item) => {
|
|
127
|
-
const itemName = item.textContent?.trim()
|
|
128
|
-
item.classList.toggle('current', itemName === exName)
|
|
129
|
-
})
|
|
130
|
-
const homeLink = this.querySelector('.home-link')
|
|
131
|
-
homeLink?.classList.toggle('current', this._currentView === 'home')
|
|
97
|
+
this.tryBuild()
|
|
132
98
|
}
|
|
133
99
|
|
|
134
100
|
get docs(): DocItem[] {
|
|
@@ -137,8 +103,16 @@ export class DemoNav extends Component {
|
|
|
137
103
|
|
|
138
104
|
set docs(value: DocItem[]) {
|
|
139
105
|
this._docs = value
|
|
140
|
-
this.
|
|
141
|
-
|
|
106
|
+
this.tryBuild()
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Build nav once when both docs and appState are available
|
|
110
|
+
private tryBuild() {
|
|
111
|
+
if (this._built || !this._appState || !this._docs.length) return
|
|
112
|
+
const container = this.querySelector('.nav-sections')
|
|
113
|
+
if (!container) return
|
|
114
|
+
this._built = true
|
|
115
|
+
this.buildNav(container)
|
|
142
116
|
}
|
|
143
117
|
|
|
144
118
|
// Light DOM styles (no static styleSpec)
|
|
@@ -273,9 +247,7 @@ export class DemoNav extends Component {
|
|
|
273
247
|
|
|
274
248
|
connectedCallback() {
|
|
275
249
|
super.connectedCallback()
|
|
276
|
-
this.
|
|
277
|
-
// Update indicator after DOM is ready
|
|
278
|
-
this.updateCurrentIndicator()
|
|
250
|
+
this.tryBuild()
|
|
279
251
|
}
|
|
280
252
|
|
|
281
253
|
// Group labels for display
|
|
@@ -316,199 +288,156 @@ export class DemoNav extends Component {
|
|
|
316
288
|
>(examples: T[], renderItem: (ex: T) => HTMLElement): HTMLElement[] {
|
|
317
289
|
const grouped = new Map<string, T[]>()
|
|
318
290
|
|
|
319
|
-
// Group examples
|
|
320
291
|
for (const ex of examples) {
|
|
321
292
|
const group = ex.group || 'other'
|
|
322
|
-
if (!grouped.has(group))
|
|
323
|
-
grouped.set(group, [])
|
|
324
|
-
}
|
|
293
|
+
if (!grouped.has(group)) grouped.set(group, [])
|
|
325
294
|
grouped.get(group)!.push(ex)
|
|
326
295
|
}
|
|
327
296
|
|
|
328
|
-
// Sort groups by GROUP_ORDER
|
|
329
297
|
const sortedGroups = Array.from(grouped.keys()).sort((a, b) => {
|
|
330
298
|
const orderA = DemoNav.GROUP_ORDER.indexOf(a)
|
|
331
299
|
const orderB = DemoNav.GROUP_ORDER.indexOf(b)
|
|
332
300
|
return (orderA === -1 ? 99 : orderA) - (orderB === -1 ? 99 : orderB)
|
|
333
301
|
})
|
|
334
302
|
|
|
335
|
-
|
|
336
|
-
const elements: HTMLElement[] = []
|
|
303
|
+
const elts: HTMLElement[] = []
|
|
337
304
|
for (const group of sortedGroups) {
|
|
338
305
|
const items = grouped.get(group)!
|
|
339
306
|
const label = DemoNav.GROUP_LABELS[group] || group
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
elements.push(div({ class: 'group-header' }, label))
|
|
343
|
-
|
|
344
|
-
// Add items in this group
|
|
345
|
-
for (const ex of items) {
|
|
346
|
-
elements.push(renderItem(ex))
|
|
347
|
-
}
|
|
307
|
+
elts.push(div({ class: 'group-header' }, label))
|
|
308
|
+
for (const ex of items) elts.push(renderItem(ex))
|
|
348
309
|
}
|
|
310
|
+
return elts
|
|
311
|
+
}
|
|
349
312
|
|
|
350
|
-
|
|
313
|
+
// Create a nav item bound to currentExample for highlighting
|
|
314
|
+
private boundNavItem(
|
|
315
|
+
name: string,
|
|
316
|
+
extra: { description?: string; requiresApi?: boolean },
|
|
317
|
+
onClick: () => void
|
|
318
|
+
): HTMLElement {
|
|
319
|
+
const baseClass = extra.requiresApi ? 'nav-item requires-api' : 'nav-item'
|
|
320
|
+
const item = div(
|
|
321
|
+
{
|
|
322
|
+
class: baseClass,
|
|
323
|
+
title: extra.description,
|
|
324
|
+
'data-name': name,
|
|
325
|
+
onClick,
|
|
326
|
+
},
|
|
327
|
+
name
|
|
328
|
+
)
|
|
329
|
+
bind(item, 'app.currentExample', {
|
|
330
|
+
toDOM(el: HTMLElement, example: any) {
|
|
331
|
+
const current = example?.name || example?.title || null
|
|
332
|
+
el.classList.toggle('current', current === name)
|
|
333
|
+
},
|
|
334
|
+
})
|
|
335
|
+
return item
|
|
351
336
|
}
|
|
352
337
|
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
338
|
+
// Create a details section bound to openSection for open/close
|
|
339
|
+
private boundSection(
|
|
340
|
+
sectionId: string,
|
|
341
|
+
icon: Element,
|
|
342
|
+
label: string,
|
|
343
|
+
children: HTMLElement[]
|
|
344
|
+
): HTMLElement {
|
|
345
|
+
const det = details(
|
|
346
|
+
{
|
|
347
|
+
'data-section': sectionId,
|
|
348
|
+
onToggle: this.handleToggle,
|
|
349
|
+
},
|
|
350
|
+
summary(span({ class: 'section-icon' }, icon), label),
|
|
351
|
+
div({ class: 'section-content' }, ...children)
|
|
352
|
+
)
|
|
353
|
+
bind(det, 'app.openSection', {
|
|
354
|
+
toDOM(el: HTMLDetailsElement, section: string | null) {
|
|
355
|
+
el.open = section === sectionId
|
|
356
|
+
},
|
|
357
|
+
})
|
|
358
|
+
return det
|
|
359
|
+
}
|
|
356
360
|
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
)
|
|
361
|
+
// Build the nav once — all reactive updates via bind
|
|
362
|
+
private buildNav(container: Element) {
|
|
363
|
+
const homeLink = div(
|
|
364
|
+
{
|
|
365
|
+
class: 'home-link',
|
|
366
|
+
onClick: () => this.selectHome(),
|
|
367
|
+
},
|
|
368
|
+
span({ class: 'section-icon' }, icons.home({ size: 16 })),
|
|
369
|
+
'Home'
|
|
370
|
+
)
|
|
371
|
+
bind(homeLink, 'app.currentView', {
|
|
372
|
+
toDOM(el: HTMLElement, view: string) {
|
|
373
|
+
el.classList.toggle('current', view === 'home')
|
|
374
|
+
},
|
|
375
|
+
})
|
|
369
376
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
'TypeScript Examples'
|
|
380
|
-
),
|
|
381
|
-
div(
|
|
382
|
-
{ class: 'section-content' },
|
|
383
|
-
...this.renderGroupedExamples(tsExamples, (ex) =>
|
|
384
|
-
div(
|
|
385
|
-
{
|
|
386
|
-
class: 'nav-item',
|
|
387
|
-
title: ex.description,
|
|
388
|
-
onClick: () => this.selectTsExample(ex),
|
|
389
|
-
},
|
|
390
|
-
ex.name
|
|
391
|
-
)
|
|
392
|
-
)
|
|
377
|
+
container.append(
|
|
378
|
+
homeLink,
|
|
379
|
+
|
|
380
|
+
this.boundSection(
|
|
381
|
+
'ts-demos',
|
|
382
|
+
icons.code({ size: 16 }),
|
|
383
|
+
'TypeScript Examples',
|
|
384
|
+
this.renderGroupedExamples(tsExamples, (ex) =>
|
|
385
|
+
this.boundNavItem(ex.name, ex, () => this.selectTsExample(ex))
|
|
393
386
|
)
|
|
394
387
|
),
|
|
395
388
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
{
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
span({ class: 'section-icon' }, icons.code({ size: 16 })),
|
|
405
|
-
'TJS Examples'
|
|
406
|
-
),
|
|
407
|
-
div(
|
|
408
|
-
{ class: 'section-content' },
|
|
409
|
-
...this.renderGroupedExamples(this.tjsExamples, (ex) =>
|
|
410
|
-
div(
|
|
411
|
-
{
|
|
412
|
-
class: 'nav-item',
|
|
413
|
-
title: ex.description,
|
|
414
|
-
onClick: () => this.selectTjsExample(ex),
|
|
415
|
-
},
|
|
416
|
-
ex.title || ex.name
|
|
417
|
-
)
|
|
418
|
-
)
|
|
419
|
-
)
|
|
389
|
+
this.boundSection(
|
|
390
|
+
'tjs-demos',
|
|
391
|
+
icons.code({ size: 16 }),
|
|
392
|
+
'TJS Examples',
|
|
393
|
+
this.renderGroupedExamples(this.tjsExamples, (ex) => {
|
|
394
|
+
const name = ex.title || ex.name || 'Untitled'
|
|
395
|
+
return this.boundNavItem(name, ex, () => this.selectTjsExample(ex))
|
|
396
|
+
})
|
|
420
397
|
),
|
|
421
398
|
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
{
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
span({ class: 'section-icon' }, icons.code({ size: 16 })),
|
|
431
|
-
'AJS Examples'
|
|
432
|
-
),
|
|
433
|
-
div(
|
|
434
|
-
{ class: 'section-content' },
|
|
435
|
-
...this.renderGroupedExamples(this.ajsExamples, (ex) =>
|
|
436
|
-
div(
|
|
437
|
-
{
|
|
438
|
-
class: ex.requiresApi ? 'nav-item requires-api' : 'nav-item',
|
|
439
|
-
title: ex.description,
|
|
440
|
-
onClick: () => this.selectAjsExample(ex),
|
|
441
|
-
},
|
|
442
|
-
ex.title || ex.name
|
|
443
|
-
)
|
|
444
|
-
)
|
|
445
|
-
)
|
|
399
|
+
this.boundSection(
|
|
400
|
+
'ajs-demos',
|
|
401
|
+
icons.code({ size: 16 }),
|
|
402
|
+
'AJS Examples',
|
|
403
|
+
this.renderGroupedExamples(this.ajsExamples, (ex) => {
|
|
404
|
+
const name = ex.title || ex.name || 'Untitled'
|
|
405
|
+
return this.boundNavItem(name, ex, () => this.selectAjsExample(ex))
|
|
406
|
+
})
|
|
446
407
|
),
|
|
447
408
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
{
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
},
|
|
455
|
-
summary(
|
|
456
|
-
span({ class: 'section-icon' }, icons.book({ size: 16 })),
|
|
457
|
-
'TJS Docs'
|
|
458
|
-
),
|
|
459
|
-
div(
|
|
460
|
-
{ class: 'section-content' },
|
|
461
|
-
...this.getTjsDocs().map((doc) =>
|
|
462
|
-
div(
|
|
463
|
-
{
|
|
464
|
-
class: 'nav-item',
|
|
465
|
-
onClick: () => this.selectDoc(doc),
|
|
466
|
-
},
|
|
467
|
-
doc.title
|
|
468
|
-
)
|
|
469
|
-
)
|
|
409
|
+
this.boundSection(
|
|
410
|
+
'tjs-docs',
|
|
411
|
+
icons.book({ size: 16 }),
|
|
412
|
+
'TJS Docs',
|
|
413
|
+
this.getTjsDocs().map((doc) =>
|
|
414
|
+
this.boundNavItem(doc.title, doc, () => this.selectDoc(doc))
|
|
470
415
|
)
|
|
471
416
|
),
|
|
472
417
|
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
{
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
},
|
|
480
|
-
summary(
|
|
481
|
-
span({ class: 'section-icon' }, icons.book({ size: 16 })),
|
|
482
|
-
'AJS Docs'
|
|
483
|
-
),
|
|
484
|
-
div(
|
|
485
|
-
{ class: 'section-content' },
|
|
486
|
-
...this.getAjsDocs().map((doc) =>
|
|
487
|
-
div(
|
|
488
|
-
{
|
|
489
|
-
class: 'nav-item',
|
|
490
|
-
onClick: () => this.selectDoc(doc),
|
|
491
|
-
},
|
|
492
|
-
doc.title
|
|
493
|
-
)
|
|
494
|
-
)
|
|
418
|
+
this.boundSection(
|
|
419
|
+
'ajs-docs',
|
|
420
|
+
icons.book({ size: 16 }),
|
|
421
|
+
'AJS Docs',
|
|
422
|
+
this.getAjsDocs().map((doc) =>
|
|
423
|
+
this.boundNavItem(doc.title, doc, () => this.selectDoc(doc))
|
|
495
424
|
)
|
|
496
425
|
)
|
|
497
426
|
)
|
|
498
427
|
}
|
|
499
428
|
|
|
500
429
|
handleToggle = (event: Event) => {
|
|
501
|
-
const
|
|
502
|
-
const section =
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
430
|
+
const det = event.target as HTMLDetailsElement
|
|
431
|
+
const section = det.getAttribute('data-section')
|
|
432
|
+
if (!this._appState) return
|
|
433
|
+
|
|
434
|
+
const current = this._appState.openSection.valueOf()
|
|
435
|
+
if (det.open && current !== section) {
|
|
436
|
+
// User opened a section — update state (bind handles the rest)
|
|
437
|
+
this._appState.openSection.value = section
|
|
438
|
+
} else if (!det.open && current === section) {
|
|
439
|
+
// User closed the current section
|
|
440
|
+
this._appState.openSection.value = null
|
|
512
441
|
}
|
|
513
442
|
}
|
|
514
443
|
|
|
@@ -602,6 +531,7 @@ export class DemoNav extends Component {
|
|
|
602
531
|
padding: '4px',
|
|
603
532
|
border: 'none',
|
|
604
533
|
background: 'transparent',
|
|
534
|
+
color: vars.textColor,
|
|
605
535
|
cursor: 'pointer',
|
|
606
536
|
},
|
|
607
537
|
},
|
|
@@ -620,7 +550,8 @@ export class DemoNav extends Component {
|
|
|
620
550
|
width: '500px',
|
|
621
551
|
maxWidth: 'calc(100vw - 40px)',
|
|
622
552
|
maxHeight: '80vh',
|
|
623
|
-
background:
|
|
553
|
+
background: vars.background,
|
|
554
|
+
color: vars.textColor,
|
|
624
555
|
borderRadius: '8px',
|
|
625
556
|
boxShadow: '0 4px 20px rgba(0,0,0,0.15)',
|
|
626
557
|
overflow: 'hidden',
|
|
@@ -634,13 +565,16 @@ export class DemoNav extends Component {
|
|
|
634
565
|
display: 'flex',
|
|
635
566
|
alignItems: 'center',
|
|
636
567
|
padding: '6px 12px',
|
|
637
|
-
background:
|
|
638
|
-
borderBottom:
|
|
568
|
+
background: vars.codeBackground,
|
|
569
|
+
borderBottom: `1px solid ${vars.codeBorder}`,
|
|
639
570
|
cursor: 'move',
|
|
640
571
|
},
|
|
641
572
|
},
|
|
642
573
|
span(
|
|
643
|
-
{
|
|
574
|
+
{
|
|
575
|
+
class: 'float-title',
|
|
576
|
+
style: { flex: '1', fontWeight: '500', color: vars.textColor },
|
|
577
|
+
},
|
|
644
578
|
doc.title
|
|
645
579
|
),
|
|
646
580
|
closeBtn
|
package/demo/src/imports.ts
CHANGED
|
@@ -20,19 +20,28 @@ const moduleCache = new Map<string, string>()
|
|
|
20
20
|
// Common packages with pinned versions and ESM paths
|
|
21
21
|
// Packages with proper "exports" or "module" fields in package.json
|
|
22
22
|
// may work without explicit paths, but it's safer to specify them
|
|
23
|
-
|
|
24
|
-
string
|
|
25
|
-
|
|
26
|
-
|
|
23
|
+
interface PinnedPackage {
|
|
24
|
+
version: string
|
|
25
|
+
path?: string
|
|
26
|
+
cdn?: string
|
|
27
|
+
// Transitive deps to include in the import map when this package is imported
|
|
28
|
+
deps?: string[]
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const PINNED_PACKAGES: Record<string, PinnedPackage> = {
|
|
27
32
|
// tjs-lang itself (used by demos like Universal Endpoint)
|
|
28
33
|
'tjs-lang': {
|
|
29
|
-
version: '0.5.
|
|
30
|
-
cdn: 'https://cdn.jsdelivr.net/npm/tjs-lang@0.5.
|
|
34
|
+
version: '0.5.4',
|
|
35
|
+
cdn: 'https://cdn.jsdelivr.net/npm/tjs-lang@0.5.4/dist/index.js',
|
|
31
36
|
},
|
|
32
37
|
|
|
33
38
|
// tosijs ecosystem
|
|
34
39
|
tosijs: { version: '1.2.0', path: '/dist/module.js' },
|
|
35
|
-
'tosijs-ui': {
|
|
40
|
+
'tosijs-ui': {
|
|
41
|
+
version: '1.2.0',
|
|
42
|
+
path: '/dist/index.js',
|
|
43
|
+
deps: ['tosijs', 'marked'],
|
|
44
|
+
},
|
|
36
45
|
|
|
37
46
|
// Utilities - lodash-es is native ESM
|
|
38
47
|
'lodash-es': { version: '4.17.21' },
|
|
@@ -56,13 +65,38 @@ const PINNED_PACKAGES: Record<
|
|
|
56
65
|
'react-dom': {
|
|
57
66
|
version: '18.2.0',
|
|
58
67
|
cdn: 'https://cdn.jsdelivr.net/npm/react-dom@18.2.0/+esm',
|
|
68
|
+
deps: ['react'],
|
|
59
69
|
},
|
|
60
70
|
'react-dom/client': {
|
|
61
71
|
version: '18.2.0',
|
|
62
72
|
cdn: 'https://cdn.jsdelivr.net/npm/react-dom@18.2.0/client/+esm',
|
|
73
|
+
deps: ['react', 'react-dom'],
|
|
63
74
|
},
|
|
64
75
|
}
|
|
65
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Expand a list of specifiers to include transitive deps from PINNED_PACKAGES.
|
|
79
|
+
* Walks the deps graph to collect all needed packages.
|
|
80
|
+
*/
|
|
81
|
+
function expandWithDeps(specifiers: string[]): string[] {
|
|
82
|
+
const all = new Set(specifiers)
|
|
83
|
+
const queue = [...specifiers]
|
|
84
|
+
while (queue.length > 0) {
|
|
85
|
+
const spec = queue.pop()!
|
|
86
|
+
const { name } = parseSpecifier(spec)
|
|
87
|
+
const pinned = PINNED_PACKAGES[spec] || PINNED_PACKAGES[name]
|
|
88
|
+
if (pinned?.deps) {
|
|
89
|
+
for (const dep of pinned.deps) {
|
|
90
|
+
if (!all.has(dep)) {
|
|
91
|
+
all.add(dep)
|
|
92
|
+
queue.push(dep)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return [...all]
|
|
98
|
+
}
|
|
99
|
+
|
|
66
100
|
/**
|
|
67
101
|
* Extract import specifiers from source code
|
|
68
102
|
*/
|
|
@@ -156,7 +190,7 @@ export function generateImportMap(specifiers: string[]): {
|
|
|
156
190
|
} {
|
|
157
191
|
const imports: Record<string, string> = {}
|
|
158
192
|
|
|
159
|
-
for (const specifier of specifiers) {
|
|
193
|
+
for (const specifier of expandWithDeps(specifiers)) {
|
|
160
194
|
imports[specifier] = getCDNUrl(specifier)
|
|
161
195
|
}
|
|
162
196
|
|
|
@@ -300,7 +334,7 @@ export async function resolveImports(source: string): Promise<{
|
|
|
300
334
|
errors: string[]
|
|
301
335
|
localModules: string[]
|
|
302
336
|
}> {
|
|
303
|
-
const specifiers = extractImports(source)
|
|
337
|
+
const specifiers = expandWithDeps(extractImports(source))
|
|
304
338
|
const errors: string[] = []
|
|
305
339
|
const imports: Record<string, string> = {}
|
|
306
340
|
const localModules: string[] = []
|
|
@@ -79,6 +79,8 @@ export interface IframeDocOptions {
|
|
|
79
79
|
parentBindings?: boolean
|
|
80
80
|
/** Auto-find and call TJS-annotated functions, append DOM results */
|
|
81
81
|
autoCallTjsFunction?: boolean
|
|
82
|
+
/** Whether parent is in dark mode — sets color-scheme on iframe */
|
|
83
|
+
darkMode?: boolean
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
/**
|
|
@@ -95,8 +97,11 @@ export function buildIframeDoc(options: IframeDocOptions): string {
|
|
|
95
97
|
importStatements = [],
|
|
96
98
|
parentBindings = false,
|
|
97
99
|
autoCallTjsFunction = false,
|
|
100
|
+
darkMode = false,
|
|
98
101
|
} = options
|
|
99
102
|
|
|
103
|
+
const colorScheme = darkMode ? 'dark' : 'light dark'
|
|
104
|
+
|
|
100
105
|
const parentBindingsScript = parentBindings
|
|
101
106
|
? `
|
|
102
107
|
if (parent.run) window.run = parent.run.bind(parent);
|
|
@@ -146,6 +151,7 @@ export function buildIframeDoc(options: IframeDocOptions): string {
|
|
|
146
151
|
return `<!DOCTYPE html>
|
|
147
152
|
<html>
|
|
148
153
|
<head>
|
|
154
|
+
<style>:root { color-scheme: ${colorScheme} }</style>
|
|
149
155
|
<style>${cssContent}</style>
|
|
150
156
|
${importMapScript}
|
|
151
157
|
</head>
|
|
@@ -174,6 +180,7 @@ ${CONSOLE_CAPTURE_SCRIPT}
|
|
|
174
180
|
return `<!DOCTYPE html>
|
|
175
181
|
<html>
|
|
176
182
|
<head>
|
|
183
|
+
<style>:root { color-scheme: ${colorScheme} }</style>
|
|
177
184
|
<style>${cssContent}</style>
|
|
178
185
|
${importMapScript}
|
|
179
186
|
</head>
|
|
@@ -1034,6 +1034,7 @@ export class TJSPlayground extends Component<TJSPlaygroundParts> {
|
|
|
1034
1034
|
importStatements,
|
|
1035
1035
|
parentBindings: true,
|
|
1036
1036
|
autoCallTjsFunction: true,
|
|
1037
|
+
darkMode: document.body.classList.contains('darkmode'),
|
|
1037
1038
|
})
|
|
1038
1039
|
|
|
1039
1040
|
// Listen for messages from iframe
|