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 +130 -60
- package/package.json +1 -1
- package/runtime/aiplang-hydrate.js +993 -1301
- package/runtime/aiplang-runtime.js +47 -1
- package/server/server.js +84 -18
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.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
|
|
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.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
|
|
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
|
}
|