aiplang 2.11.11 → 2.11.12

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,51 @@ const fs = require('fs')
5
5
  const path = require('path')
6
6
  const http = require('http')
7
7
 
8
- const VERSION = '2.11.11'
8
+
9
+ // ── ANSI colors — sem dependências ───────────────────────────────
10
+ const CLR = {
11
+ reset: '\x1b[0m',
12
+ bold: '\x1b[1m',
13
+ dim: '\x1b[2m',
14
+ green: '\x1b[32m',
15
+ blue: '\x1b[34m',
16
+ cyan: '\x1b[36m',
17
+ yellow:'\x1b[33m',
18
+ red: '\x1b[31m',
19
+ white: '\x1b[37m',
20
+ gray: '\x1b[90m',
21
+ }
22
+ const c_ = (color, text) => process.stdout.isTTY ? CLR[color]+text+CLR.reset : text
23
+ const bold = t => c_('bold', t)
24
+ const dim = t => c_('dim', t)
25
+ const green = t => c_('green', t)
26
+ const blue = t => c_('blue', t)
27
+ const cyan = t => c_('cyan', t)
28
+ const yellow= t => c_('yellow', t)
29
+ const red = t => c_('red', t)
30
+ const gray = t => c_('gray', t)
31
+
32
+ // Spinner simples
33
+ class Spinner {
34
+ constructor(text) {
35
+ this.frames = ['⠋','⠙','⠹','⠸','⠼','⠴','⠦','⠧','⠇','⠏']
36
+ this.i = 0; this.text = text; this.timer = null
37
+ }
38
+ start() {
39
+ if (!process.stdout.isTTY) { process.stdout.write(this.text+'...\n'); return this }
40
+ this.timer = setInterval(() => {
41
+ process.stdout.write('\r ' + cyan(this.frames[this.i++ % this.frames.length]) + ' ' + this.text + ' ')
42
+ }, 80)
43
+ return this
44
+ }
45
+ stop(ok=true, msg='') {
46
+ if (this.timer) { clearInterval(this.timer); this.timer = null }
47
+ if (process.stdout.isTTY) process.stdout.write('\r')
48
+ console.log(' ' + (ok ? green('✓') : red('✗')) + ' ' + (msg||this.text) + ' ')
49
+ }
50
+ }
51
+
52
+ const VERSION = '2.11.12'
9
53
  const RUNTIME_DIR = path.join(__dirname, '..', 'runtime')
10
54
  const cmd = process.argv[2]
11
55
  const args = process.argv.slice(3)
@@ -26,55 +70,27 @@ const hSize = n => n<1024?`${n}B`:`${(n/1024).toFixed(1)}KB`
26
70
 
27
71
  if (!cmd||cmd==='--help'||cmd==='-h') {
28
72
  console.log(`
29
- aiplang v${VERSION}
30
- AI-first web language — full apps in ~20 lines.
31
-
32
- Usage:
33
- npx aiplang init [name] create project (default template)
34
- npx aiplang init [name] --template <t> use template: saas|landing|crud|dashboard|portfolio|blog
35
- npx aiplang init [name] --template ./my.aip use a local .aip file as template
36
- npx aiplang init [name] --template my-custom use a saved custom template
37
- npx aiplang serve [dir] dev server + hot reload
38
- npx aiplang build [dir/file] compile static HTML
39
- npx aiplang validate <app.aip> validate syntax with AI-friendly errors
40
- npx aiplang types <app.aip> generate TypeScript types (.d.ts)
41
- npx aiplang context [app.aip] dump minimal AI context (<500 tokens)
42
- npx aiplang new <page> new page template
43
- npx aiplang --version
44
-
45
- Full-stack:
46
- npx aiplang start app.aip start full-stack server (API + DB + frontend)
47
- PORT=8080 aiplang start app.aip custom port
48
-
49
- Templates:
50
- npx aiplang template list list all templates (built-in + custom)
51
- npx aiplang template save <n> save current project as template
52
- npx aiplang template save <n> --from <f> save a specific .aip file as template
53
- npx aiplang template edit <n> open template in editor
54
- npx aiplang template show <n> print template source
55
- npx aiplang template export <n> export template to .aip file
56
- npx aiplang template remove <n> delete a custom template
57
-
58
- Custom template variables:
59
- {{name}} project name
60
- {{year}} current year
61
-
62
- Customization:
63
- # Bancos de dados suportados:
64
- # ~db sqlite ./app.db (padrão — sem configuração)
65
- # ~db pg $DATABASE_URL (PostgreSQL)
66
- # ~db mysql $MYSQL_URL (MySQL / MariaDB)
67
- # ~db mongodb $MONGODB_URL (MongoDB)
68
- # ~db redis $REDIS_URL (Redis — cache/session)
69
- ~theme accent=#7c3aed radius=1.5rem font=Syne bg=#000 text=#fff
70
- hero{...} animate:fade-up
71
- row3{...} class:my-class animate:stagger
72
- raw{<div>any HTML here</div>}
73
-
74
- GitHub: https://github.com/isacamartin/aiplang
75
- npm: https://npmjs.com/package/aiplang
76
- Docs: https://isacamartin.github.io/aiplang
77
- `)
73
+ ${bold(cyan('aiplang'))} ${dim('v'+VERSION)} ${gray('AI-First Web Language')}
74
+
75
+ ${bold('Usage:')}
76
+ ${cyan('npx aiplang')} ${yellow('<command>')} ${gray('[options]')}
77
+
78
+ ${bold('Commands:')}
79
+ ${green('start')} ${gray('<file.aip> [port]')} Start dev server
80
+ ${green('build')} ${gray('<file.aip> --out <dir>')} Build static HTML
81
+ ${green('validate')}${gray(' <file.aip>')} Lint syntax
82
+ ${green('types')} ${gray('<file.aip>')} Generate TypeScript .d.ts
83
+ ${green('context')} ${gray('<file.aip>')} AI context summary
84
+ ${green('init')} ${gray('[project-name]')} Create new project
85
+
86
+ ${bold('Examples:')}
87
+ ${dim('npx aiplang start app.aip')}
88
+ ${dim('npx aiplang build app.aip --out ./dist')}
89
+ ${dim('npx aiplang init my-saas')}
90
+
91
+ ${bold('Docs:')} ${blue('https://github.com/isacamartin/aiplang')}
92
+ ${bold('npm:')} ${blue('npmjs.com/package/aiplang')}
93
+ `)
78
94
  process.exit(0)
79
95
  }
80
96
  if (cmd==='--version'||cmd==='-v') { console.log(`aiplang v${VERSION}`); process.exit(0) }
@@ -686,7 +702,7 @@ function generateTypes(app, srcFile) {
686
702
  }
687
703
 
688
704
  lines.push(`// ── aiplang version ──────────────────────────────────────────`)
689
- lines.push(`export const AIPLANG_VERSION = '2.11.11'`)
705
+ lines.push(`export const AIPLANG_VERSION = '2.11.12'`)
690
706
  lines.push(``)
691
707
  return lines.join('\n')
692
708
  }
@@ -697,7 +713,7 @@ function _cap(s) { return s ? s[0].toUpperCase() + s.slice(1) : s }
697
713
  function validateAipSrc(source) {
698
714
  const errors = []
699
715
  const lines = source.split('\n')
700
- const knownDirs = new Set(['db','auth','env','mail','s3','stripe','plan','admin','realtime','use','plugin','import','store','ssr','interval','mount','theme','guard','validate','unique','hash','check','cache','rateLimit','broadcast','soft-delete','belongs'])
716
+ const knownDirs = new Set(['db','auth','env','mail','s3','stripe','plan','admin','realtime','use','plugin','import','store','ssr','interval','mount','theme','guard','validate','unique','hash','check','cache','rateLimit','broadcast','soft-delete','belongs','var','component','end'])
701
717
  for (let i=0; i<lines.length; i++) {
702
718
  const line = lines[i].trim()
703
719
  if (!line || line.startsWith('#')) continue
@@ -856,7 +872,7 @@ if (cmd==='build') {
856
872
  const pages=parsePages(src)
857
873
  if(!pages.length){console.error('\n ✗ No pages found.\n');process.exit(1)}
858
874
  fs.mkdirSync(outDir,{recursive:true})
859
- console.log(`\n aiplang build v${VERSION} — ${files.length} file(s)\n`)
875
+ console.log(`\n ${bold(cyan('aiplang'))} build ${dim('v'+VERSION)} — ${files.length} file(s)\n`)
860
876
  let total=0
861
877
  for(const page of pages){
862
878
  const html=renderPage(page,pages)
@@ -936,18 +952,49 @@ console.error(`\n ✗ Unknown command: ${cmd}\n Run aiplang --help\n`)
936
952
  process.exit(1)
937
953
 
938
954
  function parsePages(src) {
939
- // Extrair variáveis globais ~var name = value
955
+ // ── Extrair variáveis globais ~var ──────────────────────────────
940
956
  const gVars = {}
941
957
  src.split('\n').forEach(line => {
942
958
  const vm = line.trim().match(/^~var\s+(\w+)\s*=\s*(.+)$/)
943
959
  if (vm) gVars[vm[1].trim()] = vm[2].trim().replace(/^["']|["']$/g,'')
944
960
  })
945
- // Expandir $var em todo o src
961
+
962
+ // ── Extrair componentes ~component name(params) ... ~end ────────
963
+ const gComponents = {}
964
+ const compRx = /^~component\s+(\w+)\s*\(([^)]*)\)\s*$([\s\S]*?)^~end\s*$/mg
965
+ let m; while((m=compRx.exec(src))!==null) {
966
+ const name = m[1].trim()
967
+ const params = m[2].split(',').map(p=>p.trim()).filter(Boolean)
968
+ const body = m[3]
969
+ gComponents[name] = { params, body }
970
+ }
971
+
972
+ // ── Expandir ~use name(arg1, arg2) → corpo do componente ───────
973
+ function expandComponents(s) {
974
+ if (!Object.keys(gComponents).length) return s
975
+ return s.replace(/^~use\s+(\w+)\s*\(([^)]*)\)\s*$/mg, (_, name, argsStr) => {
976
+ const comp = gComponents[name]
977
+ if (!comp) return `# ~use: component "${name}" not found`
978
+ const args = argsStr.split(',').map(a=>a.trim())
979
+ let body = comp.body
980
+ comp.params.forEach((p,i) => {
981
+ body = body.replace(new RegExp('\\$'+p, 'g'), args[i] !== undefined ? args[i] : '')
982
+ })
983
+ return body
984
+ })
985
+ }
986
+
987
+ // ── Expandir $var ───────────────────────────────────────────────
946
988
  function expand(s) {
947
- if (!Object.keys(gVars).length) return s
948
- return s.replace(/\$(\w+)/g, (m,k) => gVars[k]!==undefined ? gVars[k] : m)
989
+ let r = expandComponents(s)
990
+ if (Object.keys(gVars).length)
991
+ r = r.replace(/\$(\w+)/g, (m,k) => gVars[k]!==undefined ? gVars[k] : m)
992
+ return r
949
993
  }
950
- return src.split(/\n---\n/).map(s=>parsePage(expand(s.trim()))).filter(Boolean)
994
+
995
+ // Remover blocos ~component...~end antes de parsear
996
+ const cleanSrc = src.replace(/^~component[\s\S]*?^~end\s*$/mg, '')
997
+ return cleanSrc.split(/\n---\n/).map(s=>parsePage(expand(s.trim()))).filter(Boolean)
951
998
  }
952
999
 
953
1000
  function parsePage(src) {
@@ -955,7 +1002,10 @@ function parsePage(src) {
955
1002
  if(!lines.length) return null
956
1003
  const p={id:'page',theme:'dark',route:'/',customTheme:null,themeVars:null,state:{},queries:[],blocks:[]}
957
1004
  for(const line of lines) {
958
- if(line.startsWith('~var ')) continue // já processado globalmente
1005
+ if(line.startsWith('~var ')) continue // já processado globalmente
1006
+ if(line.startsWith('~component ')) continue // componente já expandido
1007
+ if(line.startsWith('~use ')) continue // já expandido
1008
+ if(line.startsWith('~end')) continue // fim de componente
959
1009
  if(line.startsWith('%')) {
960
1010
  const pts=line.slice(1).trim().split(/\s+/)
961
1011
  p.id=pts[0]||'page'; p.route=pts[2]||'/'
@@ -1333,7 +1383,27 @@ function renderBlock(b, page) {
1333
1383
  case 'chart': return rChart(b)
1334
1384
  case 'kanban': return rKanban(b)
1335
1385
  case 'editor': return rEditor(b)
1336
- 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`
1386
+ case 'each': {
1387
+ // SSR: se a página tem dados do binding (via ~ssr), renderizar server-side
1388
+ const _eachBinding = b.binding||''
1389
+ const _eachData = page && page.ssrData && page.ssrData[_eachBinding.replace('@','')]
1390
+ if (_eachData && Array.isArray(_eachData) && _eachData.length > 0) {
1391
+ // SSR: expandir template para cada item
1392
+ const _tpl = b.tpl||''
1393
+ const _items = _eachData.map((item, idx) => {
1394
+ let row = _tpl
1395
+ // Substituir {item.campo} e {item} pelos valores reais
1396
+ row = row.replace(/\{item\.(\w+)\}/g, (_, k) => esc(String(item[k]??'')))
1397
+ row = row.replace(/\{item\}/g, esc(String(item??'')))
1398
+ // Renderizar o bloco expandido
1399
+ try { return applyMods(renderBlock(parseBlock(row)||{kind:'html',content:row}, page), {}) } catch { return '' }
1400
+ }).join('')
1401
+ const style = b.style ? ` style="${b.style.replace(/,/g,';')}"` : ''
1402
+ return `<div class="fx-each fx-each-${b.variant||'list'} fx-each-ssr" data-fx-each="${esc(_eachBinding)}"${style}>${_items}</div>\n`
1403
+ }
1404
+ // Client-side fallback (comportamento original)
1405
+ return `<div class="fx-each fx-each-${b.variant||'list'}" data-fx-each="${esc(_eachBinding)}" 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`
1406
+ }
1337
1407
  case 'if': return `<div class="fx-if-wrap" data-fx-if="${esc(b.cond)}" style="display:none"></div>\n`
1338
1408
  default: return ''
1339
1409
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiplang",
3
- "version": "2.11.11",
3
+ "version": "2.11.12",
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
@@ -1877,7 +1877,8 @@ async function parseBody(req) {
1877
1877
  // ═══════════════════════════════════════════════════════════════════
1878
1878
  function renderHTML(page, allPages) {
1879
1879
  const needsJS=page.queries.length>0||page.blocks.some(b=>['table','form','if','btn','select','faq'].includes(b.kind))
1880
- const body=page.blocks.map(b=>renderBlock(b)).join('')
1880
+ const ssrData = page.ssrData || {}
1881
+ const body=page.blocks.map(b=>renderBlock(b, ssrData)).join('')
1881
1882
  const config=needsJS?JSON.stringify({id:page.id,theme:page.theme,state:page.state,routes:allPages.map(p=>p.route),queries:page.queries}):''
1882
1883
  const hydrate=needsJS?`<script>window.__AIPLANG_PAGE__=${config};</script><script src="/aiplang-hydrate.js" defer></script>`:''
1883
1884
  const themeCSS=page.themeVars?genThemeCSS(page.themeVars):''
@@ -1885,8 +1886,9 @@ function renderHTML(page, allPages) {
1885
1886
  return `<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${page.id}</title><style>${baseCSS(page.theme)}${customCSS}${themeCSS}</style></head><body>${body}${hydrate}</body></html>`
1886
1887
  }
1887
1888
 
1888
- function renderBlock(b) {
1889
+ function renderBlock(b, ssrData) {
1889
1890
  const line=b.rawLine
1891
+ ssrData = ssrData || {}
1890
1892
  let animate='',extraClass=''
1891
1893
  const am=line.match(/\banimate:(\S+)/); if(am)animate='fx-anim-'+am[1]
1892
1894
  const cm=line.match(/\bclass:(\S+)/); if(cm)extraClass=cm[1]
@@ -1905,6 +1907,23 @@ function renderBlock(b) {
1905
1907
  case 'faq': return rFaq(line)
1906
1908
  case 'testimonial':return rTestimonial(line)
1907
1909
  case 'gallery':return rGallery(line)
1910
+ case 'each': {
1911
+ const _bind = line.match(/data-fx-each="([^"]+)"/)
1912
+ const _tpl = line.match(/data-fx-tpl="([^"]+)"/)
1913
+ const _bkey = (_bind && _bind[1]) ? _bind[1].replace('@','') : ''
1914
+ const _data = ssrData[_bkey]
1915
+ if (_data && Array.isArray(_data) && _data.length > 0) {
1916
+ const _t = _tpl ? _tpl[1] : ''
1917
+ const _items = _data.map(item => {
1918
+ let row = _t
1919
+ row = row.replace(/\{item\.(\w+)\}/g, (_,k) => esc(String(item[k]??'')))
1920
+ row = row.replace(/\{item\}/g, esc(String(item??'')))
1921
+ return row
1922
+ }).join('')
1923
+ return `<div class="fx-each fx-each-ssr">${_items}</div>\n`
1924
+ }
1925
+ return line + '\n'
1926
+ }
1908
1927
  case 'raw': return extractBody(line)+'\n'
1909
1928
  case 'if': return `<div class="fx-if-wrap" data-fx-if="${esc(extractCond(line))}" style="display:none"></div>\n`
1910
1929
  default: return ''
@@ -2407,8 +2426,21 @@ async function startServer(aipFile, port = 3000) {
2407
2426
 
2408
2427
  // Frontend
2409
2428
  for (const page of app.pages) {
2410
- srv.addRoute('GET', page.route, (req, res) => {
2411
- const html = renderHTML(page, app.pages)
2429
+ srv.addRoute('GET', page.route, async (req, res) => {
2430
+ // Executar SSR queries para each{} blocks
2431
+ const ssrData = {}
2432
+ for (const q of (page.queries||[])) {
2433
+ if (q.trigger === 'ssr' || q.trigger === 'mount') {
2434
+ try {
2435
+ const Model = q.model && srv.models[q.model]
2436
+ if (Model) {
2437
+ const data = await Model.all()
2438
+ if (q.binding) ssrData[q.binding.replace('@','')] = data
2439
+ }
2440
+ } catch(e) { /* ignore SSR query errors */ }
2441
+ }
2442
+ }
2443
+ const html = renderHTML({...page, ssrData}, app.pages)
2412
2444
  res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); res.end(html)
2413
2445
  })
2414
2446
  console.log(`[aiplang] Page: ${page.route}`)
@@ -2462,7 +2494,7 @@ async function startServer(aipFile, port = 3000) {
2462
2494
  })
2463
2495
 
2464
2496
  srv.addRoute('GET', '/health', (req, res) => res.json(200, {
2465
- status:'ok', version:'2.11.11',
2497
+ status:'ok', version:'2.11.12',
2466
2498
  models: app.models.map(m=>m.name),
2467
2499
  routes: app.apis.length, pages: app.pages.length,
2468
2500
  admin: app.admin?.prefix || null,