aiplang 2.4.0 → 2.5.0
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/README.md +6 -6
- package/bin/aiplang.js +3 -3
- package/package.json +1 -1
- package/runtime/aiplang-runtime.js +5 -5
- package/server/server.js +37 -2
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@ cd my-app
|
|
|
8
8
|
npx aiplang serve
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
Ask Claude to generate a page → paste into `pages/home.
|
|
11
|
+
Ask Claude to generate a page → paste into `pages/home.aiplang` → see it live.
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
|
@@ -16,9 +16,9 @@ Ask Claude to generate a page → paste into `pages/home.flux` → see it live.
|
|
|
16
16
|
|
|
17
17
|
**aiplang** is a web language designed to be generated by AI (Claude), not written by humans.
|
|
18
18
|
|
|
19
|
-
A single `.
|
|
19
|
+
A single `.aiplang` file describes a complete app: frontend, backend, database, auth, email, jobs.
|
|
20
20
|
|
|
21
|
-
```
|
|
21
|
+
```aiplang
|
|
22
22
|
~db sqlite ./app.db
|
|
23
23
|
~auth jwt $JWT_SECRET expire=7d
|
|
24
24
|
~admin /admin
|
|
@@ -83,7 +83,7 @@ npx aiplang init --template landing # landing page template
|
|
|
83
83
|
npx aiplang init --template crud # CRUD app template
|
|
84
84
|
npx aiplang serve # dev server + hot reload → localhost:3000
|
|
85
85
|
npx aiplang build pages/ --out dist/ # compile → static HTML
|
|
86
|
-
npx aiplang start app.
|
|
86
|
+
npx aiplang start app.aiplang # full-stack server (Node.js)
|
|
87
87
|
npx aiplang new dashboard # create new page template
|
|
88
88
|
```
|
|
89
89
|
|
|
@@ -111,13 +111,13 @@ All blocks accept: animate:fade-up class:my-class | raw{<html>} | foot{text>/pat
|
|
|
111
111
|
|
|
112
112
|
```
|
|
113
113
|
aiplang/
|
|
114
|
-
├── packages/
|
|
114
|
+
├── packages/aiplang-pkg/ ← npm package (aiplang CLI + runtime)
|
|
115
115
|
│ ├── bin/aiplang.js ← CLI: init, serve, build, new, start
|
|
116
116
|
│ ├── runtime/aiplang-hydrate.js← 10KB reactive runtime
|
|
117
117
|
│ ├── server/server.js ← full-stack Node.js server
|
|
118
118
|
│ └── aiplang-knowledge.md ← Claude Project knowledge file
|
|
119
119
|
├── aiplang-go/ ← Go compiler + server (v2)
|
|
120
|
-
│ ├── compiler/compiler.go ← .
|
|
120
|
+
│ ├── compiler/compiler.go ← .aiplang → AST parser
|
|
121
121
|
│ ├── server/server.go ← Go HTTP server
|
|
122
122
|
│ └── cmd/aiplangd/main.go ← binary entrypoint
|
|
123
123
|
├── docs/ ← GitHub Pages (aiplang.io)
|
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.
|
|
8
|
+
const VERSION = '2.5.0'
|
|
9
9
|
const RUNTIME_DIR = path.join(__dirname, '..', 'runtime')
|
|
10
10
|
const cmd = process.argv[2]
|
|
11
11
|
const args = process.argv.slice(3)
|
|
@@ -454,7 +454,7 @@ if (cmd==='build') {
|
|
|
454
454
|
} else if(input.endsWith('.aiplang')&&fs.existsSync(input)){ files.push(input) }
|
|
455
455
|
if(!files.length){console.error(`\n ✗ No .aiplang files in: ${input}\n`);process.exit(1)}
|
|
456
456
|
const src=files.map(f=>fs.readFileSync(f,'utf8')).join('\n---\n')
|
|
457
|
-
const pages=
|
|
457
|
+
const pages=parsePages(src)
|
|
458
458
|
if(!pages.length){console.error('\n ✗ No pages found.\n');process.exit(1)}
|
|
459
459
|
fs.mkdirSync(outDir,{recursive:true})
|
|
460
460
|
console.log(`\n aiplang build v${VERSION} — ${files.length} file(s)\n`)
|
|
@@ -542,7 +542,7 @@ process.exit(1)
|
|
|
542
542
|
// PARSER
|
|
543
543
|
// ═════════════════════════════════════════════════════════════════
|
|
544
544
|
|
|
545
|
-
function
|
|
545
|
+
function parsePages(src) {
|
|
546
546
|
return src.split(/\n---\n/).map(s=>parsePage(s.trim())).filter(Boolean)
|
|
547
547
|
}
|
|
548
548
|
|
package/package.json
CHANGED
|
@@ -456,7 +456,7 @@ class Renderer {
|
|
|
456
456
|
|
|
457
457
|
render(page) {
|
|
458
458
|
this.container.innerHTML = ''
|
|
459
|
-
this.container.className = `
|
|
459
|
+
this.container.className = `aiplang-root aiplang-theme-${page.theme}`
|
|
460
460
|
for (const block of page.blocks) {
|
|
461
461
|
const el = this.renderBlock(block)
|
|
462
462
|
if (el) this.container.appendChild(el)
|
|
@@ -1069,9 +1069,9 @@ input,button,select{font-family:inherit}
|
|
|
1069
1069
|
|
|
1070
1070
|
function boot(src, container) {
|
|
1071
1071
|
// Inject CSS once
|
|
1072
|
-
if (!document.getElementById('
|
|
1072
|
+
if (!document.getElementById('aiplang-css')) {
|
|
1073
1073
|
const style = document.createElement('style')
|
|
1074
|
-
style.id = '
|
|
1074
|
+
style.id = 'aiplang-css'
|
|
1075
1075
|
style.textContent = CSS
|
|
1076
1076
|
document.head.appendChild(style)
|
|
1077
1077
|
}
|
|
@@ -1089,9 +1089,9 @@ return { boot, parseFlux, State, Renderer, Router, QueryEngine }
|
|
|
1089
1089
|
|
|
1090
1090
|
})()
|
|
1091
1091
|
|
|
1092
|
-
// Auto-boot from <script type="text/
|
|
1092
|
+
// Auto-boot from <script type="text/aiplang">
|
|
1093
1093
|
document.addEventListener('DOMContentLoaded', () => {
|
|
1094
|
-
const script = document.querySelector('script[type="text/
|
|
1094
|
+
const script = document.querySelector('script[type="text/aiplang"]')
|
|
1095
1095
|
if (script) {
|
|
1096
1096
|
const targetSel = script.getAttribute('target') || '#app'
|
|
1097
1097
|
const container = document.querySelector(targetSel)
|
package/server/server.js
CHANGED
|
@@ -29,7 +29,15 @@ function persistDB() {
|
|
|
29
29
|
if (!_db || !DB_FILE || DB_FILE === ':memory:') return
|
|
30
30
|
try { fs.writeFileSync(DB_FILE, Buffer.from(_db.export())) } catch {}
|
|
31
31
|
}
|
|
32
|
-
|
|
32
|
+
let _dirty = false, _persistTimer = null
|
|
33
|
+
function dbRun(sql, params = []) {
|
|
34
|
+
_db.run(sql, params)
|
|
35
|
+
_dirty = true
|
|
36
|
+
if (!_persistTimer) _persistTimer = setTimeout(() => {
|
|
37
|
+
if (_dirty) { try { persistDB() } catch {} _dirty = false }
|
|
38
|
+
_persistTimer = null
|
|
39
|
+
}, 200)
|
|
40
|
+
}
|
|
33
41
|
function dbAll(sql, params = []) {
|
|
34
42
|
const stmt = _db.prepare(sql); stmt.bind(params)
|
|
35
43
|
const rows = []; while (stmt.step()) rows.push(stmt.getAsObject()); stmt.free()
|
|
@@ -135,7 +143,10 @@ class Model {
|
|
|
135
143
|
if (this.softDelete) conditions.push('deleted_at IS NULL')
|
|
136
144
|
if (opts.where) { conditions.push(opts.where); if (opts.whereParams) params.push(...opts.whereParams) }
|
|
137
145
|
if (conditions.length) sql += ` WHERE ${conditions.join(' AND ')}`
|
|
138
|
-
if (opts.order)
|
|
146
|
+
if (opts.order) {
|
|
147
|
+
const safeOrder = /^[a-zA-Z_][a-zA-Z0-9_]*(\s+(asc|desc))?$/i
|
|
148
|
+
if (safeOrder.test(String(opts.order))) sql += ` ORDER BY ${opts.order}`
|
|
149
|
+
}
|
|
139
150
|
if (opts.limit) sql += ` LIMIT ${opts.limit}`
|
|
140
151
|
if (opts.offset) sql += ` OFFSET ${opts.offset}`
|
|
141
152
|
return dbAll(sql, params)
|
|
@@ -276,6 +287,15 @@ function migrateModels(models) {
|
|
|
276
287
|
if (!cols.some(c=>c.startsWith('updated_at'))) cols.push('updated_at TEXT')
|
|
277
288
|
if (model.softDelete) { if (!cols.some(c=>c.startsWith('deleted_at'))) cols.push('deleted_at TEXT') }
|
|
278
289
|
try { dbRun(`CREATE TABLE IF NOT EXISTS ${table} (${cols.join(', ')})`) } catch {}
|
|
290
|
+
// Auto-index on unique + indexed fields
|
|
291
|
+
for (const f of model.fields) {
|
|
292
|
+
const colName = toCol(f.name)
|
|
293
|
+
if (f.modifiers.includes('unique') || f.modifiers.includes('index')) {
|
|
294
|
+
try { dbRun(`CREATE INDEX IF NOT EXISTS idx_${table}_${colName} ON ${table}(${colName})`) } catch {}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
// Always index created_at for pagination performance
|
|
298
|
+
try { dbRun(`CREATE INDEX IF NOT EXISTS idx_${table}_created_at ON ${table}(created_at)`) } catch {}
|
|
279
299
|
console.log(`[aiplang] ✓ ${table} (${cols.length} cols${model.softDelete ? ', soft-delete' : ''})`)
|
|
280
300
|
MODEL_DEFS[model.name] = { softDelete: model.softDelete, timestamps: true }
|
|
281
301
|
}
|
|
@@ -796,6 +816,11 @@ class AiplangServer {
|
|
|
796
816
|
res.writeHead(429, { 'Content-Type': 'application/json' })
|
|
797
817
|
res.end(JSON.stringify({ error: 'Too many requests' })); return
|
|
798
818
|
}
|
|
819
|
+
// Auto rate-limit on auth endpoints
|
|
820
|
+
if (this._authRateLimit && this._authRateLimit(req)) {
|
|
821
|
+
res.writeHead(429, { 'Content-Type': 'application/json' })
|
|
822
|
+
res.end(JSON.stringify({ error: 'Too many requests. Try again in 1 minute.' })); return
|
|
823
|
+
}
|
|
799
824
|
|
|
800
825
|
// CORS — use plugin config if set, otherwise allow all
|
|
801
826
|
const origins = this._corsOrigins || ['*']
|
|
@@ -1294,6 +1319,16 @@ async function startServer(aipFile, port = 3000) {
|
|
|
1294
1319
|
// Events
|
|
1295
1320
|
for (const ev of app.events) on(ev.event, (data) => console.log(`[aiplang:event] ${ev.event}:`, ev.action))
|
|
1296
1321
|
|
|
1322
|
+
// Auth rate limiting (automatic — 20 req/min per IP on /api/auth/*)
|
|
1323
|
+
const _authAttempts = {}
|
|
1324
|
+
srv._authRateLimit = (req) => {
|
|
1325
|
+
if (!req.path?.includes('/api/auth/')) return false
|
|
1326
|
+
const ip = req.socket?.remoteAddress || 'unknown'
|
|
1327
|
+
const key = `${ip}:${Math.floor(Date.now() / 60000)}`
|
|
1328
|
+
_authAttempts[key] = (_authAttempts[key] || 0) + 1
|
|
1329
|
+
return _authAttempts[key] > 20
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1297
1332
|
// Routes
|
|
1298
1333
|
for (const route of app.apis) {
|
|
1299
1334
|
compileRoute(route, srv)
|