aiplang 2.11.2 → 2.11.3

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.11.2'
8
+ const VERSION = '2.11.3'
9
9
  const RUNTIME_DIR = path.join(__dirname, '..', 'runtime')
10
10
  const cmd = process.argv[2]
11
11
  const args = process.argv.slice(3)
@@ -37,6 +37,7 @@ if (!cmd||cmd==='--help'||cmd==='-h') {
37
37
  npx aiplang serve [dir] dev server + hot reload
38
38
  npx aiplang build [dir/file] compile → static HTML
39
39
  npx aiplang validate <app.aip> validate syntax with AI-friendly errors
40
+ npx aiplang types <app.aip> generate TypeScript types (.d.ts)
40
41
  npx aiplang context [app.aip] dump minimal AI context (<500 tokens)
41
42
  npx aiplang new <page> new page template
42
43
  npx aiplang --version
@@ -451,6 +452,242 @@ const _KNOWN_TYPES = new Set([
451
452
  'bigint','smallint','tinyint','currency','money','price'
452
453
  ])
453
454
 
455
+
456
+
457
+ function _parseForTypes(src) {
458
+ const app = { models:[], apis:[], auth:null }
459
+ const _ta = {
460
+ integer:'int',boolean:'bool',double:'float',number:'float',string:'text',
461
+ varchar:'text',datetime:'timestamp',email:'email',url:'url',uri:'url',
462
+ phone:'phone',currency:'float',money:'float',price:'float',
463
+ json:'json',jsonb:'json',bigint:'int',smallint:'int',tinyint:'int'
464
+ }
465
+ const norm = t => _ta[(t||'').toLowerCase()] || t || 'text'
466
+
467
+ function parseField(line) {
468
+ const p = line.split(/\s*:\s*/)
469
+ const f = { name:(p[0]||'').trim(), type:norm(p[1]), modifiers:[], enumVals:[], constraints:{}, default:null }
470
+ if (f.type === 'enum') {
471
+ const ev = p.slice(2).find(x => x && !x.startsWith('default=') && !['required','unique','hashed','pk','auto','index'].includes(x.trim()))
472
+ if (ev) f.enumVals = ev.includes('|') ? ev.split('|').map(v=>v.trim()) : ev.split(',').map(v=>v.trim())
473
+ }
474
+ for (let j=2; j<p.length; j++) {
475
+ const x = (p[j]||'').trim()
476
+ if (!x) continue
477
+ if (x.startsWith('default=')) f.default = x.slice(8)
478
+ else if (x.startsWith('min=')) f.constraints.min = Number(x.slice(4))
479
+ else if (x.startsWith('max=')) f.constraints.max = Number(x.slice(4))
480
+ else if (x.startsWith('minLen=')) f.constraints.minLen = Number(x.slice(7))
481
+ else if (x.startsWith('maxLen=')) f.constraints.maxLen = Number(x.slice(7))
482
+ else if (x.startsWith('enum:')) f.enumVals = x.slice(5).includes('|') ? x.slice(5).split('|').map(v=>v.trim()) : x.slice(5).split(',').map(v=>v.trim())
483
+ else if (x && !x.includes('=')) f.modifiers.push(x)
484
+ }
485
+ return f
486
+ }
487
+
488
+ // Juntar todas as linhas e tokenizar por estado
489
+ const lines = src.split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#'))
490
+ let inModel=false, inAPI=false, curModel=null, depth=0
491
+
492
+ for (let i=0; i<lines.length; i++) {
493
+ const line = lines[i]
494
+
495
+ // Auth
496
+ if (line.startsWith('~auth ')) { app.auth = { type: line.includes('jwt') ? 'jwt' : 'basic' }; continue }
497
+
498
+ // Model
499
+ if (line.startsWith('model ') || (line.startsWith('model') && line.includes('{'))) {
500
+ if (inModel && curModel) app.models.push(curModel)
501
+ curModel = { name: line.replace(/^model\s+/, '').replace(/\s*\{.*$/, '').trim(), fields:[] }
502
+ inModel=true; inAPI=false; depth=0
503
+ if (line.includes('{')) depth++
504
+ continue
505
+ }
506
+
507
+ // API
508
+ if (line.startsWith('api ')) {
509
+ if (inModel && curModel) { app.models.push(curModel); curModel=null; inModel=false }
510
+ const m = line.match(/^api\s+(\w+)\s+(\S+)/)
511
+ if (m) {
512
+ const guards = []
513
+ // Procura guard tanto na linha atual quanto nas próximas linhas do bloco
514
+ const blockLines = []
515
+ let j=i; let bd=0
516
+ while(j<lines.length) {
517
+ const bl = lines[j]
518
+ for(const ch of bl) { if(ch==='{') bd++; else if(ch==='}') bd-- }
519
+ blockLines.push(bl)
520
+ if(bd===0 && j>i) break
521
+ j++
522
+ }
523
+ const blockStr = blockLines.join(' ')
524
+ if (blockStr.includes('~guard auth')) guards.push('auth')
525
+ if (blockStr.includes('~guard admin')) guards.push('admin')
526
+ if (blockStr.includes('=> auth')) guards.push('auth')
527
+ if (blockStr.includes('=> admin')) guards.push('admin')
528
+ app.apis.push({ method:m[1].toUpperCase(), path:m[2], guards })
529
+ }
530
+ inAPI=true; inModel=false
531
+ // Contar profundidade das chaves para saber quando o bloco fecha
532
+ depth=0
533
+ for(const ch of line) { if(ch==='{') depth++; else if(ch==='}') depth-- }
534
+ if(depth<=0) { inAPI=false; depth=0 }
535
+ continue
536
+ }
537
+
538
+ // Dentro de api block: rastrear chaves
539
+ if (inAPI) {
540
+ for(const ch of line) { if(ch==='{') depth++; else if(ch==='}') depth-- }
541
+ if(depth<=0) { inAPI=false; depth=0 }
542
+ continue
543
+ }
544
+
545
+ // Fechar model
546
+ if (line === '}' && inModel) {
547
+ if (curModel) { app.models.push(curModel); curModel=null }
548
+ inModel=false; continue
549
+ }
550
+
551
+ // Campos do model
552
+ if (inModel && curModel && line && line !== '{' && !line.startsWith('~') && line.includes(':')) {
553
+ const f = parseField(line)
554
+ if (f.name) curModel.fields.push(f)
555
+ }
556
+ }
557
+ if (inModel && curModel) app.models.push(curModel)
558
+ return app
559
+ }
560
+
561
+ // ── TypeScript type generator ────────────────────────────────────
562
+ // Maps aiplang types to TypeScript equivalents
563
+ const _AIP_TO_TS = {
564
+ text:'string', string:'string', email:'string', url:'string',
565
+ phone:'string', slug:'string', color:'string', file:'string', image:'string',
566
+ int:'number', integer:'number', float:'number', double:'number',
567
+ number:'number', currency:'number', money:'number', price:'number',
568
+ bool:'boolean', boolean:'boolean',
569
+ uuid:'string', date:'string', timestamp:'string', datetime:'string',
570
+ json:'Record<string,unknown>', jsonb:'Record<string,unknown>',
571
+ }
572
+
573
+ function generateTypes(app, srcFile) {
574
+ const lines = [
575
+ '// ─────────────────────────────────────────────────────────────',
576
+ `// aiplang generated types — ${srcFile || 'app.aip'}`,
577
+ `// Generated: ${new Date().toISOString()}`,
578
+ '// DO NOT EDIT — regenerate with: npx aiplang types <app.aip>',
579
+ '// ─────────────────────────────────────────────────────────────',
580
+ '',
581
+ ]
582
+
583
+ // Model interfaces
584
+ for (const model of (app.models || [])) {
585
+ const name = model.name
586
+ lines.push(`// Model: ${name}`)
587
+
588
+ // Enum types first
589
+ for (const f of (model.fields || [])) {
590
+ if (f.type === 'enum' && f.enumVals && f.enumVals.length) {
591
+ lines.push(`export type ${name}${_cap(f.name)} = ${f.enumVals.map(v => `'${v}'`).join(' | ')}`)
592
+ }
593
+ }
594
+
595
+ // Main interface
596
+ lines.push(`export interface ${name} {`)
597
+ for (const f of (model.fields || [])) {
598
+ const req = f.modifiers && f.modifiers.includes('required')
599
+ const pk = f.modifiers && f.modifiers.includes('pk')
600
+ const auto = f.modifiers && f.modifiers.includes('auto')
601
+ const opt = !req || pk || auto
602
+ let tsType
603
+ if (f.type === 'enum' && f.enumVals && f.enumVals.length) {
604
+ tsType = `${name}${_cap(f.name)}`
605
+ } else {
606
+ tsType = _AIP_TO_TS[f.type] || 'string'
607
+ }
608
+ // Constraint comments
609
+ const constraints = []
610
+ if (f.constraints) {
611
+ if (f.constraints.min != null) constraints.push(`min:${f.constraints.min}`)
612
+ if (f.constraints.max != null) constraints.push(`max:${f.constraints.max}`)
613
+ if (f.constraints.minLen != null) constraints.push(`minLen:${f.constraints.minLen}`)
614
+ if (f.constraints.maxLen != null) constraints.push(`maxLen:${f.constraints.maxLen}`)
615
+ if (f.constraints.format) constraints.push(`format:${f.constraints.format}`)
616
+ }
617
+ const comment = constraints.length ? ` // ${constraints.join(', ')}` : ''
618
+ const mods = []
619
+ if (f.modifiers) {
620
+ if (f.modifiers.includes('unique')) mods.push('@unique')
621
+ if (f.modifiers.includes('hashed')) mods.push('@hashed')
622
+ if (pk) mods.push('@pk')
623
+ if (auto) mods.push('@auto')
624
+ }
625
+ const modStr = mods.length ? ` /** ${mods.join(' ')} */` : ''
626
+ lines.push(` ${f.name}${opt ? '?' : ''}: ${tsType}${modStr}${comment}`)
627
+ }
628
+ lines.push(`}`)
629
+ lines.push(``)
630
+
631
+ // Input type (for POST/PUT — excludes pk/auto, optional for patches)
632
+ const inputFields = (model.fields || []).filter(f =>
633
+ !(f.modifiers && f.modifiers.includes('pk')) &&
634
+ !(f.modifiers && f.modifiers.includes('auto')) &&
635
+ f.name !== 'created_at' && f.name !== 'updated_at'
636
+ )
637
+ if (inputFields.length) {
638
+ lines.push(`export interface ${name}Input {`)
639
+ for (const f of inputFields) {
640
+ const req = f.modifiers && f.modifiers.includes('required')
641
+ let tsType
642
+ if (f.type === 'enum' && f.enumVals && f.enumVals.length) {
643
+ tsType = `${name}${_cap(f.name)}`
644
+ } else {
645
+ tsType = _AIP_TO_TS[f.type] || 'string'
646
+ }
647
+ lines.push(` ${f.name}${req ? '' : '?'}: ${tsType}`)
648
+ }
649
+ lines.push(`}`)
650
+ lines.push(``)
651
+ }
652
+ }
653
+
654
+ // API route types
655
+ if ((app.apis || []).length) {
656
+ lines.push(`// ── API Route types ──────────────────────────────────────────`)
657
+ lines.push(``)
658
+ lines.push(`export interface AiplangRoutes {`)
659
+ for (const api of (app.apis || [])) {
660
+ const method = api.method.toUpperCase()
661
+ const path = api.path.replace(/\//g, '_').replace(/[^a-zA-Z0-9_]/g,'').replace(/^_/,'')
662
+ const guards = (api.guards || []).join(', ')
663
+ const guardComment = guards ? ` /** guards: ${guards} */` : ''
664
+ lines.push(` '${method} ${api.path}': {${guardComment}}`)
665
+ }
666
+ lines.push(`}`)
667
+ lines.push(``)
668
+ }
669
+
670
+ // Convenience type for auth user
671
+ const hasAuth = app.auth && app.auth.type
672
+ if (hasAuth) {
673
+ const userModel = (app.models || []).find(m => m.name === 'User' || m.name === 'user')
674
+ if (userModel) {
675
+ lines.push(`// ── Auth types ───────────────────────────────────────────────`)
676
+ lines.push(`export type AuthUser = Pick<User, 'id' | 'email'${(userModel.fields||[]).some(f=>f.name==='role')?" | 'role'":''}> & { type: 'access' | 'refresh', iat: number, exp: number }`)
677
+ lines.push(`declare global { namespace Express { interface Request { user?: AuthUser } } }`)
678
+ lines.push(``)
679
+ }
680
+ }
681
+
682
+ lines.push(`// ── aiplang version ──────────────────────────────────────────`)
683
+ lines.push(`export const AIPLANG_VERSION = '2.11.3'`)
684
+ lines.push(``)
685
+ return lines.join('\n')
686
+ }
687
+
688
+ function _cap(s) { return s ? s[0].toUpperCase() + s.slice(1) : s }
689
+
690
+
454
691
  function validateAipSrc(source) {
455
692
  const errors = []
456
693
  const lines = source.split('\n')
@@ -490,6 +727,34 @@ function validateAipSrc(source) {
490
727
  return errors
491
728
  }
492
729
 
730
+ if (cmd==='types'||cmd==='type'||cmd==='dts') {
731
+ const file = args[0]
732
+ if (!file) { console.error('\n Usage: aiplang types <app.aip> [--out types.d.ts]\n'); process.exit(1) }
733
+ if (!require('fs').existsSync(file)) { console.error(`\n ✗ Arquivo não encontrado: ${file}\n`); process.exit(1) }
734
+ const src = require('fs').readFileSync(file,'utf8')
735
+ const errs = validateAipSrc(src)
736
+ if (errs.some(e => e.severity === 'error')) {
737
+ console.error('\n ✗ Corrija os erros antes de gerar tipos:\n')
738
+ errs.filter(e=>e.severity==='error').forEach(e=>console.error(` Line ${e.line}: ${e.message}`))
739
+ process.exit(1)
740
+ }
741
+ const serverPath = require('path').join(__dirname,'../server/server.js')
742
+ // Parse models and routes from .aip source for type generation
743
+ // Lightweight inline parser — no DB, no server init required
744
+ const app = _parseForTypes(src)
745
+ const _outIdx = args.indexOf('--out')
746
+ const outFile = (_outIdx >= 0 && args[_outIdx+1]) ? args[_outIdx+1] : file.replace(/\.aip$/,'') + '.d.ts'
747
+ const dts = generateTypes(app, require('path').basename(file))
748
+ require('fs').writeFileSync(outFile, dts)
749
+ console.log(`\n ✅ Tipos gerados: ${outFile}`)
750
+ console.log(` ${(app.models||[]).length} models · ${(app.apis||[]).length} routes\n`)
751
+ // Also show preview
752
+ const preview = dts.split('\n').slice(0,30).join('\n')
753
+ console.log(preview)
754
+ if (dts.split('\n').length > 30) console.log(' ...')
755
+ process.exit(0)
756
+ }
757
+
493
758
  if (cmd==='validate'||cmd==='check'||cmd==='lint') {
494
759
  const file = args[0]
495
760
  if (!file) { console.error('\n Usage: aiplang validate <app.aip>\n'); process.exit(1) }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiplang",
3
- "version": "2.11.2",
3
+ "version": "2.11.3",
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
@@ -2246,7 +2246,7 @@ async function startServer(aipFile, port = 3000) {
2246
2246
  })
2247
2247
 
2248
2248
  srv.addRoute('GET', '/health', (req, res) => res.json(200, {
2249
- status:'ok', version:'2.11.2',
2249
+ status:'ok', version:'2.11.3',
2250
2250
  models: app.models.map(m=>m.name),
2251
2251
  routes: app.apis.length, pages: app.pages.length,
2252
2252
  admin: app.admin?.prefix || null,