aiplang 2.9.4 → 2.10.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/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.9.4'
8
+ const VERSION = '2.10.0'
9
9
  const RUNTIME_DIR = path.join(__dirname, '..', 'runtime')
10
10
  const cmd = process.argv[2]
11
11
  const args = process.argv.slice(3)
@@ -648,9 +648,15 @@ function parseBlock(line) {
648
648
  const binding=line.slice(start,idx).trim().replace(/^@/,'@')
649
649
  const content=line.slice(idx+1,line.lastIndexOf('}')).trim()
650
650
  const em=content.match(/edit\s+(PUT|PATCH)\s+(\S+)/), dm=content.match(/delete\s+(?:DELETE\s+)?(\S+)/)
651
- const clean=content.replace(/edit\s+(PUT|PATCH)\s+\S+/g,'').replace(/delete\s+(?:DELETE\s+)?\S+/g,'')
651
+ const fallbackM=content.match(/fallback\s*:\s*([^|]+)/)
652
+ const retryM=content.match(/retry\s*:\s*(\S+)/)
653
+ const clean=content
654
+ .replace(/edit\s+(PUT|PATCH)\s+\S+/g,'')
655
+ .replace(/delete\s+(?:DELETE\s+)?\S+/g,'')
656
+ .replace(/fallback\s*:[^|]+/g,'')
657
+ .replace(/retry\s*:\s*\S+/g,'')
652
658
  const cols=parseCols(clean)
653
- 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',extraClass,animate,variant,style,bg}
659
+ 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}
654
660
  }
655
661
 
656
662
  // ── form ────────────────────────────────────────────────────
@@ -658,12 +664,17 @@ function parseBlock(line) {
658
664
  const bi=line.indexOf('{');if(bi===-1) return null
659
665
  let head=line.slice(line.startsWith('form{')?4:5,bi).trim()
660
666
  const content=line.slice(bi+1,line.lastIndexOf('}')).trim()
661
- let action=''; const ai=head.indexOf('=>')
662
- if(ai!==-1){action=head.slice(ai+2).trim();head=head.slice(0,ai).trim()}
667
+ let action='', optimistic=false; const ai=head.indexOf('=>')
668
+ if(ai!==-1){
669
+ action=head.slice(ai+2).trim()
670
+ // Optimistic: => @list.optimistic($result)
671
+ if(action.includes('.optimistic(')){optimistic=true;action=action.replace('.optimistic','')}
672
+ head=head.slice(0,ai).trim()
673
+ }
663
674
  const parts=head.trim().split(/\s+/)
664
675
  const method=parts[0]&&['GET','POST','PUT','PATCH','DELETE'].includes(parts[0].toUpperCase())?parts[0].toUpperCase():'POST'
665
676
  const bpath=parts[method===parts[0].toUpperCase()?1:0]||''
666
- return{kind:'form',method,bpath,action,fields:parseFields(content)||[],extraClass,animate,variant,style,bg}
677
+ return{kind:'form',method,bpath,action,optimistic,fields:parseFields(content)||[],extraClass,animate,variant,style,bg}
667
678
  }
668
679
 
669
680
  // ── pricing ─────────────────────────────────────────────────
@@ -858,11 +869,33 @@ function renderBlock(b, page) {
858
869
  case 'testimonial': return rTestimonial(b)
859
870
  case 'gallery': return rGallery(b)
860
871
  case 'raw': return (b.html||'')+'\n'
872
+ case 'html': return `<div class="fx-html">${b.content||''}</div>\n`
873
+ case 'spacer': return `<div class="fx-spacer" style="height:${esc(b.height||'2rem')}"></div>\n`
874
+ case 'divider': return b.label?`<div class="fx-divider"><span class="fx-divider-label">${esc(b.label)}</span></div>\n`:`<hr class="fx-hr">\n`
875
+ case 'badge': return `<div class="fx-badge-row"><span class="fx-badge-tag">${esc(b.content||'')}</span></div>\n`
876
+ case 'card': return rCardBlock(b)
877
+ case 'cols': return rColsBlock(b)
878
+ case 'each': return `<div class="fx-each fx-each-${b.variant||'list'}" data-fx-each="${esc(b.binding||'')}" data-fx-tpl="${esc(b.tpl||'')}"${b.style?` style="${b.style.replace(/,/g,';')}"`:''}>\n<div class="fx-each-empty fx-td-empty">Loading...</div></div>\n`
861
879
  case 'if': return `<div class="fx-if-wrap" data-fx-if="${esc(b.cond)}" style="display:none"></div>\n`
862
880
  default: return ''
863
881
  }
864
882
  }
865
883
 
884
+ function rCardBlock(b) {
885
+ const img=b.img?`<img src="${esc(b.img)}" class="fx-card-img" alt="${esc(b.title||'')}" loading="lazy">`:'';
886
+ const badge=b.badge?`<span class="fx-card-badge">${esc(b.badge)}</span>`:'';
887
+ const title=b.title?`<h3 class="fx-card-title">${esc(b.title)}</h3>`:'';
888
+ const sub=b.subtitle?`<p class="fx-card-body">${esc(b.subtitle)}</p>`:'';
889
+ const link=b.link?`<a href="${esc(b.link.split(':')[0])}" class="fx-card-link">${esc(b.link.split(':')[1]||'View')} →</a>`:'';
890
+ const bg=b.bg?` style="background:${b.bg}"`:b.style?` style="${b.style.replace(/,/g,';')}"`:''
891
+ return`<div class="fx-card"${bg}>${img}${badge}${title}${sub}${link}</div>\n`
892
+ }
893
+ function rColsBlock(b) {
894
+ const cols=(b.items||[]).map(col=>`<div class="fx-col">${col}</div>`).join('')
895
+ const style=b.style?` style="${b.style.replace(/,/g,';')}"`:''
896
+ return`<div class="fx-cols fx-cols-${b.n||2}"${style}>${cols}</div>\n`
897
+ }
898
+
866
899
  function rNav(b) {
867
900
  if(!b.items?.[0]) return ''
868
901
  const it=b.items[0]
@@ -973,7 +1006,9 @@ function rTable(b) {
973
1006
  const da=b.deletePath?` data-fx-delete="${esc(b.deletePath)}"`:''
974
1007
  const at=(b.editPath||b.deletePath)?'<th class="fx-th fx-th-actions">Actions</th>':''
975
1008
  const span=cols.length+((b.editPath||b.deletePath)?1:0)
976
- return `<div class="fx-table-wrap"><table class="fx-table" data-fx-table="${esc(b.binding)}" data-fx-cols='${keys}' data-fx-col-map='${cm}'${ea}${da}><thead><tr>${ths}${at}</tr></thead><tbody class="fx-tbody"><tr><td colspan="${span}" class="fx-td-empty">${esc(b.empty)}</td></tr></tbody></table></div>\n`
1009
+ const fallbackAttr=b.fallback?` data-fx-fallback="${esc(b.fallback)}"`:''
1010
+ const retryAttr=b.retry?` data-fx-retry="${esc(b.retry)}"`:''
1011
+ return `<div class="fx-table-wrap"><table class="fx-table" data-fx-table="${esc(b.binding)}" data-fx-cols='${keys}' data-fx-col-map='${cm}'${ea}${da}${fallbackAttr}${retryAttr}><thead><tr>${ths}${at}</tr></thead><tbody class="fx-tbody"><tr><td colspan="${span}" class="fx-td-empty">${esc(b.empty)}</td></tr></tbody></table></div>\n`
977
1012
  }
978
1013
 
979
1014
  function rForm(b) {
@@ -993,7 +1028,8 @@ function rForm(b) {
993
1028
  if(v==='minimal') {
994
1029
  return `<div class="fx-form-minimal"><form data-fx-form="${esc(b.bpath)}" data-fx-method="${esc(b.method)}" data-fx-action="${esc(b.action)}">${fields}<div class="fx-form-msg"></div><button type="submit" class="fx-btn">${esc(label)}</button></form></div>\n`
995
1030
  }
996
- return `<div class="fx-form-wrap"><form class="fx-form"${bgStyle} data-fx-form="${esc(b.bpath)}" data-fx-method="${esc(b.method)}" data-fx-action="${esc(b.action)}">${fields}<div class="fx-form-msg"></div><button type="submit" class="fx-btn">${esc(label)}</button></form></div>\n`
1031
+ const optAttr=b.optimistic?' data-fx-optimistic="true"':''
1032
+ return `<div class="fx-form-wrap"><form class="fx-form"${bgStyle}${optAttr} data-fx-form="${esc(b.bpath)}" data-fx-method="${esc(b.method)}" data-fx-action="${esc(b.action)}">${fields}<div class="fx-form-msg"></div><button type="submit" class="fx-btn">${esc(label)}</button></form></div>\n`
997
1033
  }
998
1034
 
999
1035
  function rBtn(b) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiplang",
3
- "version": "2.9.4",
3
+ "version": "2.10.0",
4
4
  "description": "AI-first web language. One .aip file = complete app. Frontend + backend + database + auth.",
5
5
  "keywords": [
6
6
  "aiplang",
package/server/server.js CHANGED
@@ -1797,7 +1797,7 @@ async function startServer(aipFile, port = 3000) {
1797
1797
 
1798
1798
  // Health
1799
1799
  srv.addRoute('GET', '/health', (req, res) => res.json(200, {
1800
- status:'ok', version:'2.9.4',
1800
+ status:'ok', version:'2.10.0',
1801
1801
  models: app.models.map(m=>m.name),
1802
1802
  routes: app.apis.length, pages: app.pages.length,
1803
1803
  admin: app.admin?.prefix || null,