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 +130 -60
- package/package.json +1 -1
- package/server/server.js +37 -5
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
|
-
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
npx aiplang
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
948
|
-
|
|
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
|
-
|
|
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 '))
|
|
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':
|
|
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
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
|
|
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
|
-
|
|
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.
|
|
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,
|