aiplang 2.10.8 → 2.10.9
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/bin/aiplang.js +29 -89
- package/package.json +1 -1
- package/runtime/aiplang-hydrate.js +63 -97
- package/server/server.js +24 -10
package/bin/aiplang.js
CHANGED
|
@@ -5,7 +5,7 @@ const fs = require('fs')
|
|
|
5
5
|
const path = require('path')
|
|
6
6
|
const http = require('http')
|
|
7
7
|
|
|
8
|
-
const VERSION = '2.10.
|
|
8
|
+
const VERSION = '2.10.9'
|
|
9
9
|
const RUNTIME_DIR = path.join(__dirname, '..', 'runtime')
|
|
10
10
|
const cmd = process.argv[2]
|
|
11
11
|
const args = process.argv.slice(3)
|
|
@@ -24,10 +24,6 @@ const ic = n => ICONS[n] || n
|
|
|
24
24
|
const isDyn = s => s&&(s.includes('@')||s.includes('$'))
|
|
25
25
|
const hSize = n => n<1024?`${n}B`:`${(n/1024).toFixed(1)}KB`
|
|
26
26
|
|
|
27
|
-
// ─────────────────────────────────────────────────────────────────
|
|
28
|
-
// CLI COMMANDS
|
|
29
|
-
// ─────────────────────────────────────────────────────────────────
|
|
30
|
-
|
|
31
27
|
if (!cmd||cmd==='--help'||cmd==='-h') {
|
|
32
28
|
console.log(`
|
|
33
29
|
aiplang v${VERSION}
|
|
@@ -74,18 +70,12 @@ if (!cmd||cmd==='--help'||cmd==='-h') {
|
|
|
74
70
|
}
|
|
75
71
|
if (cmd==='--version'||cmd==='-v') { console.log(`aiplang v${VERSION}`); process.exit(0) }
|
|
76
72
|
|
|
77
|
-
// ─────────────────────────────────────────────────────────────────
|
|
78
|
-
// TEMPLATE SYSTEM
|
|
79
|
-
// Custom templates stored at ~/.aip/templates/<name>.aip
|
|
80
|
-
// ─────────────────────────────────────────────────────────────────
|
|
81
|
-
|
|
82
73
|
const TEMPLATES_DIR = path.join(require('os').homedir(), '.aip', 'templates')
|
|
83
74
|
|
|
84
75
|
function ensureTemplatesDir() {
|
|
85
76
|
if (!fs.existsSync(TEMPLATES_DIR)) fs.mkdirSync(TEMPLATES_DIR, { recursive: true })
|
|
86
77
|
}
|
|
87
78
|
|
|
88
|
-
// Built-in templates (interpolate {{name}} and {{year}})
|
|
89
79
|
const BUILTIN_TEMPLATES = {
|
|
90
80
|
saas: `# {{name}}
|
|
91
81
|
~db sqlite ./app.db
|
|
@@ -255,24 +245,20 @@ function applyTemplateVars(src, name, year) {
|
|
|
255
245
|
function getTemplate(tplName, name, year) {
|
|
256
246
|
ensureTemplatesDir()
|
|
257
247
|
|
|
258
|
-
// 1. Local file path: --template ./my-template.aip or --template /abs/path.aip
|
|
259
248
|
if (tplName.startsWith('./') || tplName.startsWith('../') || tplName.startsWith('/')) {
|
|
260
249
|
const full = path.resolve(tplName)
|
|
261
250
|
if (!fs.existsSync(full)) { console.error(`\n ✗ Template file not found: ${full}\n`); process.exit(1) }
|
|
262
251
|
return applyTemplateVars(fs.readFileSync(full, 'utf8'), name, year)
|
|
263
252
|
}
|
|
264
253
|
|
|
265
|
-
// 2. User custom template: ~/.aip/templates/<name>.aip
|
|
266
254
|
const customPath = path.join(TEMPLATES_DIR, tplName + '.aip')
|
|
267
255
|
if (fs.existsSync(customPath)) {
|
|
268
256
|
return applyTemplateVars(fs.readFileSync(customPath, 'utf8'), name, year)
|
|
269
257
|
}
|
|
270
258
|
|
|
271
|
-
// 3. Built-in template
|
|
272
259
|
const builtin = BUILTIN_TEMPLATES[tplName]
|
|
273
260
|
if (builtin) return applyTemplateVars(builtin, name, year)
|
|
274
261
|
|
|
275
|
-
// Not found — show what's available
|
|
276
262
|
const customs = fs.existsSync(TEMPLATES_DIR)
|
|
277
263
|
? fs.readdirSync(TEMPLATES_DIR).filter(f=>f.endsWith('.aip')).map(f=>f.replace('.aip',''))
|
|
278
264
|
: []
|
|
@@ -297,17 +283,14 @@ function listTemplates() {
|
|
|
297
283
|
console.log()
|
|
298
284
|
}
|
|
299
285
|
|
|
300
|
-
// ── template subcommand ──────────────────────────────────────────
|
|
301
286
|
if (cmd === 'template') {
|
|
302
287
|
const sub = args[0]
|
|
303
288
|
ensureTemplatesDir()
|
|
304
289
|
|
|
305
|
-
// aiplang template list
|
|
306
290
|
if (!sub || sub === 'list' || sub === 'ls') {
|
|
307
291
|
listTemplates(); process.exit(0)
|
|
308
292
|
}
|
|
309
293
|
|
|
310
|
-
// aiplang template save <name> [--from <file>]
|
|
311
294
|
if (sub === 'save' || sub === 'add') {
|
|
312
295
|
const tname = args[1]
|
|
313
296
|
if (!tname) { console.error('\n ✗ Usage: aiplang template save <name> [--from <file>]\n'); process.exit(1) }
|
|
@@ -318,7 +301,7 @@ if (cmd === 'template') {
|
|
|
318
301
|
if (!fs.existsSync(fp)) { console.error(`\n ✗ File not found: ${fp}\n`); process.exit(1) }
|
|
319
302
|
src = fs.readFileSync(fp, 'utf8')
|
|
320
303
|
} else {
|
|
321
|
-
|
|
304
|
+
|
|
322
305
|
const sources = ['pages', 'app.aip', 'index.aip']
|
|
323
306
|
const found = sources.find(s => fs.existsSync(s))
|
|
324
307
|
if (!found) { console.error('\n ✗ No .aip files found. Use --from <file> to specify source.\n'); process.exit(1) }
|
|
@@ -335,7 +318,6 @@ if (cmd === 'template') {
|
|
|
335
318
|
process.exit(0)
|
|
336
319
|
}
|
|
337
320
|
|
|
338
|
-
// aiplang template remove <name>
|
|
339
321
|
if (sub === 'remove' || sub === 'rm' || sub === 'delete') {
|
|
340
322
|
const tname = args[1]
|
|
341
323
|
if (!tname) { console.error('\n ✗ Usage: aiplang template remove <name>\n'); process.exit(1) }
|
|
@@ -345,13 +327,12 @@ if (cmd === 'template') {
|
|
|
345
327
|
console.log(`\n ✓ Removed template: ${tname}\n`); process.exit(0)
|
|
346
328
|
}
|
|
347
329
|
|
|
348
|
-
// aiplang template edit <name>
|
|
349
330
|
if (sub === 'edit' || sub === 'open') {
|
|
350
331
|
const tname = args[1]
|
|
351
332
|
if (!tname) { console.error('\n ✗ Usage: aiplang template edit <name>\n'); process.exit(1) }
|
|
352
333
|
let dest = path.join(TEMPLATES_DIR, tname + '.aip')
|
|
353
334
|
if (!fs.existsSync(dest)) {
|
|
354
|
-
|
|
335
|
+
|
|
355
336
|
const builtin = BUILTIN_TEMPLATES[tname]
|
|
356
337
|
if (builtin) { fs.writeFileSync(dest, builtin); console.log(`\n ✓ Copied built-in "${tname}" to custom templates.\n`) }
|
|
357
338
|
else { console.error(`\n ✗ Template "${tname}" not found.\n`); process.exit(1) }
|
|
@@ -362,7 +343,6 @@ if (cmd === 'template') {
|
|
|
362
343
|
process.exit(0)
|
|
363
344
|
}
|
|
364
345
|
|
|
365
|
-
// aiplang template show <name>
|
|
366
346
|
if (sub === 'show' || sub === 'cat') {
|
|
367
347
|
const tname = args[1] || 'default'
|
|
368
348
|
const customPath = path.join(TEMPLATES_DIR, tname + '.aip')
|
|
@@ -372,7 +352,6 @@ if (cmd === 'template') {
|
|
|
372
352
|
console.error(`\n ✗ Template "${tname}" not found.\n`); process.exit(1)
|
|
373
353
|
}
|
|
374
354
|
|
|
375
|
-
// aiplang template export <name> [--out <file>]
|
|
376
355
|
if (sub === 'export') {
|
|
377
356
|
const tname = args[1]
|
|
378
357
|
if (!tname) { console.error('\n ✗ Usage: aiplang template export <name>\n'); process.exit(1) }
|
|
@@ -390,7 +369,6 @@ if (cmd === 'template') {
|
|
|
390
369
|
process.exit(1)
|
|
391
370
|
}
|
|
392
371
|
|
|
393
|
-
// ── Init ─────────────────────────────────────────────────────────
|
|
394
372
|
if (cmd==='init') {
|
|
395
373
|
const tplIdx = args.indexOf('--template')
|
|
396
374
|
const tplName = tplIdx !== -1 ? args[tplIdx+1] : 'default'
|
|
@@ -399,15 +377,13 @@ if (cmd==='init') {
|
|
|
399
377
|
|
|
400
378
|
if (fs.existsSync(dir)) { console.error(`\n ✗ Directory "${name}" already exists.\n`); process.exit(1) }
|
|
401
379
|
|
|
402
|
-
// Get template source (built-in, custom, or file path)
|
|
403
380
|
const tplSrc = getTemplate(tplName, name, year)
|
|
404
381
|
|
|
405
|
-
// Check if template has full-stack backend (models/api blocks)
|
|
406
382
|
const isFullStack = tplSrc.includes('\nmodel ') || tplSrc.includes('\napi ')
|
|
407
383
|
const isMultiFile = tplSrc.includes('\n---\n')
|
|
408
384
|
|
|
409
385
|
if (isFullStack) {
|
|
410
|
-
|
|
386
|
+
|
|
411
387
|
fs.mkdirSync(dir, { recursive: true })
|
|
412
388
|
fs.writeFileSync(path.join(dir, 'app.aip'), tplSrc)
|
|
413
389
|
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({
|
|
@@ -421,7 +397,7 @@ if (cmd==='init') {
|
|
|
421
397
|
const label = tplName !== 'default' ? ` (template: ${tplName})` : ''
|
|
422
398
|
console.log(`\n ✓ Created ${name}/${label}\n\n app.aip ← full-stack app (backend + frontend)\n\n Next:\n cd ${name} && npx aiplang start app.aip\n`)
|
|
423
399
|
} else if (isMultiFile) {
|
|
424
|
-
|
|
400
|
+
|
|
425
401
|
fs.mkdirSync(path.join(dir,'pages'), {recursive:true})
|
|
426
402
|
fs.mkdirSync(path.join(dir,'public'), {recursive:true})
|
|
427
403
|
for (const f of ['aiplang-runtime.js','aiplang-hydrate.js']) {
|
|
@@ -439,7 +415,7 @@ if (cmd==='init') {
|
|
|
439
415
|
const files = fs.readdirSync(path.join(dir,'pages')).map(f=>f).join(', ')
|
|
440
416
|
console.log(`\n ✓ Created ${name}/${label}\n\n pages/{${files}} ← edit these\n\n Next:\n cd ${name} && npx aiplang serve\n`)
|
|
441
417
|
} else {
|
|
442
|
-
|
|
418
|
+
|
|
443
419
|
fs.mkdirSync(path.join(dir,'pages'), {recursive:true})
|
|
444
420
|
fs.mkdirSync(path.join(dir,'public'), {recursive:true})
|
|
445
421
|
for (const f of ['aiplang-runtime.js','aiplang-hydrate.js']) {
|
|
@@ -454,7 +430,6 @@ if (cmd==='init') {
|
|
|
454
430
|
process.exit(0)
|
|
455
431
|
}
|
|
456
432
|
|
|
457
|
-
// ── New ───────────────────────────────────────────────────────────
|
|
458
433
|
if (cmd==='new') {
|
|
459
434
|
const name=args[0]; if(!name){console.error('\n ✗ Usage: aiplang new <page>\n');process.exit(1)}
|
|
460
435
|
const dir=fs.existsSync('pages')?'pages':'.'
|
|
@@ -466,7 +441,6 @@ if (cmd==='new') {
|
|
|
466
441
|
process.exit(0)
|
|
467
442
|
}
|
|
468
443
|
|
|
469
|
-
// ── Build ─────────────────────────────────────────────────────────
|
|
470
444
|
if (cmd==='build') {
|
|
471
445
|
const outIdx=args.indexOf('--out')
|
|
472
446
|
const outDir=outIdx!==-1?args[outIdx+1]:'dist'
|
|
@@ -476,7 +450,7 @@ if (cmd==='build') {
|
|
|
476
450
|
fs.readdirSync(input).filter(f=>f.endsWith('.aip')).forEach(f=>files.push(path.join(input,f)))
|
|
477
451
|
} else if(input.endsWith('.aip')&&fs.existsSync(input)){ files.push(input) }
|
|
478
452
|
if(!files.length){console.error(`\n ✗ No .aip files in: ${input}\n`);process.exit(1)}
|
|
479
|
-
|
|
453
|
+
|
|
480
454
|
function resolveImports(content, baseDir, seen=new Set()) {
|
|
481
455
|
return content.replace(/^~import\s+["']?([^"'\n]+)["']?$/mg, (_, importPath) => {
|
|
482
456
|
const resolved = path.resolve(baseDir, importPath.trim())
|
|
@@ -511,7 +485,6 @@ if (cmd==='build') {
|
|
|
511
485
|
process.exit(0)
|
|
512
486
|
}
|
|
513
487
|
|
|
514
|
-
// ── Serve (hot reload) ────────────────────────────────────────────
|
|
515
488
|
if (cmd==='serve'||cmd==='dev') {
|
|
516
489
|
const root=path.resolve(args[0]||'.')
|
|
517
490
|
const port=parseInt(process.env.PORT||'3000')
|
|
@@ -549,7 +522,6 @@ if (cmd==='serve'||cmd==='dev') {
|
|
|
549
522
|
return
|
|
550
523
|
}
|
|
551
524
|
|
|
552
|
-
// ── Dev server (full-stack) ──────────────────────────────────────
|
|
553
525
|
if (cmd === 'start' || cmd === 'run') {
|
|
554
526
|
const aipFile = args[0]
|
|
555
527
|
if (!aipFile || !fs.existsSync(aipFile)) {
|
|
@@ -573,10 +545,6 @@ if (cmd === 'start' || cmd === 'run') {
|
|
|
573
545
|
console.error(`\n ✗ Unknown command: ${cmd}\n Run aiplang --help\n`)
|
|
574
546
|
process.exit(1)
|
|
575
547
|
|
|
576
|
-
// ═════════════════════════════════════════════════════════════════
|
|
577
|
-
// PARSER
|
|
578
|
-
// ═════════════════════════════════════════════════════════════════
|
|
579
|
-
|
|
580
548
|
function parsePages(src) {
|
|
581
549
|
return src.split(/\n---\n/).map(s=>parsePage(s.trim())).filter(Boolean)
|
|
582
550
|
}
|
|
@@ -619,8 +587,7 @@ function parseQuery(s) {
|
|
|
619
587
|
}
|
|
620
588
|
|
|
621
589
|
function parseBlock(line) {
|
|
622
|
-
|
|
623
|
-
// animate:fade-up class:my-class (can appear at end of any block line)
|
|
590
|
+
|
|
624
591
|
let extraClass=null, animate=null
|
|
625
592
|
const _cm=line.match(/\bclass:(\S+)/)
|
|
626
593
|
if(_cm){extraClass=_cm[1];line=line.replace(_cm[0],'').trim()}
|
|
@@ -636,12 +603,10 @@ function parseBlock(line) {
|
|
|
636
603
|
const _bgm=line.match(/\bbg:(#[0-9a-fA-F]+|[a-z]+)/)
|
|
637
604
|
if(_bgm){bg=_bgm[1];line=line.replace(_bgm[0],'').trim()}
|
|
638
605
|
|
|
639
|
-
// ── raw{} HTML passthrough ──────────────────────────────────
|
|
640
606
|
if(line.startsWith('raw{')) {
|
|
641
607
|
return{kind:'raw',html:line.slice(4,line.lastIndexOf('}')),extraClass,animate}
|
|
642
608
|
}
|
|
643
609
|
|
|
644
|
-
// ── table ───────────────────────────────────────────────────
|
|
645
610
|
if(line.startsWith('table ') || line.startsWith('table{')) {
|
|
646
611
|
const idx=line.indexOf('{');if(idx===-1) return null
|
|
647
612
|
const start=line.startsWith('table{')?6:6
|
|
@@ -659,7 +624,6 @@ function parseBlock(line) {
|
|
|
659
624
|
return{kind:'table',binding,cols:Array.isArray(cols)?cols:[],empty:parseEmpty(clean),editPath:em?.[2]||null,editMethod:em?.[1]||'PUT',deletePath:dm?.[1]||null,deleteKey:'id',fallback:fallbackM?.[1]?.trim()||null,retry:retryM?.[1]||null,extraClass,animate,variant,style,bg}
|
|
660
625
|
}
|
|
661
626
|
|
|
662
|
-
// ── form ────────────────────────────────────────────────────
|
|
663
627
|
if(line.startsWith('form ') || line.startsWith('form{')) {
|
|
664
628
|
const bi=line.indexOf('{');if(bi===-1) return null
|
|
665
629
|
let head=line.slice(line.startsWith('form{')?4:5,bi).trim()
|
|
@@ -667,7 +631,7 @@ function parseBlock(line) {
|
|
|
667
631
|
let action='', optimistic=false; const ai=head.indexOf('=>')
|
|
668
632
|
if(ai!==-1){
|
|
669
633
|
action=head.slice(ai+2).trim()
|
|
670
|
-
|
|
634
|
+
|
|
671
635
|
if(action.includes('.optimistic(')){optimistic=true;action=action.replace('.optimistic','')}
|
|
672
636
|
head=head.slice(0,ai).trim()
|
|
673
637
|
}
|
|
@@ -677,7 +641,6 @@ function parseBlock(line) {
|
|
|
677
641
|
return{kind:'form',method,bpath,action,optimistic,fields:parseFields(content)||[],extraClass,animate,variant,style,bg}
|
|
678
642
|
}
|
|
679
643
|
|
|
680
|
-
// ── pricing ─────────────────────────────────────────────────
|
|
681
644
|
if(line.startsWith('pricing{')) {
|
|
682
645
|
const body=line.slice(8,line.lastIndexOf('}')).trim()
|
|
683
646
|
const plans=body.split('|').map(p=>{
|
|
@@ -687,14 +650,12 @@ function parseBlock(line) {
|
|
|
687
650
|
return{kind:'pricing',plans,extraClass,animate,variant,style,bg}
|
|
688
651
|
}
|
|
689
652
|
|
|
690
|
-
// ── faq ─────────────────────────────────────────────────────
|
|
691
653
|
if(line.startsWith('faq{')) {
|
|
692
654
|
const body=line.slice(4,line.lastIndexOf('}')).trim()
|
|
693
655
|
const items=body.split('|').map(i=>{const idx=i.indexOf('>');return{q:i.slice(0,idx).trim(),a:i.slice(idx+1).trim()}}).filter(i=>i.q&&i.a)
|
|
694
656
|
return{kind:'faq',items,extraClass,animate}
|
|
695
657
|
}
|
|
696
658
|
|
|
697
|
-
// ── testimonial ──────────────────────────────────────────────
|
|
698
659
|
if(line.startsWith('testimonial{')) {
|
|
699
660
|
const body=line.slice(12,line.lastIndexOf('}')).trim()
|
|
700
661
|
const parts=body.split('|').map(x=>x.trim())
|
|
@@ -702,12 +663,10 @@ function parseBlock(line) {
|
|
|
702
663
|
return{kind:'testimonial',author:parts[0],quote:parts[1]?.replace(/^"|"$/g,''),img:imgPart?.slice(4)||null,extraClass,animate}
|
|
703
664
|
}
|
|
704
665
|
|
|
705
|
-
// ── gallery ──────────────────────────────────────────────────
|
|
706
666
|
if(line.startsWith('gallery{')) {
|
|
707
667
|
return{kind:'gallery',imgs:line.slice(8,line.lastIndexOf('}')).trim().split('|').map(x=>x.trim()).filter(Boolean),extraClass,animate}
|
|
708
668
|
}
|
|
709
669
|
|
|
710
|
-
// ── btn ──────────────────────────────────────────────────────
|
|
711
670
|
if(line.startsWith('btn{')) {
|
|
712
671
|
const parts=line.slice(4,line.lastIndexOf('}')).split('>').map(p=>p.trim())
|
|
713
672
|
const label=parts[0]||'Click', method=parts[1]?.split(' ')[0]||'POST'
|
|
@@ -717,7 +676,6 @@ function parseBlock(line) {
|
|
|
717
676
|
return{kind:'btn',label,method,bpath,action,confirm,extraClass,animate}
|
|
718
677
|
}
|
|
719
678
|
|
|
720
|
-
// ── card{} — standalone card customizável ──────────────────
|
|
721
679
|
if(line.startsWith('card{') || line.startsWith('card ')) {
|
|
722
680
|
const bi=line.indexOf('{'); if(bi===-1) return null
|
|
723
681
|
const body=line.slice(bi+1,line.lastIndexOf('}')).trim()
|
|
@@ -731,7 +689,6 @@ function parseBlock(line) {
|
|
|
731
689
|
return{kind:'card',title,subtitle,img:imgPart?.slice(4)||null,link:linkPart||null,badge,bind,extraClass,animate,variant,style,bg}
|
|
732
690
|
}
|
|
733
691
|
|
|
734
|
-
// ── cols{} — grid de conteúdo livre ─────────────────────────
|
|
735
692
|
if(line.startsWith('cols{') || (line.startsWith('cols ') && line.includes('{'))) {
|
|
736
693
|
const bi=line.indexOf('{'); if(bi===-1) return null
|
|
737
694
|
const head=line.slice(0,bi).trim()
|
|
@@ -742,19 +699,16 @@ function parseBlock(line) {
|
|
|
742
699
|
return{kind:'cols',n,items,extraClass,animate,variant,style,bg}
|
|
743
700
|
}
|
|
744
701
|
|
|
745
|
-
// ── divider{} — separador visual ─────────────────────────────
|
|
746
702
|
if(line.startsWith('divider') || line.startsWith('hr{')) {
|
|
747
703
|
const label=line.match(/\{([^}]*)\}/)?.[1]?.trim()||null
|
|
748
704
|
return{kind:'divider',label,extraClass,animate,variant,style}
|
|
749
705
|
}
|
|
750
706
|
|
|
751
|
-
// ── badge{} — label/tag destacado ───────────────────────────
|
|
752
707
|
if(line.startsWith('badge{') || line.startsWith('tag{')) {
|
|
753
708
|
const content=line.slice(line.indexOf('{')+1,line.lastIndexOf('}')).trim()
|
|
754
709
|
return{kind:'badge',content,extraClass,animate,variant,style}
|
|
755
710
|
}
|
|
756
711
|
|
|
757
|
-
// ── select ───────────────────────────────────────────────────
|
|
758
712
|
if(line.startsWith('select ')) {
|
|
759
713
|
const bi=line.indexOf('{')
|
|
760
714
|
const varName=bi!==-1?line.slice(7,bi).trim():line.slice(7).trim()
|
|
@@ -762,13 +716,11 @@ function parseBlock(line) {
|
|
|
762
716
|
return{kind:'select',binding:varName,options:body.split('|').map(o=>o.trim()).filter(Boolean),extraClass,animate}
|
|
763
717
|
}
|
|
764
718
|
|
|
765
|
-
// ── if ───────────────────────────────────────────────────────
|
|
766
719
|
if(line.startsWith('if ')) {
|
|
767
720
|
const bi=line.indexOf('{');if(bi===-1) return null
|
|
768
721
|
return{kind:'if',cond:line.slice(3,bi).trim(),inner:line.slice(bi+1,line.lastIndexOf('}')).trim(),extraClass,animate}
|
|
769
722
|
}
|
|
770
723
|
|
|
771
|
-
// ── chart{} — data visualization ───────────────────────────────
|
|
772
724
|
if(line.startsWith('chart{') || line.startsWith('chart ')) {
|
|
773
725
|
const bi=line.indexOf('{'); if(bi===-1) return null
|
|
774
726
|
const body=line.slice(bi+1,line.lastIndexOf('}')).trim()
|
|
@@ -781,7 +733,6 @@ function parseBlock(line) {
|
|
|
781
733
|
return{kind:'chart',type,binding,labels,values,title,extraClass,animate,variant,style}
|
|
782
734
|
}
|
|
783
735
|
|
|
784
|
-
// ── kanban{} — drag-and-drop board ───────────────────────────────
|
|
785
736
|
if(line.startsWith('kanban{') || line.startsWith('kanban ')) {
|
|
786
737
|
const bi=line.indexOf('{'); if(bi===-1) return null
|
|
787
738
|
const body=line.slice(bi+1,line.lastIndexOf('}')).trim()
|
|
@@ -793,7 +744,6 @@ function parseBlock(line) {
|
|
|
793
744
|
return{kind:'kanban',binding,cols,statusField,updatePath,extraClass,animate,style}
|
|
794
745
|
}
|
|
795
746
|
|
|
796
|
-
// ── editor{} — rich text editor ──────────────────────────────────
|
|
797
747
|
if(line.startsWith('editor{') || line.startsWith('editor ')) {
|
|
798
748
|
const bi=line.indexOf('{'); if(bi===-1) return null
|
|
799
749
|
const body=line.slice(bi+1,line.lastIndexOf('}')).trim()
|
|
@@ -804,7 +754,6 @@ function parseBlock(line) {
|
|
|
804
754
|
return{kind:'editor',name,placeholder,submitPath,extraClass,animate,style}
|
|
805
755
|
}
|
|
806
756
|
|
|
807
|
-
// ── each @list { template } — loop como React .map() ────────
|
|
808
757
|
if(line.startsWith('each ')) {
|
|
809
758
|
const bi=line.indexOf('{');if(bi===-1) return null
|
|
810
759
|
const binding=line.slice(5,bi).trim()
|
|
@@ -812,18 +761,15 @@ function parseBlock(line) {
|
|
|
812
761
|
return{kind:'each',binding,tpl,extraClass,animate,variant}
|
|
813
762
|
}
|
|
814
763
|
|
|
815
|
-
// ── spacer{} — espaçamento customizável ──────────────────────
|
|
816
764
|
if(line.startsWith('spacer{') || line.startsWith('spacer ')) {
|
|
817
765
|
const h=line.match(/[{\s](\S+)[}]?/)?.[1]||'3rem'
|
|
818
766
|
return{kind:'spacer',height:h,extraClass,animate}
|
|
819
767
|
}
|
|
820
768
|
|
|
821
|
-
// ── html{} — HTML inline com interpolação de @state ──────────
|
|
822
769
|
if(line.startsWith('html{')) {
|
|
823
770
|
return{kind:'html',content:line.slice(5,line.lastIndexOf('}')),extraClass,animate}
|
|
824
771
|
}
|
|
825
772
|
|
|
826
|
-
// ── regular blocks (nav, hero, stats, rowN, sect, foot) ──────
|
|
827
773
|
const bi=line.indexOf('{');if(bi===-1) return null
|
|
828
774
|
const head=line.slice(0,bi).trim()
|
|
829
775
|
const body=line.slice(bi+1,line.lastIndexOf('}')).trim()
|
|
@@ -846,41 +792,43 @@ function parseCols(s){return s.split('|').map(c=>{c=c.trim();if(c.startsWith('em
|
|
|
846
792
|
function parseEmpty(s){const m=s.match(/empty:\s*([^|]+)/);return m?m[1].trim():'No data.'}
|
|
847
793
|
function parseFields(s){return s.split('|').map(f=>{const[label,type,ph]=f.split(':').map(x=>x.trim());return label?{label,type:type||'text',placeholder:ph||'',name:label.toLowerCase().replace(/\s+/g,'_')}:null}).filter(Boolean)}
|
|
848
794
|
|
|
849
|
-
// ═════════════════════════════════════════════════════════════════
|
|
850
|
-
// RENDERER
|
|
851
|
-
// ═════════════════════════════════════════════════════════════════
|
|
852
|
-
|
|
853
795
|
function applyMods(html, b) {
|
|
854
796
|
if(!html||(!b.extraClass&&!b.animate)) return html
|
|
855
797
|
const cls=[(b.extraClass||''),(b.animate?'fx-anim-'+b.animate:'')].filter(Boolean).join(' ')
|
|
856
|
-
|
|
798
|
+
|
|
857
799
|
return html.replace(/class="([^"]*)"/, (_,c)=>`class="${c} ${cls}"`)
|
|
858
800
|
}
|
|
859
801
|
|
|
860
802
|
function renderPage(page, allPages) {
|
|
861
803
|
const needsJS=page.queries.length>0||page.blocks.some(b=>['table','list','form','if','btn','select','faq'].includes(b.kind))
|
|
862
804
|
const body=page.blocks.map(b=>{try{return applyMods(renderBlock(b,page),b)}catch(e){console.error('[aiplang] Block render error:',b.kind,e.message);return ''}}).join('')
|
|
863
|
-
|
|
805
|
+
|
|
864
806
|
const tableBlocks = page.blocks.filter(b => b.kind === 'table' && b.binding && b.cols && b.cols.length)
|
|
865
807
|
const numericKeys = ['score','count','total','amount','price','value','qty','age','rank','num','int','float','rate','pct','percent']
|
|
866
808
|
const compiledDiffs = tableBlocks.map(b => {
|
|
867
809
|
const binding = b.binding.replace(/^@/, '')
|
|
810
|
+
|
|
811
|
+
const safeId = s => (s||'').replace(/[^a-zA-Z0-9_]/g, '_').slice(0,64) || 'col'
|
|
812
|
+
const safeBinding = safeId(binding)
|
|
868
813
|
const colDefs = b.cols.map((col, j) => ({
|
|
869
|
-
key: col.key,
|
|
814
|
+
key: safeId(col.key),
|
|
815
|
+
origKey: col.key,
|
|
870
816
|
idx: j,
|
|
871
817
|
numeric: numericKeys.some(kw => col.key.toLowerCase().includes(kw))
|
|
872
818
|
}))
|
|
873
819
|
const initParts = colDefs.map(d =>
|
|
874
|
-
d.numeric ? `c${d.idx}:new Float64Array(rows.map(r=>+(r.${d.
|
|
875
|
-
: `c${d.idx}:rows.map(r=>r
|
|
820
|
+
d.numeric ? `c${d.idx}:new Float64Array(rows.map(r=>+(r.${JSON.stringify(d.origKey)}===undefined?r['${d.origKey}']:r[${JSON.stringify(d.origKey)}])||0))`
|
|
821
|
+
: `c${d.idx}:rows.map(r=>r[${JSON.stringify(d.origKey)}]??'')`
|
|
876
822
|
).join(',')
|
|
877
|
-
const diffParts = colDefs.map(d =>
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
823
|
+
const diffParts = colDefs.map(d => {
|
|
824
|
+
const k = JSON.stringify(d.origKey)
|
|
825
|
+
return d.numeric
|
|
826
|
+
? `if(c${d.idx}[i]!==(r[${k}]||0)){c${d.idx}[i]=r[${k}]||0;p.push(i<<4|${d.idx})}`
|
|
827
|
+
: `if(c${d.idx}[i]!==r[${k}]){c${d.idx}[i]=r[${k}];p.push(i<<4|${d.idx})}`
|
|
828
|
+
}).join(';')
|
|
881
829
|
return [
|
|
882
|
-
`window.__aip_init_${
|
|
883
|
-
`window.__aip_diff_${
|
|
830
|
+
`window.__aip_init_${safeBinding}=function(rows){return{${initParts}}};`,
|
|
831
|
+
`window.__aip_diff_${safeBinding}=function(rows,cache){`,
|
|
884
832
|
`const n=rows.length,p=[],${colDefs.map(d=>`c${d.idx}=cache.c${d.idx}`).join(',')};`,
|
|
885
833
|
`for(let i=0;i<n;i++){const r=rows[i];${diffParts}}return p};`
|
|
886
834
|
].join('')
|
|
@@ -888,11 +836,11 @@ function renderPage(page, allPages) {
|
|
|
888
836
|
const compiledScript = compiledDiffs.length
|
|
889
837
|
? `<script>/* aiplang compiled-diffs */\n${compiledDiffs}\n</script>`
|
|
890
838
|
: ''
|
|
891
|
-
const config=needsJS?JSON.stringify({id:page.id,theme:page.theme,routes:allPages.map(p=>p.route),state:page.state,queries:page.queries,stores:page.stores||[],computed:page.computed||{},compiledTables:tableBlocks.map(b=>b.binding.replace(/^@/,''))}):''
|
|
839
|
+
const config=needsJS?JSON.stringify({id:page.id,theme:page.theme,routes:allPages.map(p=>p.route),state:page.state,queries:page.queries,stores:page.stores||[],computed:page.computed||{},compiledTables:tableBlocks.map(b=>(b.binding||'').replace(/^@/,'').replace(/[^a-zA-Z0-9_]/g,'_').slice(0,64))}):''
|
|
892
840
|
const hydrate=needsJS?`\n<script>window.__AIPLANG_PAGE__=${config};</script>\n<script src="./aiplang-hydrate.js" defer></script>`:''
|
|
893
841
|
const customVars=page.customTheme?genCustomThemeVars(page.customTheme):''
|
|
894
842
|
const themeVarCSS=page.themeVars?genThemeVarCSS(page.themeVars):''
|
|
895
|
-
|
|
843
|
+
|
|
896
844
|
const _navBlock = page.blocks.find(b=>b.kind==='nav')
|
|
897
845
|
const _brand = _navBlock?.brand || ''
|
|
898
846
|
const _title = _brand ? `${esc(_brand)} — ${esc(page.id.charAt(0).toUpperCase()+page.id.slice(1))}` : esc(page.id.charAt(0).toUpperCase()+page.id.slice(1))
|
|
@@ -948,7 +896,6 @@ function renderBlock(b, page) {
|
|
|
948
896
|
}
|
|
949
897
|
}
|
|
950
898
|
|
|
951
|
-
// ── Chart — lazy-loads Chart.js from CDN ────────────────────────
|
|
952
899
|
function rChart(b) {
|
|
953
900
|
const id = 'chart_' + Math.random().toString(36).slice(2,8)
|
|
954
901
|
const binding = b.binding || ''
|
|
@@ -959,7 +906,6 @@ function rChart(b) {
|
|
|
959
906
|
</div>\n`
|
|
960
907
|
}
|
|
961
908
|
|
|
962
|
-
// ── Kanban — drag-and-drop board ─────────────────────────────────
|
|
963
909
|
function rKanban(b) {
|
|
964
910
|
const cols = (b.cols||['Todo','In Progress','Done'])
|
|
965
911
|
const colsHtml = cols.map(col => `
|
|
@@ -971,7 +917,6 @@ function rKanban(b) {
|
|
|
971
917
|
return `<div class="fx-kanban" data-fx-kanban="${esc(b.binding||'')}" data-status-field="${esc(b.statusField||'status')}" data-update-path="${esc(b.updatePath||'')}"${style}>${colsHtml}</div>\n`
|
|
972
918
|
}
|
|
973
919
|
|
|
974
|
-
// ── Rich text editor ──────────────────────────────────────────────
|
|
975
920
|
function rEditor(b) {
|
|
976
921
|
const style = b.style ? ` style="${b.style.replace(/,/g,';')}"` : ''
|
|
977
922
|
return `<div class="fx-editor-wrap"${style}>
|
|
@@ -1057,7 +1002,7 @@ function rRow(b) {
|
|
|
1057
1002
|
pink:'#ec4899',cyan:'#06b6d4',lime:'#84cc16',amber:'#f59e0b'
|
|
1058
1003
|
}
|
|
1059
1004
|
const cards=(b.items||[]).map(item=>{
|
|
1060
|
-
|
|
1005
|
+
|
|
1061
1006
|
let colorStyle='', firstIdx=0
|
|
1062
1007
|
if(item[0]&&!item[0].isImg&&!item[0].isLink){
|
|
1063
1008
|
const colorKey=item[0].text?.toLowerCase()
|
|
@@ -1178,7 +1123,6 @@ function rGallery(b) {
|
|
|
1178
1123
|
return `<div class="fx-gallery">${imgs}</div>\n`
|
|
1179
1124
|
}
|
|
1180
1125
|
|
|
1181
|
-
// ── Theme helpers ─────────────────────────────────────────────────
|
|
1182
1126
|
function genCustomThemeVars(ct) {
|
|
1183
1127
|
return `body{background:${ct.bg};color:${ct.text}}.fx-nav{background:${ct.bg}cc;border-bottom:1px solid ${ct.text}18}.fx-cta,.fx-btn{background:${ct.accent};color:#fff}.fx-card{background:${ct.surface||ct.bg};border:1px solid ${ct.text}15}.fx-form{background:${ct.surface||ct.bg};border:1px solid ${ct.text}15}.fx-input{background:${ct.bg};border:1px solid ${ct.text}30;color:${ct.text}}.fx-stat-lbl,.fx-card-body,.fx-sub,.fx-sect-body,.fx-footer-text{color:${ct.text}88}.fx-th,.fx-nav-link{color:${ct.text}77}.fx-footer{border-top:1px solid ${ct.text}15}.fx-th{border-bottom:1px solid ${ct.text}15}`
|
|
1184
1128
|
}
|
|
@@ -1198,10 +1142,6 @@ function genThemeVarCSS(t) {
|
|
|
1198
1142
|
return r.join('')
|
|
1199
1143
|
}
|
|
1200
1144
|
|
|
1201
|
-
// ═════════════════════════════════════════════════════════════════
|
|
1202
|
-
// CSS
|
|
1203
|
-
// ═════════════════════════════════════════════════════════════════
|
|
1204
|
-
|
|
1205
1145
|
function css(theme) {
|
|
1206
1146
|
const base=`*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}html{scroll-behavior:smooth}body{font-family:-apple-system,'Segoe UI',system-ui,sans-serif;-webkit-font-smoothing:antialiased;min-height:100vh}a{text-decoration:none;color:inherit}input,button,select{font-family:inherit}img{max-width:100%;height:auto}.fx-nav{display:flex;align-items:center;justify-content:space-between;padding:1rem 2.5rem;position:sticky;top:0;z-index:50;backdrop-filter:blur(12px);flex-wrap:wrap;gap:.5rem}.fx-brand{font-size:1.25rem;font-weight:800;letter-spacing:-.03em}.fx-nav-links{display:flex;align-items:center;gap:1.75rem}.fx-nav-link{font-size:.875rem;font-weight:500;opacity:.65;transition:opacity .15s}.fx-nav-link:hover{opacity:1}.fx-hamburger{display:none;flex-direction:column;gap:5px;background:none;border:none;cursor:pointer;padding:.25rem}.fx-hamburger span{display:block;width:22px;height:2px;background:currentColor;transition:all .2s;border-radius:1px}.fx-hamburger.open span:nth-child(1){transform:rotate(45deg) translate(5px,5px)}.fx-hamburger.open span:nth-child(2){opacity:0}.fx-hamburger.open span:nth-child(3){transform:rotate(-45deg) translate(5px,-5px)}@media(max-width:640px){.fx-hamburger{display:flex}.fx-nav-links{display:none;width:100%;flex-direction:column;align-items:flex-start;gap:.75rem;padding:.75rem 0}.fx-nav-links.open{display:flex}}.fx-hero{display:flex;align-items:center;justify-content:center;min-height:92vh;padding:4rem 1.5rem}.fx-hero-split{display:grid;grid-template-columns:1fr 1fr;gap:3rem;align-items:center;padding:4rem 2.5rem;min-height:70vh}@media(max-width:768px){.fx-hero-split{grid-template-columns:1fr}}.fx-hero-img{width:100%;border-radius:1.25rem;object-fit:cover;max-height:500px}.fx-hero-inner{max-width:56rem;text-align:center;display:flex;flex-direction:column;align-items:center;gap:1.5rem}.fx-hero-split .fx-hero-inner{text-align:left;align-items:flex-start;max-width:none}.fx-title{font-size:clamp(2.5rem,8vw,5.5rem);font-weight:900;letter-spacing:-.04em;line-height:1}.fx-sub{font-size:clamp(1rem,2vw,1.25rem);line-height:1.75;max-width:40rem}.fx-cta{display:inline-flex;align-items:center;padding:.875rem 2.5rem;border-radius:.75rem;font-weight:700;font-size:1rem;letter-spacing:-.01em;transition:transform .15s;margin:.25rem}.fx-cta:hover{transform:translateY(-1px)}.fx-stats{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:3rem;padding:5rem 2.5rem;text-align:center}.fx-stat-val{font-size:clamp(2.5rem,5vw,4rem);font-weight:900;letter-spacing:-.04em;line-height:1}.fx-stat-lbl{font-size:.75rem;font-weight:600;text-transform:uppercase;letter-spacing:.1em;margin-top:.5rem}.fx-grid{display:grid;gap:1.25rem;padding:1rem 2.5rem 5rem}.fx-grid-2{grid-template-columns:repeat(auto-fit,minmax(280px,1fr))}.fx-grid-3{grid-template-columns:repeat(auto-fit,minmax(240px,1fr))}.fx-grid-4{grid-template-columns:repeat(auto-fit,minmax(200px,1fr))}.fx-card{border-radius:1rem;padding:1.75rem;transition:transform .2s,box-shadow .2s}.fx-card:hover{transform:translateY(-2px)}.fx-card-img{width:100%;border-radius:.75rem;object-fit:cover;height:180px;margin-bottom:1rem}.fx-icon{font-size:2rem;margin-bottom:1rem}.fx-card-title{font-size:1.0625rem;font-weight:700;letter-spacing:-.02em;margin-bottom:.5rem}.fx-card-body{font-size:.875rem;line-height:1.65}.fx-card-link{font-size:.8125rem;font-weight:600;display:inline-block;margin-top:1rem;opacity:.6;transition:opacity .15s}.fx-card-link:hover{opacity:1}.fx-sect{padding:5rem 2.5rem}.fx-sect-title{font-size:clamp(1.75rem,4vw,3rem);font-weight:800;letter-spacing:-.04em;margin-bottom:1.5rem;text-align:center}.fx-sect-body{font-size:1rem;line-height:1.75;text-align:center;max-width:48rem;margin:0 auto}.fx-form-wrap{padding:3rem 2.5rem;display:flex;justify-content:center}.fx-form{width:100%;max-width:28rem;border-radius:1.25rem;padding:2.5rem}.fx-field{margin-bottom:1.25rem}.fx-label{display:block;font-size:.8125rem;font-weight:600;margin-bottom:.5rem}.fx-input{width:100%;padding:.75rem 1rem;border-radius:.625rem;font-size:.9375rem;outline:none;transition:box-shadow .15s}.fx-input:focus{box-shadow:0 0 0 3px rgba(37,99,235,.35)}.fx-btn{width:100%;padding:.875rem 1.5rem;border:none;border-radius:.625rem;font-size:.9375rem;font-weight:700;cursor:pointer;margin-top:.5rem;transition:transform .15s,opacity .15s;letter-spacing:-.01em}.fx-btn:hover{transform:translateY(-1px)}.fx-btn:disabled{opacity:.5;cursor:not-allowed;transform:none}.fx-btn-wrap{padding:0 2.5rem 1.5rem}.fx-standalone-btn{width:auto;padding:.75rem 2rem;margin-top:0}.fx-form-msg{font-size:.8125rem;padding:.5rem 0;min-height:1.5rem;text-align:center}.fx-form-err{color:#f87171}.fx-form-ok{color:#4ade80}.fx-table-wrap{overflow-x:auto;padding:0 2.5rem 4rem}.fx-table{width:100%;border-collapse:collapse;font-size:.875rem}.fx-th{text-align:left;padding:.875rem 1.25rem;font-size:.75rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em}.fx-th-actions{opacity:.6}.fx-tr{transition:background .1s}.fx-td{padding:.875rem 1.25rem}.fx-td-empty{padding:2rem 1.25rem;text-align:center;opacity:.4}.fx-td-actions{white-space:nowrap;padding:.5rem 1rem!important}.fx-action-btn{border:none;cursor:pointer;font-size:.75rem;font-weight:600;padding:.3rem .75rem;border-radius:.375rem;margin-right:.375rem;font-family:inherit;transition:opacity .15s}.fx-action-btn:hover{opacity:.85}.fx-edit-btn{background:#1e40af;color:#93c5fd}.fx-delete-btn{background:#7f1d1d;color:#fca5a5}.fx-select-wrap{padding:.5rem 2.5rem}.fx-select-block{width:auto;min-width:200px;margin-top:0}.fx-pricing{display:grid;grid-template-columns:repeat(auto-fit,minmax(260px,1fr));gap:1.5rem;padding:2rem 2.5rem 5rem;align-items:start}.fx-pricing-card{border-radius:1.25rem;padding:2rem;position:relative;transition:transform .2s}.fx-pricing-featured{transform:scale(1.03)}.fx-pricing-badge{position:absolute;top:-12px;left:50%;transform:translateX(-50%);background:#2563eb;color:#fff;font-size:.7rem;font-weight:700;padding:.25rem .875rem;border-radius:999px;white-space:nowrap;letter-spacing:.05em}.fx-pricing-name{font-size:.875rem;font-weight:700;text-transform:uppercase;letter-spacing:.1em;margin-bottom:.5rem;opacity:.7}.fx-pricing-price{font-size:3rem;font-weight:900;letter-spacing:-.05em;line-height:1;margin-bottom:.75rem}.fx-pricing-desc{font-size:.875rem;line-height:1.65;margin-bottom:1.5rem;opacity:.7}.fx-pricing-cta{display:block;text-align:center;padding:.75rem;border-radius:.625rem;font-weight:700;font-size:.9rem;transition:opacity .15s}.fx-pricing-cta:hover{opacity:.85}.fx-faq{max-width:48rem;margin:0 auto}.fx-faq-item{border-radius:.75rem;margin-bottom:.625rem;cursor:pointer;overflow:hidden;transition:background .15s}.fx-faq-q{display:flex;justify-content:space-between;align-items:center;padding:1rem 1.25rem;font-size:.9375rem;font-weight:600}.fx-faq-arrow{transition:transform .2s;font-size:.75rem;opacity:.5}.fx-faq-item.open .fx-faq-arrow{transform:rotate(90deg)}.fx-faq-a{max-height:0;overflow:hidden;padding:0 1.25rem;font-size:.875rem;line-height:1.7;transition:max-height .3s,padding .3s}.fx-faq-item.open .fx-faq-a{max-height:300px;padding:.75rem 1.25rem 1.25rem}.fx-testi-wrap{padding:5rem 2.5rem;display:flex;justify-content:center}.fx-testi{max-width:42rem;text-align:center;display:flex;flex-direction:column;align-items:center;gap:1.25rem}.fx-testi-img{width:64px;height:64px;border-radius:50%;object-fit:cover}.fx-testi-avatar{width:64px;height:64px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:1.5rem;font-weight:700;background:#1e293b}.fx-testi-quote{font-size:1.25rem;line-height:1.7;font-style:italic;opacity:.9}.fx-testi-author{font-size:.875rem;font-weight:600;opacity:.5}.fx-gallery{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:.75rem;padding:1rem 2.5rem 4rem}.fx-gallery-item{border-radius:.75rem;overflow:hidden;aspect-ratio:4/3}.fx-gallery-item img{width:100%;height:100%;object-fit:cover;transition:transform .3s}.fx-gallery-item:hover img{transform:scale(1.04)}.fx-if-wrap{display:contents}.fx-footer{padding:3rem 2.5rem;text-align:center}.fx-footer-text{font-size:.8125rem}.fx-footer-link{font-size:.8125rem;margin:0 .75rem;opacity:.5;transition:opacity .15s}.fx-footer-link:hover{opacity:1}
|
|
1207
1147
|
.fx-hero-minimal{min-height:50vh!important}
|
package/package.json
CHANGED
|
@@ -6,23 +6,20 @@
|
|
|
6
6
|
const cfg = window.__AIPLANG_PAGE__
|
|
7
7
|
if (!cfg) return
|
|
8
8
|
|
|
9
|
-
// ── Global Store — cross-page state (like React Context / Zustand) ─
|
|
10
9
|
const _STORE_KEY = 'aiplang_store_v1'
|
|
11
10
|
const _globalStore = (() => {
|
|
12
11
|
try { return JSON.parse(sessionStorage.getItem(_STORE_KEY) || '{}') } catch { return {} }
|
|
13
12
|
})()
|
|
14
13
|
function syncStore(key, value) {
|
|
15
14
|
_globalStore[key] = value
|
|
16
|
-
try { sessionStorage.setItem(_STORE_KEY, JSON.stringify(_globalStore)) } catch {}
|
|
17
|
-
try { new BroadcastChannel(_STORE_KEY).postMessage({ key, value }) } catch {}
|
|
15
|
+
try { sessionStorage.setItem(_STORE_KEY, JSON.stringify(_globalStore)) } catch(_e) { if(typeof console !== 'undefined') console.debug('[aiplang]',_e?.message) }
|
|
16
|
+
try { new BroadcastChannel(_STORE_KEY).postMessage({ key, value }) } catch(_e) { if(typeof console !== 'undefined') console.debug('[aiplang]',_e?.message) }
|
|
18
17
|
}
|
|
19
18
|
|
|
20
|
-
// ── Page-level State ─────────────────────────────────────────────
|
|
21
19
|
const _state = {}
|
|
22
20
|
const _watchers = {}
|
|
23
21
|
const _storeKeys = new Set((cfg.stores || []).map(s => s.key))
|
|
24
22
|
|
|
25
|
-
// Bootstrap state: SSR data > global store > page state declarations
|
|
26
23
|
const _boot = { ...(window.__SSR_DATA__ || {}), ..._globalStore }
|
|
27
24
|
for (const [k, v] of Object.entries({ ...(cfg.state || {}), ..._boot })) {
|
|
28
25
|
try { _state[k] = typeof v === 'string' && (v.startsWith('[') || v.startsWith('{') || v === 'true' || v === 'false' || !isNaN(v)) ? JSON.parse(v) : v } catch { _state[k] = v }
|
|
@@ -31,14 +28,14 @@ for (const [k, v] of Object.entries({ ...(cfg.state || {}), ..._boot })) {
|
|
|
31
28
|
function get(key) { return _state[key] }
|
|
32
29
|
|
|
33
30
|
function set(key, value, _persist) {
|
|
34
|
-
|
|
31
|
+
|
|
35
32
|
const old = _state[key]
|
|
36
33
|
if (old === value) return
|
|
37
34
|
if (typeof value !== 'object' && old === value) return
|
|
38
35
|
if (typeof value === 'object' && value !== null && typeof old === 'object' && old !== null) {
|
|
39
|
-
|
|
36
|
+
|
|
40
37
|
if (Array.isArray(value) && Array.isArray(old) && value.length !== old.length) {
|
|
41
|
-
|
|
38
|
+
|
|
42
39
|
} else if (JSON.stringify(old) === JSON.stringify(value)) return
|
|
43
40
|
}
|
|
44
41
|
_state[key] = value
|
|
@@ -46,13 +43,12 @@ function set(key, value, _persist) {
|
|
|
46
43
|
notify(key)
|
|
47
44
|
}
|
|
48
45
|
|
|
49
|
-
// Cross-tab store sync (other pages update when store changes)
|
|
50
46
|
try {
|
|
51
47
|
const _bc = new BroadcastChannel(_STORE_KEY)
|
|
52
48
|
_bc.onmessage = ({ data: { key, value } }) => {
|
|
53
49
|
_state[key] = value; notify(key)
|
|
54
50
|
}
|
|
55
|
-
} catch {}
|
|
51
|
+
} catch(_e) { if(typeof console !== 'undefined') console.debug('[aiplang]',_e?.message) }
|
|
56
52
|
|
|
57
53
|
function watch(key, cb) {
|
|
58
54
|
if (!_watchers[key]) _watchers[key] = []
|
|
@@ -77,8 +73,7 @@ function notify(key) {
|
|
|
77
73
|
_pending.add(key)
|
|
78
74
|
if (!_batchScheduled) {
|
|
79
75
|
_batchScheduled = true
|
|
80
|
-
|
|
81
|
-
// Use rAF for user interaction (avoids mid-frame layout thrash)
|
|
76
|
+
|
|
82
77
|
Promise.resolve().then(() => {
|
|
83
78
|
if (_batchScheduled) requestAnimationFrame(flushBatch)
|
|
84
79
|
})
|
|
@@ -105,7 +100,6 @@ function resolvePath(tmpl, row) {
|
|
|
105
100
|
|
|
106
101
|
const _intervals = []
|
|
107
102
|
|
|
108
|
-
// Persistent session ID for delta tracking
|
|
109
103
|
const _sid = Math.random().toString(36).slice(2)
|
|
110
104
|
|
|
111
105
|
async function runQuery(q) {
|
|
@@ -119,13 +113,13 @@ async function runQuery(q) {
|
|
|
119
113
|
'x-session-id': _sid
|
|
120
114
|
}
|
|
121
115
|
}
|
|
122
|
-
|
|
116
|
+
|
|
123
117
|
if (isGet && q._deltaReady) opts.headers['x-aiplang-delta'] = '1'
|
|
124
118
|
if (q.body) opts.body = JSON.stringify(q.body)
|
|
125
119
|
try {
|
|
126
120
|
const res = await fetch(path, opts)
|
|
127
|
-
|
|
128
|
-
if (res.status === 304) { q._deltaReady = true; return null }
|
|
121
|
+
|
|
122
|
+
if (res.status === 304) { q._deltaReady = true; return get(q.target?.replace(/^@/,'')) || null }
|
|
129
123
|
if (!res.ok) throw new Error('HTTP ' + res.status)
|
|
130
124
|
const ct = res.headers.get('Content-Type') || ''
|
|
131
125
|
let data
|
|
@@ -135,17 +129,17 @@ async function runQuery(q) {
|
|
|
135
129
|
} else {
|
|
136
130
|
data = await res.json()
|
|
137
131
|
}
|
|
138
|
-
|
|
132
|
+
|
|
139
133
|
if (data && data.__delta) {
|
|
140
134
|
const key = q.target ? q.target.replace(/^@/, '') : null
|
|
141
135
|
if (key) {
|
|
142
136
|
const current = [...(get(key) || [])]
|
|
143
|
-
|
|
137
|
+
|
|
144
138
|
for (const row of (data.changed || [])) {
|
|
145
139
|
const idx = current.findIndex(r => r.id === row.id)
|
|
146
140
|
if (idx >= 0) current[idx] = row; else current.push(row)
|
|
147
141
|
}
|
|
148
|
-
|
|
142
|
+
|
|
149
143
|
const delSet = new Set(data.deleted || [])
|
|
150
144
|
const merged = current.filter(r => !delSet.has(r.id))
|
|
151
145
|
set(key, merged)
|
|
@@ -188,7 +182,7 @@ function applyAction(data, target, action) {
|
|
|
188
182
|
return true
|
|
189
183
|
})
|
|
190
184
|
set(fm[1], filtered)
|
|
191
|
-
} catch {}
|
|
185
|
+
} catch(_e) { if(typeof console !== 'undefined') console.debug('[aiplang]',_e?.message) }
|
|
192
186
|
return
|
|
193
187
|
}
|
|
194
188
|
const am = action.match(/^@([a-zA-Z_]+)\s*=\s*\$result$/)
|
|
@@ -322,15 +316,12 @@ function hydrateTables() {
|
|
|
322
316
|
}
|
|
323
317
|
}
|
|
324
318
|
|
|
325
|
-
|
|
326
|
-
// First render: full DocumentFragment build (fast)
|
|
327
|
-
// Re-renders: only update cells that actually changed
|
|
328
|
-
const _rowCache = new Map() // id → {score, status, ..., tr element}
|
|
319
|
+
const _rowCache = new Map()
|
|
329
320
|
const _colKeys = cols.map(c => c.key)
|
|
330
321
|
let _initialized = false
|
|
331
322
|
|
|
332
323
|
const renderRow = (row, idx) => {
|
|
333
|
-
|
|
324
|
+
|
|
334
325
|
}
|
|
335
326
|
|
|
336
327
|
const render = () => {
|
|
@@ -338,7 +329,6 @@ function hydrateTables() {
|
|
|
338
329
|
let rows = get(key)
|
|
339
330
|
if (!Array.isArray(rows)) rows = []
|
|
340
331
|
|
|
341
|
-
// Empty state
|
|
342
332
|
if (!rows.length) {
|
|
343
333
|
tbody.innerHTML = ''
|
|
344
334
|
_rowCache.clear()
|
|
@@ -469,9 +459,8 @@ function hydrateTables() {
|
|
|
469
459
|
tbody.appendChild(tr)
|
|
470
460
|
}
|
|
471
461
|
|
|
472
|
-
// ── Surgical update vs full initial build ────────────────────
|
|
473
462
|
if (!_initialized) {
|
|
474
|
-
|
|
463
|
+
|
|
475
464
|
_initialized = true
|
|
476
465
|
tbody.innerHTML = ''
|
|
477
466
|
const frag = document.createDocumentFragment()
|
|
@@ -487,7 +476,7 @@ function hydrateTables() {
|
|
|
487
476
|
td.textContent = row[col.key] != null ? row[col.key] : ''
|
|
488
477
|
tr.appendChild(td)
|
|
489
478
|
}
|
|
490
|
-
|
|
479
|
+
|
|
491
480
|
if (editPath || delPath) {
|
|
492
481
|
const actTd = document.createElement('td')
|
|
493
482
|
actTd.className = 'fx-td fx-td-actions'
|
|
@@ -525,23 +514,19 @@ function hydrateTables() {
|
|
|
525
514
|
frag.appendChild(tr)
|
|
526
515
|
}
|
|
527
516
|
tbody.appendChild(frag)
|
|
528
|
-
|
|
517
|
+
|
|
529
518
|
try {
|
|
530
519
|
const tc = _buildTypedCache(rows, _colKeys)
|
|
531
520
|
_rowCache._typed = tc
|
|
532
|
-
|
|
521
|
+
|
|
533
522
|
const compiledInit = window['__aip_init_' + stateKey]
|
|
534
523
|
if (compiledInit) {
|
|
535
524
|
_rowCache._compiled = compiledInit(rows)
|
|
536
525
|
_rowCache._compiled_n = rows.length
|
|
537
526
|
_rowCache._ids = rows.map(r => r.id != null ? r.id : null)
|
|
538
527
|
}
|
|
539
|
-
} catch {}
|
|
528
|
+
} catch(_e) { if(typeof console !== 'undefined') console.debug('[aiplang]',_e?.message) }
|
|
540
529
|
} else {
|
|
541
|
-
// UPDATE: off-main-thread diff + requestIdleCallback
|
|
542
|
-
// For 500+ rows: Worker computes diff on separate CPU core
|
|
543
|
-
// For <500 rows: sync diff (worker overhead not worth it)
|
|
544
|
-
// DOM patches always run on main thread but are minimal
|
|
545
530
|
|
|
546
531
|
const _makeRow = (row, idx) => {
|
|
547
532
|
const tr = document.createElement('tr')
|
|
@@ -579,10 +564,10 @@ function hydrateTables() {
|
|
|
579
564
|
}
|
|
580
565
|
|
|
581
566
|
if (rows.length >= 500) {
|
|
582
|
-
|
|
567
|
+
|
|
583
568
|
_diffAsync(rows, _colKeys, _rowCache).then(result => _schedIdle(() => _applyResult(result)))
|
|
584
569
|
} else {
|
|
585
|
-
|
|
570
|
+
|
|
586
571
|
_applyResult(_diffSync(rows, _colKeys, _rowCache))
|
|
587
572
|
}
|
|
588
573
|
}
|
|
@@ -696,7 +681,7 @@ function hydrateBindings() {
|
|
|
696
681
|
document.querySelectorAll('[data-fx-bind]').forEach(el => {
|
|
697
682
|
const expr = el.getAttribute('data-fx-bind')
|
|
698
683
|
const keys = (expr.match(/[@$][a-zA-Z_][a-zA-Z0-9_.]*/g) || []).map(m => m.slice(1).split('.')[0])
|
|
699
|
-
|
|
684
|
+
|
|
700
685
|
const simpleM = expr.match(/^[@$]([a-zA-Z_][a-zA-Z0-9_.]*)$/)
|
|
701
686
|
if (simpleM) {
|
|
702
687
|
const path = simpleM[1].split('.')
|
|
@@ -874,25 +859,14 @@ function injectActionCSS() {
|
|
|
874
859
|
document.head.appendChild(style)
|
|
875
860
|
}
|
|
876
861
|
|
|
877
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
878
|
-
// OFF-MAIN-THREAD ENGINE — better than React Fiber
|
|
879
|
-
// Fiber splits work across frames on the SAME thread.
|
|
880
|
-
// This moves diff computation to a SEPARATE CPU core via Web Worker.
|
|
881
|
-
// Main thread only handles tiny DOM patches — never competes with animations.
|
|
882
|
-
// ═══════════════════════════════════════════════════════════════════
|
|
883
|
-
|
|
884
862
|
const _workerSrc = `
|
|
885
863
|
'use strict'
|
|
886
|
-
// aiplang Diff Worker v2 — SharedArrayBuffer edition
|
|
887
|
-
// Uses SAB when available (HTTPS + COOP/COEP headers)
|
|
888
|
-
// Falls back to postMessage for unsupported environments
|
|
889
864
|
|
|
890
865
|
let _sab = null, _sabView = null
|
|
891
866
|
|
|
892
867
|
self.onmessage = function(e) {
|
|
893
868
|
const { type, rows, colKeys, cache, reqId, sab } = e.data
|
|
894
869
|
|
|
895
|
-
// Accept SharedArrayBuffer reference
|
|
896
870
|
if (sab) { _sab = sab; _sabView = new Int32Array(sab) }
|
|
897
871
|
|
|
898
872
|
if (type !== 'diff') return
|
|
@@ -916,17 +890,16 @@ self.onmessage = function(e) {
|
|
|
916
890
|
if (!seenIds.has(String(id)) && !seenIds.has(nid)) deletes.push(id)
|
|
917
891
|
}
|
|
918
892
|
|
|
919
|
-
// Write to SharedArrayBuffer if available (zero-copy)
|
|
920
893
|
if (_sabView && patches.length < 8000) {
|
|
921
894
|
Atomics.store(_sabView, 0, patches.length)
|
|
922
895
|
for (let i = 0; i < patches.length; i++) {
|
|
923
|
-
|
|
896
|
+
|
|
924
897
|
_sabView[1 + i*3] = typeof patches[i].id === 'number' ? patches[i].id : patches[i].id.length
|
|
925
898
|
_sabView[1 + i*3+1] = patches[i].col
|
|
926
|
-
_sabView[1 + i*3+2] = 0
|
|
899
|
+
_sabView[1 + i*3+2] = 0
|
|
927
900
|
}
|
|
928
|
-
Atomics.store(_sabView, 0, patches.length | 0x80000000)
|
|
929
|
-
|
|
901
|
+
Atomics.store(_sabView, 0, patches.length | 0x80000000)
|
|
902
|
+
|
|
930
903
|
self.postMessage({ type: 'patches', patches, inserts, deletes, reqId, usedSAB: true })
|
|
931
904
|
} else {
|
|
932
905
|
self.postMessage({ type: 'patches', patches, inserts, deletes, reqId, usedSAB: false })
|
|
@@ -937,7 +910,7 @@ self.onmessage = function(e) {
|
|
|
937
910
|
let _diffWorker = null
|
|
938
911
|
const _wCbs = new Map()
|
|
939
912
|
let _wReq = 0
|
|
940
|
-
let _wSAB = null
|
|
913
|
+
let _wSAB = null
|
|
941
914
|
|
|
942
915
|
function _getWorker() {
|
|
943
916
|
if (_diffWorker) return _diffWorker
|
|
@@ -948,12 +921,12 @@ function _getWorker() {
|
|
|
948
921
|
if (cb) { cb(e.data); _wCbs.delete(e.data.reqId) }
|
|
949
922
|
}
|
|
950
923
|
_diffWorker.onerror = () => { _diffWorker = null }
|
|
951
|
-
|
|
924
|
+
|
|
952
925
|
try {
|
|
953
|
-
const sab = new SharedArrayBuffer(8000 * 3 * 4 + 4)
|
|
926
|
+
const sab = new SharedArrayBuffer(8000 * 3 * 4 + 4)
|
|
954
927
|
_diffWorker.postMessage({ type: 'init_sab', sab }, [])
|
|
955
928
|
_wSAB = new Int32Array(sab)
|
|
956
|
-
} catch {
|
|
929
|
+
} catch(_e) { if(typeof console !== 'undefined') console.debug('[aiplang]',_e?.message) }
|
|
957
930
|
} catch { _diffWorker = null }
|
|
958
931
|
return _diffWorker
|
|
959
932
|
}
|
|
@@ -970,35 +943,30 @@ function _diffAsync(rows, colKeys, rowCache) {
|
|
|
970
943
|
})
|
|
971
944
|
}
|
|
972
945
|
|
|
973
|
-
// ── TypedArray positional cache — beats Vue Vapor at all sizes ───
|
|
974
|
-
// Float64Array for numeric fields, Uint8Array for status/enum
|
|
975
|
-
// No Map.get in hot loop — pure positional array scan
|
|
976
|
-
// Strategy: string-compare first for status (cheap), only encode int on change
|
|
977
|
-
|
|
978
946
|
function _buildTypedCache(rows, colKeys) {
|
|
979
947
|
const n = rows.length
|
|
980
948
|
const isNum = colKeys.map(k => {
|
|
981
949
|
const v = rows[0]?.[k]; return typeof v === 'number' || (v != null && !isNaN(Number(v)) && typeof v !== 'string')
|
|
982
950
|
})
|
|
983
951
|
const scores = new Float64Array(n)
|
|
984
|
-
const statuses = new Uint8Array(n)
|
|
985
|
-
const strCols = []
|
|
952
|
+
const statuses = new Uint8Array(n)
|
|
953
|
+
const strCols = []
|
|
986
954
|
colKeys.forEach((k,j) => {
|
|
987
955
|
if (!isNum[j]) strCols.push(j)
|
|
988
956
|
})
|
|
989
957
|
rows.forEach((r,i) => {
|
|
990
|
-
|
|
958
|
+
|
|
991
959
|
colKeys.forEach((k,j) => { if(isNum[j]) { const buf=j===0?scores:null; if(buf) buf[i]=Number(r[k])||0 } })
|
|
992
|
-
|
|
960
|
+
|
|
993
961
|
const numIdx = colKeys.findIndex((_,j)=>isNum[j] && j>1)
|
|
994
962
|
if(numIdx>=0) scores[i] = Number(rows[i][colKeys[numIdx]])||0
|
|
995
|
-
|
|
963
|
+
|
|
996
964
|
const enumIdx = colKeys.findIndex((_,j)=>!isNum[j] && j>1)
|
|
997
965
|
if(enumIdx>=0) statuses[i] = _STATUS_INT[rows[i][colKeys[enumIdx]]]??0
|
|
998
966
|
})
|
|
999
967
|
return {
|
|
1000
968
|
scores, statuses,
|
|
1001
|
-
prevVals: colKeys.map(k => rows.map(r => r[k])),
|
|
969
|
+
prevVals: colKeys.map(k => rows.map(r => r[k])),
|
|
1002
970
|
isNum, colKeys, n,
|
|
1003
971
|
ids: rows.map(r => r.id)
|
|
1004
972
|
}
|
|
@@ -1008,14 +976,13 @@ function _diffSync(rows, colKeys, rowCache) {
|
|
|
1008
976
|
const patches = [], inserts = [], deletes = [], seen = new Set()
|
|
1009
977
|
const nCols = colKeys.length
|
|
1010
978
|
|
|
1011
|
-
|
|
1012
|
-
// Generated at build time — specific field access, no generic loops
|
|
1013
|
-
const compiledKey = stateKey // table binding name
|
|
979
|
+
const compiledKey = stateKey
|
|
1014
980
|
const compiledInit = window['__aip_init_' + compiledKey]
|
|
1015
981
|
const compiledDiff = window['__aip_diff_' + compiledKey]
|
|
1016
982
|
|
|
1017
|
-
if (compiledDiff && rowCache._compiled && rows.length === rowCache._compiled_n
|
|
1018
|
-
|
|
983
|
+
if (compiledDiff && rowCache._compiled && rows.length === rowCache._compiled_n
|
|
984
|
+
&& rows.length > 0 && rowCache._ids?.length === rows.length) {
|
|
985
|
+
|
|
1019
986
|
const raw = compiledDiff(rows, rowCache._compiled)
|
|
1020
987
|
for (const pack of raw) {
|
|
1021
988
|
const i = pack >> 4, col = pack & 0xf
|
|
@@ -1029,7 +996,6 @@ function _diffSync(rows, colKeys, rowCache) {
|
|
|
1029
996
|
return { patches, inserts, deletes }
|
|
1030
997
|
}
|
|
1031
998
|
|
|
1032
|
-
// TIER 2 — TYPED CACHE fast path
|
|
1033
999
|
const tc = rowCache._typed
|
|
1034
1000
|
if (tc && rows.length === tc.n) {
|
|
1035
1001
|
for (let i = 0; i < rows.length; i++) {
|
|
@@ -1049,7 +1015,6 @@ function _diffSync(rows, colKeys, rowCache) {
|
|
|
1049
1015
|
return { patches, inserts, deletes }
|
|
1050
1016
|
}
|
|
1051
1017
|
|
|
1052
|
-
// STANDARD PATH: Map-based diff (first render or variable-length data)
|
|
1053
1018
|
for (let i = 0; i < rows.length; i++) {
|
|
1054
1019
|
const r = rows[i], id = r.id != null ? r.id : i
|
|
1055
1020
|
seen.add(id)
|
|
@@ -1073,8 +1038,6 @@ function _diffSync(rows, colKeys, rowCache) {
|
|
|
1073
1038
|
return { patches, inserts, deletes }
|
|
1074
1039
|
}
|
|
1075
1040
|
|
|
1076
|
-
// requestIdleCallback scheduler — runs low-priority work when browser is idle
|
|
1077
|
-
// Polling updates (30s intervals) don't need to be urgent — let animations breathe
|
|
1078
1041
|
const _idleQ = []
|
|
1079
1042
|
let _idleSched = false
|
|
1080
1043
|
const _ric = window.requestIdleCallback
|
|
@@ -1092,13 +1055,11 @@ function _schedIdle(fn) {
|
|
|
1092
1055
|
function _flushIdle(dl) {
|
|
1093
1056
|
_idleSched = false
|
|
1094
1057
|
while (_idleQ.length && dl.timeRemaining() > 1) {
|
|
1095
|
-
try { _idleQ.shift()() } catch {}
|
|
1058
|
+
try { _idleQ.shift()() } catch(_e) { if(typeof console !== 'undefined') console.debug('[aiplang]',_e?.message) }
|
|
1096
1059
|
}
|
|
1097
1060
|
if (_idleQ.length) { _idleSched = true; _ric(_flushIdle, { timeout: 5000 }) }
|
|
1098
1061
|
}
|
|
1099
1062
|
|
|
1100
|
-
// Incremental renderer — processes rows in chunks between animation frames
|
|
1101
|
-
// Zero dropped frames on 100k+ row datasets
|
|
1102
1063
|
async function _renderIncremental(items, renderFn, chunkSize = 200) {
|
|
1103
1064
|
for (let i = 0; i < items.length; i += chunkSize) {
|
|
1104
1065
|
const chunk = items.slice(i, i + chunkSize)
|
|
@@ -1109,9 +1070,6 @@ async function _renderIncremental(items, renderFn, chunkSize = 200) {
|
|
|
1109
1070
|
}
|
|
1110
1071
|
}
|
|
1111
1072
|
|
|
1112
|
-
|
|
1113
|
-
// ── Global reusable TypedArray buffers — zero allocation in hot path ──
|
|
1114
|
-
// Pre-allocated at startup, reused across every table render cycle
|
|
1115
1073
|
const _MAX_ROWS = 100000
|
|
1116
1074
|
const _scoreBuf = new Float64Array(_MAX_ROWS)
|
|
1117
1075
|
const _statusBuf = new Uint8Array(_MAX_ROWS)
|
|
@@ -1120,9 +1078,6 @@ const _STATUS_INT = {active:0,inactive:1,pending:2,blocked:3,
|
|
|
1120
1078
|
pending:2,done:3,todo:0,doing:1,done:2,
|
|
1121
1079
|
new:0,open:1,closed:2,resolved:3}
|
|
1122
1080
|
|
|
1123
|
-
// ── Minimal MessagePack decoder (~1.8KB) ─────────────────────────
|
|
1124
|
-
// Handles all types produced by aiplang server's msgpack encoder
|
|
1125
|
-
// No external dependencies — AI-maintained, human-unreadable by design
|
|
1126
1081
|
const _mp = (() => {
|
|
1127
1082
|
const td = new TextDecoder()
|
|
1128
1083
|
function decode(buf) {
|
|
@@ -1131,11 +1086,11 @@ const _mp = (() => {
|
|
|
1131
1086
|
}
|
|
1132
1087
|
function _read(b, s) {
|
|
1133
1088
|
const t = b[s.p++]
|
|
1134
|
-
if (t <= 0x7f) return t
|
|
1135
|
-
if (t >= 0xe0) return t - 256
|
|
1136
|
-
if ((t & 0xe0) === 0xa0) return _str(b, s, t & 0x1f)
|
|
1137
|
-
if ((t & 0xf0) === 0x90) return _arr(b, s, t & 0xf)
|
|
1138
|
-
if ((t & 0xf0) === 0x80) return _map(b, s, t & 0xf)
|
|
1089
|
+
if (t <= 0x7f) return t
|
|
1090
|
+
if (t >= 0xe0) return t - 256
|
|
1091
|
+
if ((t & 0xe0) === 0xa0) return _str(b, s, t & 0x1f)
|
|
1092
|
+
if ((t & 0xf0) === 0x90) return _arr(b, s, t & 0xf)
|
|
1093
|
+
if ((t & 0xf0) === 0x80) return _map(b, s, t & 0xf)
|
|
1139
1094
|
switch (t) {
|
|
1140
1095
|
case 0xc0: return null
|
|
1141
1096
|
case 0xc2: return false
|
|
@@ -1221,10 +1176,21 @@ function hydrateTableErrors() {
|
|
|
1221
1176
|
if (val === '__error__') {
|
|
1222
1177
|
if (tbody) {
|
|
1223
1178
|
const cols = JSON.parse(tbl.getAttribute('data-fx-cols') || '[]')
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1179
|
+
|
|
1180
|
+
tbody.innerHTML = ''
|
|
1181
|
+
const _fe = (() => {
|
|
1182
|
+
const tr=document.createElement('tr'), td=document.createElement('td')
|
|
1183
|
+
td.colSpan=cols.length+2; td.className='fx-td-empty'; td.style.color='#f87171'
|
|
1184
|
+
td.textContent=fallback
|
|
1185
|
+
if(retryPath){
|
|
1186
|
+
const btn=document.createElement('button')
|
|
1187
|
+
btn.textContent='↻ Retry'; btn.style.cssText='margin-left:.75rem;padding:.3rem .75rem;background:rgba(248,113,113,.1);border:1px solid rgba(248,113,113,.3);color:#f87171;border-radius:.375rem;cursor:pointer;font-size:.75rem'
|
|
1188
|
+
btn.onclick=()=>window.__aiplang_retry&&window.__aiplang_retry(binding,retryPath)
|
|
1189
|
+
td.appendChild(btn)
|
|
1190
|
+
}
|
|
1191
|
+
tr.appendChild(td); return tr
|
|
1192
|
+
})()
|
|
1193
|
+
tbody.appendChild(_fe)
|
|
1228
1194
|
}
|
|
1229
1195
|
}
|
|
1230
1196
|
})
|
package/server/server.js
CHANGED
|
@@ -250,17 +250,24 @@ const _cache = new Map()
|
|
|
250
250
|
// Stores a hash of each row per client session to send only changes
|
|
251
251
|
// Key: sessionId:binding → Map<rowId, hash>
|
|
252
252
|
const _deltaCache = new Map()
|
|
253
|
-
const _DELTA_TTL = 60000
|
|
253
|
+
const _DELTA_TTL = 60000 // 1 min: expire client state
|
|
254
|
+
const _DELTA_MAX = 2000 // max concurrent sessions (prevent memory DoS)
|
|
254
255
|
|
|
255
256
|
function _rowHash(row) {
|
|
256
|
-
//
|
|
257
|
+
// FNV-1a 32-bit — fast, low collision for row change detection
|
|
258
|
+
// Only hashes enumerable own properties to avoid prototype pollution
|
|
257
259
|
let h = 2166136261
|
|
258
|
-
const
|
|
259
|
-
for (
|
|
260
|
-
|
|
261
|
-
|
|
260
|
+
const keys = Object.keys(row).sort()
|
|
261
|
+
for (const k of keys) {
|
|
262
|
+
const v = row[k]
|
|
263
|
+
if (v === null || v === undefined) continue
|
|
264
|
+
const s = k + ':' + String(v)
|
|
265
|
+
for (let i = 0; i < s.length; i++) {
|
|
266
|
+
h = (h ^ s.charCodeAt(i)) >>> 0
|
|
267
|
+
h = Math.imul(h, 16777619) >>> 0
|
|
268
|
+
}
|
|
262
269
|
}
|
|
263
|
-
return h
|
|
270
|
+
return h >>> 0
|
|
264
271
|
}
|
|
265
272
|
|
|
266
273
|
function _getDelta(sessionId, binding, rows) {
|
|
@@ -268,6 +275,12 @@ function _getDelta(sessionId, binding, rows) {
|
|
|
268
275
|
const prev = _deltaCache.get(key)
|
|
269
276
|
// First request — send full dataset, store hashes
|
|
270
277
|
if (!prev) {
|
|
278
|
+
// Evict oldest entry when at capacity (LRU approximation)
|
|
279
|
+
if (_deltaCache.size >= _DELTA_MAX) {
|
|
280
|
+
let oldest = null, oldestTs = Infinity
|
|
281
|
+
for (const [k, v] of _deltaCache) if (v.ts < oldestTs) { oldest = k; oldestTs = v.ts }
|
|
282
|
+
if (oldest) _deltaCache.delete(oldest)
|
|
283
|
+
}
|
|
271
284
|
const hashes = new Map(rows.map(r => [r.id, _rowHash(r)]))
|
|
272
285
|
_deltaCache.set(key, { hashes, ts: Date.now() })
|
|
273
286
|
return { full: true, rows }
|
|
@@ -1032,8 +1045,9 @@ async function execOp(line, ctx, server) {
|
|
|
1032
1045
|
if (Array.isArray(result) && ctx.req?.headers?.['x-aiplang-delta'] === '1' && result.length > 0) {
|
|
1033
1046
|
try {
|
|
1034
1047
|
const _req = ctx.req
|
|
1035
|
-
const sid = (_req.headers['x-session-id'] || _req.socket?.remoteAddress || '
|
|
1036
|
-
|
|
1048
|
+
const sid = (_req.headers['x-session-id'] || _req.socket?.remoteAddress || 'anon').slice(0,64)
|
|
1049
|
+
// Key = session + full path (not just binding) to handle multi-table pages correctly
|
|
1050
|
+
const binding = (_req.url?.split('?')[0] || '/data').slice(0,128)
|
|
1037
1051
|
const delta = _getDelta(sid, binding, result)
|
|
1038
1052
|
if (delta.full) {
|
|
1039
1053
|
ctx.res.json(status, result); return '__DONE__'
|
|
@@ -1928,7 +1942,7 @@ async function startServer(aipFile, port = 3000) {
|
|
|
1928
1942
|
|
|
1929
1943
|
// Health
|
|
1930
1944
|
srv.addRoute('GET', '/health', (req, res) => res.json(200, {
|
|
1931
|
-
status:'ok', version:'2.10.
|
|
1945
|
+
status:'ok', version:'2.10.9',
|
|
1932
1946
|
models: app.models.map(m=>m.name),
|
|
1933
1947
|
routes: app.apis.length, pages: app.pages.length,
|
|
1934
1948
|
admin: app.admin?.prefix || null,
|