aiplang 2.11.11 → 2.11.13

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.13'
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.13'`)
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.13",
4
4
  "description": "AI-first web language. One .aip file = complete app. Frontend + backend + database + auth.",
5
5
  "keywords": [
6
6
  "aiplang",