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,635 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified Module Store for Playground Examples
|
|
3
|
+
*
|
|
4
|
+
* Stores user-created modules (both AJS and TJS) in IndexedDB.
|
|
5
|
+
* Modules can be imported by name from other modules.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* const store = await ModuleStore.open()
|
|
10
|
+
*
|
|
11
|
+
* // Save a TJS module
|
|
12
|
+
* await store.save({
|
|
13
|
+
* name: 'utils',
|
|
14
|
+
* type: 'tjs',
|
|
15
|
+
* code: 'export function add(a: 0, b: 0) { return a + b }',
|
|
16
|
+
* description: 'Math utilities'
|
|
17
|
+
* })
|
|
18
|
+
*
|
|
19
|
+
* // Import it from another module
|
|
20
|
+
* // import { add } from 'utils'
|
|
21
|
+
*
|
|
22
|
+
* // Get transpiled code for import resolution
|
|
23
|
+
* const compiled = await store.getCompiled('utils')
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import {
|
|
28
|
+
transpileToJS,
|
|
29
|
+
extractTests,
|
|
30
|
+
testUtils,
|
|
31
|
+
type TJSTranspileResult,
|
|
32
|
+
} from '../../src/lang'
|
|
33
|
+
|
|
34
|
+
// ============================================================================
|
|
35
|
+
// Validation Result
|
|
36
|
+
// ============================================================================
|
|
37
|
+
|
|
38
|
+
export interface ValidationResult {
|
|
39
|
+
valid: boolean
|
|
40
|
+
errors: ValidationError[]
|
|
41
|
+
warnings: string[]
|
|
42
|
+
testResults?: TestResult[]
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface ValidationError {
|
|
46
|
+
type: 'transpile' | 'test' | 'syntax'
|
|
47
|
+
message: string
|
|
48
|
+
line?: number
|
|
49
|
+
column?: number
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface TestResult {
|
|
53
|
+
name: string
|
|
54
|
+
passed: boolean
|
|
55
|
+
error?: string
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ============================================================================
|
|
59
|
+
// Types
|
|
60
|
+
// ============================================================================
|
|
61
|
+
|
|
62
|
+
export type ModuleType = 'tjs' | 'ajs'
|
|
63
|
+
|
|
64
|
+
export interface StoredModule {
|
|
65
|
+
/** Unique module name (used for imports) */
|
|
66
|
+
name: string
|
|
67
|
+
/** Module type */
|
|
68
|
+
type: ModuleType
|
|
69
|
+
/** Source code */
|
|
70
|
+
code: string
|
|
71
|
+
/** Optional description */
|
|
72
|
+
description?: string
|
|
73
|
+
/** When created */
|
|
74
|
+
created: number
|
|
75
|
+
/** When last modified */
|
|
76
|
+
modified: number
|
|
77
|
+
/** Version number (increments on save) */
|
|
78
|
+
version: number
|
|
79
|
+
/** Cached transpilation result (for TJS) */
|
|
80
|
+
compiled?: {
|
|
81
|
+
code: string
|
|
82
|
+
version: number
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface ModuleStoreStats {
|
|
87
|
+
total: number
|
|
88
|
+
tjs: number
|
|
89
|
+
ajs: number
|
|
90
|
+
bytes: number
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ============================================================================
|
|
94
|
+
// IndexedDB Setup
|
|
95
|
+
// ============================================================================
|
|
96
|
+
|
|
97
|
+
const DB_NAME = 'tjs-modules'
|
|
98
|
+
const DB_VERSION = 1
|
|
99
|
+
const STORE_NAME = 'modules'
|
|
100
|
+
|
|
101
|
+
// ============================================================================
|
|
102
|
+
// ModuleStore Class
|
|
103
|
+
// ============================================================================
|
|
104
|
+
|
|
105
|
+
export class ModuleStore {
|
|
106
|
+
private db: IDBDatabase | null = null
|
|
107
|
+
private static instance: ModuleStore | null = null
|
|
108
|
+
|
|
109
|
+
private constructor() {}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Get singleton instance
|
|
113
|
+
*/
|
|
114
|
+
static async open(): Promise<ModuleStore> {
|
|
115
|
+
if (ModuleStore.instance?.db) {
|
|
116
|
+
return ModuleStore.instance
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const store = new ModuleStore()
|
|
120
|
+
await store._open()
|
|
121
|
+
ModuleStore.instance = store
|
|
122
|
+
return store
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private async _open(): Promise<void> {
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
const request = indexedDB.open(DB_NAME, DB_VERSION)
|
|
128
|
+
|
|
129
|
+
request.onerror = () => reject(request.error)
|
|
130
|
+
|
|
131
|
+
request.onsuccess = () => {
|
|
132
|
+
this.db = request.result
|
|
133
|
+
resolve()
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
request.onupgradeneeded = (event) => {
|
|
137
|
+
const db = (event.target as IDBOpenDBRequest).result
|
|
138
|
+
|
|
139
|
+
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
|
140
|
+
const store = db.createObjectStore(STORE_NAME, { keyPath: 'name' })
|
|
141
|
+
store.createIndex('type', 'type')
|
|
142
|
+
store.createIndex('modified', 'modified')
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Validate a module before saving
|
|
150
|
+
* Checks transpilation and runs inline tests
|
|
151
|
+
*/
|
|
152
|
+
async validate(code: string, type: ModuleType): Promise<ValidationResult> {
|
|
153
|
+
const errors: ValidationError[] = []
|
|
154
|
+
const warnings: string[] = []
|
|
155
|
+
let testResults: TestResult[] | undefined
|
|
156
|
+
|
|
157
|
+
if (type === 'tjs') {
|
|
158
|
+
// Step 1: Try to transpile (use 'report' mode to get test results without throwing)
|
|
159
|
+
let transpileResult: TJSTranspileResult
|
|
160
|
+
try {
|
|
161
|
+
transpileResult = transpileToJS(code, { runTests: 'report' })
|
|
162
|
+
if (transpileResult.warnings) {
|
|
163
|
+
warnings.push(...transpileResult.warnings)
|
|
164
|
+
}
|
|
165
|
+
} catch (e: any) {
|
|
166
|
+
// This is a real transpilation error (syntax, parse, etc.)
|
|
167
|
+
errors.push({
|
|
168
|
+
type: 'transpile',
|
|
169
|
+
message: e.message,
|
|
170
|
+
line: e.line,
|
|
171
|
+
column: e.column,
|
|
172
|
+
})
|
|
173
|
+
return { valid: false, errors, warnings }
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Step 2: Collect signature test results from transpile
|
|
177
|
+
// Map from transpiler's TestResult (description) to our TestResult (name)
|
|
178
|
+
if (
|
|
179
|
+
transpileResult.testResults &&
|
|
180
|
+
transpileResult.testResults.length > 0
|
|
181
|
+
) {
|
|
182
|
+
testResults = transpileResult.testResults.map((t) => ({
|
|
183
|
+
name: t.description,
|
|
184
|
+
passed: t.passed,
|
|
185
|
+
error: t.error,
|
|
186
|
+
}))
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Step 3: Extract and run explicit test blocks
|
|
190
|
+
const testExtraction = extractTests(code)
|
|
191
|
+
if (testExtraction.tests.length > 0) {
|
|
192
|
+
try {
|
|
193
|
+
const explicitResults = await this.runTests(
|
|
194
|
+
transpileResult.code,
|
|
195
|
+
testExtraction.testRunner
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
// Merge with signature test results
|
|
199
|
+
testResults = testResults
|
|
200
|
+
? [...testResults, ...explicitResults]
|
|
201
|
+
: explicitResults
|
|
202
|
+
|
|
203
|
+
// Check for failed tests
|
|
204
|
+
const failed = explicitResults.filter((t) => !t.passed)
|
|
205
|
+
if (failed.length > 0) {
|
|
206
|
+
for (const f of failed) {
|
|
207
|
+
errors.push({
|
|
208
|
+
type: 'test',
|
|
209
|
+
message: `Test "${f.name}" failed: ${f.error}`,
|
|
210
|
+
})
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
} catch (e: any) {
|
|
214
|
+
errors.push({
|
|
215
|
+
type: 'test',
|
|
216
|
+
message: `Test execution error: ${e.message}`,
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// AJS validation could be added here (parse with ajs())
|
|
223
|
+
|
|
224
|
+
return {
|
|
225
|
+
valid: errors.length === 0,
|
|
226
|
+
errors,
|
|
227
|
+
warnings,
|
|
228
|
+
testResults,
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Run tests in an isolated context
|
|
234
|
+
*/
|
|
235
|
+
private async runTests(
|
|
236
|
+
moduleCode: string,
|
|
237
|
+
testRunnerCode: string
|
|
238
|
+
): Promise<TestResult[]> {
|
|
239
|
+
// Create isolated execution context
|
|
240
|
+
const fullCode = `
|
|
241
|
+
${testUtils}
|
|
242
|
+
${moduleCode}
|
|
243
|
+
${testRunnerCode}
|
|
244
|
+
`
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
// Run in async function to handle await in tests
|
|
248
|
+
const runTests = new Function(`
|
|
249
|
+
return (async () => {
|
|
250
|
+
${fullCode}
|
|
251
|
+
})()
|
|
252
|
+
`)
|
|
253
|
+
|
|
254
|
+
const result = await runTests()
|
|
255
|
+
|
|
256
|
+
if (result?.results) {
|
|
257
|
+
return result.results.map((r: any) => ({
|
|
258
|
+
name: r.description,
|
|
259
|
+
passed: r.passed,
|
|
260
|
+
error: r.error,
|
|
261
|
+
}))
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
return []
|
|
265
|
+
} catch (e: any) {
|
|
266
|
+
throw new Error(`Test execution failed: ${e.message}`)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Save a module (create or update)
|
|
272
|
+
* Validates the module first - fails if transpilation or tests fail
|
|
273
|
+
*/
|
|
274
|
+
async save(
|
|
275
|
+
module: Omit<StoredModule, 'created' | 'modified' | 'version' | 'compiled'>,
|
|
276
|
+
options: { skipValidation?: boolean } = {}
|
|
277
|
+
): Promise<StoredModule> {
|
|
278
|
+
if (!this.db) throw new Error('Store not open')
|
|
279
|
+
|
|
280
|
+
// Validate first (unless skipped)
|
|
281
|
+
if (!options.skipValidation) {
|
|
282
|
+
const validation = await this.validate(module.code, module.type)
|
|
283
|
+
if (!validation.valid) {
|
|
284
|
+
const errorMessages = validation.errors.map((e) => e.message).join('\n')
|
|
285
|
+
throw new Error(`Module validation failed:\n${errorMessages}`)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Check for existing
|
|
290
|
+
const existing = await this.get(module.name)
|
|
291
|
+
|
|
292
|
+
const now = Date.now()
|
|
293
|
+
const entry: StoredModule = {
|
|
294
|
+
...module,
|
|
295
|
+
created: existing?.created ?? now,
|
|
296
|
+
modified: now,
|
|
297
|
+
version: (existing?.version ?? 0) + 1,
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// Pre-compile TJS modules (we know it succeeds because validation passed)
|
|
301
|
+
if (module.type === 'tjs') {
|
|
302
|
+
try {
|
|
303
|
+
const result = transpileToJS(module.code)
|
|
304
|
+
entry.compiled = {
|
|
305
|
+
code: result.code,
|
|
306
|
+
version: entry.version,
|
|
307
|
+
}
|
|
308
|
+
} catch (e) {
|
|
309
|
+
// Should not happen if validation passed, but handle anyway
|
|
310
|
+
console.warn(`Compilation warning for ${module.name}:`, e)
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return new Promise((resolve, reject) => {
|
|
315
|
+
const tx = this.db!.transaction(STORE_NAME, 'readwrite')
|
|
316
|
+
const store = tx.objectStore(STORE_NAME)
|
|
317
|
+
const request = store.put(entry)
|
|
318
|
+
|
|
319
|
+
request.onerror = () => reject(request.error)
|
|
320
|
+
request.onsuccess = () => resolve(entry)
|
|
321
|
+
})
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Save without validation (use with caution)
|
|
326
|
+
*/
|
|
327
|
+
async saveUnsafe(
|
|
328
|
+
module: Omit<StoredModule, 'created' | 'modified' | 'version' | 'compiled'>
|
|
329
|
+
): Promise<StoredModule> {
|
|
330
|
+
return this.save(module, { skipValidation: true })
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Get a module by name
|
|
335
|
+
*/
|
|
336
|
+
async get(name: string): Promise<StoredModule | undefined> {
|
|
337
|
+
if (!this.db) throw new Error('Store not open')
|
|
338
|
+
|
|
339
|
+
return new Promise((resolve, reject) => {
|
|
340
|
+
const tx = this.db!.transaction(STORE_NAME, 'readonly')
|
|
341
|
+
const store = tx.objectStore(STORE_NAME)
|
|
342
|
+
const request = store.get(name)
|
|
343
|
+
|
|
344
|
+
request.onerror = () => reject(request.error)
|
|
345
|
+
request.onsuccess = () => resolve(request.result)
|
|
346
|
+
})
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Get compiled code for a TJS module (for import resolution)
|
|
351
|
+
*/
|
|
352
|
+
async getCompiled(name: string): Promise<string | undefined> {
|
|
353
|
+
const module = await this.get(name)
|
|
354
|
+
if (!module) return undefined
|
|
355
|
+
|
|
356
|
+
if (module.type === 'ajs') {
|
|
357
|
+
// AJS modules export their transpiled AST
|
|
358
|
+
return `export default ${JSON.stringify(module.code)}`
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Return cached compiled code, or compile on demand
|
|
362
|
+
if (module.compiled?.version === module.version) {
|
|
363
|
+
return module.compiled.code
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// Recompile if needed
|
|
367
|
+
try {
|
|
368
|
+
const result = transpileToJS(module.code)
|
|
369
|
+
// Update cache
|
|
370
|
+
await this.save(module)
|
|
371
|
+
return result.code
|
|
372
|
+
} catch (e: any) {
|
|
373
|
+
throw new Error(`Failed to compile module '${name}': ${e.message}`)
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Delete a module
|
|
379
|
+
*/
|
|
380
|
+
async delete(name: string): Promise<void> {
|
|
381
|
+
if (!this.db) throw new Error('Store not open')
|
|
382
|
+
|
|
383
|
+
return new Promise((resolve, reject) => {
|
|
384
|
+
const tx = this.db!.transaction(STORE_NAME, 'readwrite')
|
|
385
|
+
const store = tx.objectStore(STORE_NAME)
|
|
386
|
+
const request = store.delete(name)
|
|
387
|
+
|
|
388
|
+
request.onerror = () => reject(request.error)
|
|
389
|
+
request.onsuccess = () => resolve()
|
|
390
|
+
})
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* List all modules
|
|
395
|
+
*/
|
|
396
|
+
async list(type?: ModuleType): Promise<StoredModule[]> {
|
|
397
|
+
if (!this.db) throw new Error('Store not open')
|
|
398
|
+
|
|
399
|
+
return new Promise((resolve, reject) => {
|
|
400
|
+
const tx = this.db!.transaction(STORE_NAME, 'readonly')
|
|
401
|
+
const store = tx.objectStore(STORE_NAME)
|
|
402
|
+
|
|
403
|
+
let request: IDBRequest
|
|
404
|
+
if (type) {
|
|
405
|
+
const index = store.index('type')
|
|
406
|
+
request = index.getAll(type)
|
|
407
|
+
} else {
|
|
408
|
+
request = store.getAll()
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
request.onerror = () => reject(request.error)
|
|
412
|
+
request.onsuccess = () => {
|
|
413
|
+
const modules = request.result as StoredModule[]
|
|
414
|
+
// Sort by modified desc
|
|
415
|
+
modules.sort((a, b) => b.modified - a.modified)
|
|
416
|
+
resolve(modules)
|
|
417
|
+
}
|
|
418
|
+
})
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Check if a module exists
|
|
423
|
+
*/
|
|
424
|
+
async exists(name: string): Promise<boolean> {
|
|
425
|
+
const module = await this.get(name)
|
|
426
|
+
return module !== undefined
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Rename a module
|
|
431
|
+
*/
|
|
432
|
+
async rename(
|
|
433
|
+
oldName: string,
|
|
434
|
+
newName: string
|
|
435
|
+
): Promise<StoredModule | undefined> {
|
|
436
|
+
const module = await this.get(oldName)
|
|
437
|
+
if (!module) return undefined
|
|
438
|
+
|
|
439
|
+
// Check new name isn't taken
|
|
440
|
+
if (await this.exists(newName)) {
|
|
441
|
+
throw new Error(`Module '${newName}' already exists`)
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Delete old, create new
|
|
445
|
+
await this.delete(oldName)
|
|
446
|
+
return this.save({ ...module, name: newName })
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Get store statistics
|
|
451
|
+
*/
|
|
452
|
+
async getStats(): Promise<ModuleStoreStats> {
|
|
453
|
+
const modules = await this.list()
|
|
454
|
+
|
|
455
|
+
let bytes = 0
|
|
456
|
+
let tjs = 0
|
|
457
|
+
let ajs = 0
|
|
458
|
+
|
|
459
|
+
for (const m of modules) {
|
|
460
|
+
bytes += m.code.length * 2 // UTF-16
|
|
461
|
+
if (m.compiled) bytes += m.compiled.code.length * 2
|
|
462
|
+
if (m.type === 'tjs') tjs++
|
|
463
|
+
else ajs++
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
return { total: modules.length, tjs, ajs, bytes }
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/**
|
|
470
|
+
* Clear all modules
|
|
471
|
+
*/
|
|
472
|
+
async clear(): Promise<void> {
|
|
473
|
+
if (!this.db) throw new Error('Store not open')
|
|
474
|
+
|
|
475
|
+
return new Promise((resolve, reject) => {
|
|
476
|
+
const tx = this.db!.transaction(STORE_NAME, 'readwrite')
|
|
477
|
+
const store = tx.objectStore(STORE_NAME)
|
|
478
|
+
const request = store.clear()
|
|
479
|
+
|
|
480
|
+
request.onerror = () => reject(request.error)
|
|
481
|
+
request.onsuccess = () => resolve()
|
|
482
|
+
})
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Export all modules as JSON
|
|
487
|
+
*/
|
|
488
|
+
async export(): Promise<string> {
|
|
489
|
+
const modules = await this.list()
|
|
490
|
+
return JSON.stringify(modules, null, 2)
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Import modules from JSON
|
|
495
|
+
*/
|
|
496
|
+
async import(json: string, overwrite = false): Promise<number> {
|
|
497
|
+
const modules = JSON.parse(json) as StoredModule[]
|
|
498
|
+
let imported = 0
|
|
499
|
+
|
|
500
|
+
for (const m of modules) {
|
|
501
|
+
if (!overwrite && (await this.exists(m.name))) {
|
|
502
|
+
continue
|
|
503
|
+
}
|
|
504
|
+
await this.save({
|
|
505
|
+
name: m.name,
|
|
506
|
+
type: m.type,
|
|
507
|
+
code: m.code,
|
|
508
|
+
description: m.description,
|
|
509
|
+
})
|
|
510
|
+
imported++
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
return imported
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Get module names for import resolution
|
|
518
|
+
*/
|
|
519
|
+
async getModuleNames(): Promise<string[]> {
|
|
520
|
+
const modules = await this.list()
|
|
521
|
+
return modules.map((m) => m.name)
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Close the database connection
|
|
526
|
+
*/
|
|
527
|
+
close(): void {
|
|
528
|
+
if (this.db) {
|
|
529
|
+
this.db.close()
|
|
530
|
+
this.db = null
|
|
531
|
+
ModuleStore.instance = null
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// ============================================================================
|
|
537
|
+
// Import Resolution
|
|
538
|
+
// ============================================================================
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* Check if an import specifier refers to a local module
|
|
542
|
+
*/
|
|
543
|
+
export async function isLocalModule(specifier: string): Promise<boolean> {
|
|
544
|
+
// Local modules are bare specifiers that exist in the store
|
|
545
|
+
if (
|
|
546
|
+
specifier.startsWith('.') ||
|
|
547
|
+
specifier.startsWith('/') ||
|
|
548
|
+
specifier.includes('://')
|
|
549
|
+
) {
|
|
550
|
+
return false
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
const store = await ModuleStore.open()
|
|
554
|
+
return store.exists(specifier)
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
/**
|
|
558
|
+
* Resolve local module imports in source code
|
|
559
|
+
* Returns a blob URL that can be used in import map
|
|
560
|
+
*/
|
|
561
|
+
export async function resolveLocalModule(name: string): Promise<string> {
|
|
562
|
+
const store = await ModuleStore.open()
|
|
563
|
+
const code = await store.getCompiled(name)
|
|
564
|
+
|
|
565
|
+
if (!code) {
|
|
566
|
+
throw new Error(`Local module '${name}' not found`)
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Create a blob URL for the compiled code
|
|
570
|
+
const blob = new Blob([code], { type: 'application/javascript' })
|
|
571
|
+
return URL.createObjectURL(blob)
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Build import map entries for all local modules referenced in source
|
|
576
|
+
*/
|
|
577
|
+
export async function resolveLocalImports(
|
|
578
|
+
specifiers: string[]
|
|
579
|
+
): Promise<Record<string, string>> {
|
|
580
|
+
const imports: Record<string, string> = {}
|
|
581
|
+
const store = await ModuleStore.open()
|
|
582
|
+
const localNames = await store.getModuleNames()
|
|
583
|
+
|
|
584
|
+
for (const spec of specifiers) {
|
|
585
|
+
if (localNames.includes(spec)) {
|
|
586
|
+
imports[spec] = await resolveLocalModule(spec)
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return imports
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// ============================================================================
|
|
594
|
+
// Migration from localStorage
|
|
595
|
+
// ============================================================================
|
|
596
|
+
|
|
597
|
+
/**
|
|
598
|
+
* Migrate existing localStorage examples to IndexedDB
|
|
599
|
+
*/
|
|
600
|
+
export async function migrateFromLocalStorage(): Promise<number> {
|
|
601
|
+
const store = await ModuleStore.open()
|
|
602
|
+
let migrated = 0
|
|
603
|
+
|
|
604
|
+
// Migrate AJS examples
|
|
605
|
+
const ajsKey = 'agent-playground-examples'
|
|
606
|
+
const ajsStored = localStorage.getItem(ajsKey)
|
|
607
|
+
if (ajsStored) {
|
|
608
|
+
try {
|
|
609
|
+
const examples = JSON.parse(ajsStored) as Array<{
|
|
610
|
+
name: string
|
|
611
|
+
code: string
|
|
612
|
+
description?: string
|
|
613
|
+
}>
|
|
614
|
+
for (const ex of examples) {
|
|
615
|
+
if (!(await store.exists(ex.name))) {
|
|
616
|
+
await store.save({
|
|
617
|
+
name: ex.name,
|
|
618
|
+
type: 'ajs',
|
|
619
|
+
code: ex.code,
|
|
620
|
+
description: ex.description,
|
|
621
|
+
})
|
|
622
|
+
migrated++
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
// Don't delete localStorage yet - keep as backup
|
|
626
|
+
// localStorage.removeItem(ajsKey)
|
|
627
|
+
} catch (e) {
|
|
628
|
+
console.warn('Failed to migrate AJS examples:', e)
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// TJS playground didn't have localStorage persistence, so nothing to migrate
|
|
633
|
+
|
|
634
|
+
return migrated
|
|
635
|
+
}
|