lumpiajs 1.0.9 → 1.0.10
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 +43 -25
- package/lib/commands/build.js +125 -332
- package/lib/commands/create.js +58 -138
- package/lib/commands/serve.js +51 -112
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,46 +1,64 @@
|
|
|
1
1
|
# 🥟 LumpiaJS
|
|
2
2
|
|
|
3
|
-
**Bahasa Pemrograman Web dengan Kearifan Lokal Semarangan
|
|
3
|
+
**"Bahasa Pemrograman Web dengan Kearifan Lokal Semarangan."**
|
|
4
|
+
|
|
5
|
+
Framework Static SPA 100% Client-Side. Coding pakai bahasa sehari-hari.
|
|
4
6
|
|
|
5
7
|
---
|
|
6
8
|
|
|
7
|
-
##
|
|
9
|
+
## 🗣️ Kamus Bahasa
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
| Semarangan | JS Asli | Arti |
|
|
12
|
+
| :------------ | :------------ | :-------------------- |
|
|
13
|
+
| **`aku`** | `this` | Diri Sendiri (Object) |
|
|
14
|
+
| **`fungsi`** | `function` | Fungsi |
|
|
15
|
+
| **`paten`** | `const` | Konstan |
|
|
16
|
+
| **`ono`** | `let` | Ada / Variabel |
|
|
17
|
+
| **`mengko`** | `async` | Nanti (Async) |
|
|
18
|
+
| **`nteni`** | `await` | Tunggu (Await) |
|
|
19
|
+
| **`balek`** | `return` | Kembali |
|
|
20
|
+
| **`kandani`** | `console.log` | Bilangi |
|
|
10
21
|
|
|
11
|
-
|
|
22
|
+
_Plus fitur **Laravel Syntax**: `aku->tampil()`._
|
|
12
23
|
|
|
13
|
-
|
|
14
|
-
- Jika di **Vercel/Node**, server otomatis mengarahkan ke `api.js`.
|
|
24
|
+
**Contoh Coding (`HomeController.lmp`):**
|
|
15
25
|
|
|
16
|
-
|
|
26
|
+
```javascript
|
|
27
|
+
export default class HomeController extends Controller {
|
|
28
|
+
mengko index() {
|
|
29
|
+
paten pesan = 'Halo Lur!';
|
|
17
30
|
|
|
18
|
-
|
|
19
|
-
|
|
31
|
+
// Panggil fungsi view
|
|
32
|
+
balek aku->tampil('home', { msg: pesan });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
20
35
|
```
|
|
21
36
|
|
|
22
|
-
|
|
37
|
+
---
|
|
23
38
|
|
|
24
|
-
|
|
39
|
+
## 🚀 Cara Pakai
|
|
25
40
|
|
|
26
|
-
1.
|
|
27
|
-
2. Edit **`api.php`** (Isi config database).
|
|
28
|
-
3. Selesai.
|
|
29
|
-
_Server otomatis pakai `.htaccess` untuk routing._
|
|
41
|
+
**1. Install**
|
|
30
42
|
|
|
31
|
-
|
|
43
|
+
```bash
|
|
44
|
+
npm install -g lumpiajs
|
|
45
|
+
```
|
|
32
46
|
|
|
33
|
-
|
|
34
|
-
2. Set Environment Variables di Vercel (`DB_HOST`, `DB_USER`, dll).
|
|
35
|
-
3. Selesai.
|
|
36
|
-
_Vercel otomatis baca `vercel.json` dan pakai `api.js` sebagai serverless function._
|
|
47
|
+
**2. Buat Project & Develop**
|
|
37
48
|
|
|
38
|
-
|
|
49
|
+
```bash
|
|
50
|
+
lumpia create-project warung-ku
|
|
51
|
+
cd warung-ku && npm install
|
|
52
|
+
lumpia kukus
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**3. Build Static (Goreng)**
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
lumpia goreng
|
|
59
|
+
```
|
|
39
60
|
|
|
40
|
-
|
|
41
|
-
2. `npm install`
|
|
42
|
-
3. `npm start`
|
|
43
|
-
_Node akan menjalankan `server.js`._
|
|
61
|
+
Upload folder `dist` kemana saja (Hosting Biasa/GitHub Pages).
|
|
44
62
|
|
|
45
63
|
---
|
|
46
64
|
|
package/lib/commands/build.js
CHANGED
|
@@ -1,45 +1,68 @@
|
|
|
1
1
|
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
//
|
|
8
|
-
|
|
2
|
+
// KAMUS SEMARANGAN (Regex Replacement Rules)
|
|
3
|
+
// Urutan penting! Keyword panjang dulu baru pendek.
|
|
4
|
+
const KAMUS = [
|
|
5
|
+
{ from: /paten\s/g, to: 'const ' },
|
|
6
|
+
{ from: /ono\s/g, to: 'let ' },
|
|
7
|
+
{ from: /fungsi\s/g, to: 'function ' }, // Changed from 'gawe'
|
|
8
|
+
{ from: /nteni\s/g, to: 'await ' },
|
|
9
|
+
{ from: /mengko\s/g, to: 'async ' },
|
|
10
|
+
{ from: /balek\s/g, to: 'return ' },
|
|
11
|
+
{ from: /yen\s*\(/g, to: 'if(' },
|
|
12
|
+
{ from: /liyane\s/g, to: 'else ' },
|
|
13
|
+
{ from: /jajal\s*\{/g, to: 'try {' },
|
|
14
|
+
{ from: /gagal\s*\(/g, to: 'catch(' },
|
|
15
|
+
{ from: /kandani\(/g, to: 'console.log(' },
|
|
16
|
+
{ from: /aku->/g, to: 'this.' }, // NEW: this -> aku
|
|
17
|
+
{ from: /aku\./g, to: 'this.' }, // Support dot notation too
|
|
18
|
+
{ from: /->/g, to: '.' }
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
function transpileSemarangan(content) {
|
|
9
22
|
let code = content;
|
|
23
|
+
|
|
24
|
+
// Import .lmp -> .js
|
|
10
25
|
code = code.replace(/from\s+['"](.+?)\.lmp['"]/g, "from '$1.js'");
|
|
26
|
+
code = code.replace(/from\s+['"]lumpiajs['"]/g, "from '/core/lumpia.js'");
|
|
27
|
+
|
|
28
|
+
// Safe Replace Logic
|
|
29
|
+
// We must handle this carefully to not break valid JS code if mixed.
|
|
30
|
+
// 'aku' is common word, but as keyword usually followed by -> or .
|
|
31
|
+
|
|
11
32
|
code = code.split('\n').map(line => {
|
|
12
|
-
|
|
13
|
-
|
|
33
|
+
let l = line;
|
|
34
|
+
if (l.trim().startsWith('//')) return l;
|
|
35
|
+
|
|
36
|
+
KAMUS.forEach(rule => {
|
|
37
|
+
l = l.replace(rule.from, rule.to);
|
|
38
|
+
});
|
|
39
|
+
return l;
|
|
14
40
|
}).join('\n');
|
|
15
|
-
|
|
16
|
-
// Rewrite imports for Browser
|
|
17
|
-
code = code.replace(/from\s+['"]lumpiajs['"]/g, "from '/core/index.js'");
|
|
18
|
-
code = code.replace(/from\s+['"]lumpiajs\/lib\/(.+?)['"]/g, "from '/core/$1'");
|
|
41
|
+
|
|
19
42
|
return code;
|
|
20
43
|
}
|
|
21
44
|
|
|
45
|
+
// ... Rest of build.js code (processDirectory, browserCore, indexHtml, buildProject) ...
|
|
46
|
+
// ... I will copy the previous logic but update browserCore as well.
|
|
47
|
+
|
|
48
|
+
import fs from 'fs';
|
|
49
|
+
import path from 'path';
|
|
50
|
+
import { spawnSync } from 'child_process';
|
|
51
|
+
import { loadConfig } from '../core/Config.js';
|
|
52
|
+
|
|
22
53
|
function processDirectory(source, dest) {
|
|
23
54
|
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
24
|
-
|
|
25
|
-
const items = fs.readdirSync(source);
|
|
26
|
-
items.forEach(item => {
|
|
55
|
+
fs.readdirSync(source).forEach(item => {
|
|
27
56
|
const srcPath = path.join(source, item);
|
|
28
57
|
const destPath = path.join(dest, item);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (stat.isDirectory()) {
|
|
58
|
+
if (fs.statSync(srcPath).isDirectory()) {
|
|
32
59
|
processDirectory(srcPath, destPath);
|
|
33
60
|
} else {
|
|
34
|
-
if (item.endsWith('.lmp')) {
|
|
61
|
+
if (item.endsWith('.lmp') || item.endsWith('.js')) {
|
|
35
62
|
const content = fs.readFileSync(srcPath, 'utf8');
|
|
36
|
-
const jsContent =
|
|
37
|
-
const
|
|
38
|
-
fs.writeFileSync(
|
|
39
|
-
} else if (item.endsWith('.js')) {
|
|
40
|
-
const content = fs.readFileSync(srcPath, 'utf8');
|
|
41
|
-
const jsContent = transpileContent(content);
|
|
42
|
-
fs.writeFileSync(destPath, jsContent);
|
|
63
|
+
const jsContent = transpileSemarangan(content);
|
|
64
|
+
const finalDest = destPath.replace('.lmp', '.js');
|
|
65
|
+
fs.writeFileSync(finalDest, jsContent);
|
|
43
66
|
} else {
|
|
44
67
|
fs.copyFileSync(srcPath, destPath);
|
|
45
68
|
}
|
|
@@ -47,263 +70,87 @@ function processDirectory(source, dest) {
|
|
|
47
70
|
});
|
|
48
71
|
}
|
|
49
72
|
|
|
50
|
-
|
|
51
|
-
const phpBridgeContent = `<?php
|
|
52
|
-
header("Access-Control-Allow-Origin: *");
|
|
53
|
-
header("Access-Control-Allow-Headers: Content-Type");
|
|
54
|
-
header("Content-Type: application/json");
|
|
55
|
-
|
|
56
|
-
// ⚠️ EDIT CONFIG INI
|
|
57
|
-
$host = "localhost";
|
|
58
|
-
$user = "root";
|
|
59
|
-
$pass = "";
|
|
60
|
-
$db = "lumpia_db";
|
|
61
|
-
|
|
62
|
-
$conn = new mysqli($host, $user, $pass, $db);
|
|
63
|
-
if ($conn->connect_error) die(json_encode(["error" => $conn->connect_error]));
|
|
64
|
-
|
|
65
|
-
$input = json_decode(file_get_contents('php://input'), true);
|
|
66
|
-
if (!$input) exit;
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
$stmt = $conn->prepare($input['sql']);
|
|
70
|
-
if($input['params']) {
|
|
71
|
-
$types = str_repeat("s", count($input['params']));
|
|
72
|
-
$stmt->bind_param($types, ...$input['params']);
|
|
73
|
-
}
|
|
74
|
-
$stmt->execute();
|
|
75
|
-
$res = $stmt->get_result();
|
|
76
|
-
$data = $res ? $res->fetch_all(MYSQLI_ASSOC) : ["affected" => $stmt->affected_rows];
|
|
77
|
-
echo json_encode($data);
|
|
78
|
-
} catch (Exception $e) {
|
|
79
|
-
http_response_code(500);
|
|
80
|
-
echo json_encode(["error" => $e->getMessage()]);
|
|
81
|
-
}
|
|
82
|
-
$conn->close();
|
|
83
|
-
?>`;
|
|
84
|
-
|
|
85
|
-
// --- 2. NODE.JS / VERCEL BACKEND ADAPTER ---
|
|
86
|
-
// Ini file 'api.js' yang akan dijalankan oleh Vercel atau Server.js local
|
|
87
|
-
const nodeBridgeContent = `
|
|
88
|
-
import { createPool } from 'mysql2/promise';
|
|
89
|
-
|
|
90
|
-
// ⚠️ CONFIG DARI ENV (Vercel/Node style)
|
|
91
|
-
const pool = createPool({
|
|
92
|
-
host: process.env.DB_HOST || 'localhost',
|
|
93
|
-
user: process.env.DB_USER || 'root',
|
|
94
|
-
password: process.env.DB_PASSWORD || '',
|
|
95
|
-
database: process.env.DB_NAME || 'lumpia_db',
|
|
96
|
-
waitForConnections: true,
|
|
97
|
-
connectionLimit: 10
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
export default async function handler(req, res) {
|
|
101
|
-
// Vercel / Express handler signature
|
|
102
|
-
if (req.method !== 'POST') {
|
|
103
|
-
res.statusCode = 405;
|
|
104
|
-
return res.end('Method Not Allowed');
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
try {
|
|
108
|
-
// Parsing body helper
|
|
109
|
-
let body = req.body;
|
|
110
|
-
if (typeof body === 'string') body = JSON.parse(body); // if raw string
|
|
111
|
-
|
|
112
|
-
const { sql, params } = body;
|
|
113
|
-
const [rows] = await pool.execute(sql, params);
|
|
114
|
-
|
|
115
|
-
res.statusCode = 200;
|
|
116
|
-
res.setHeader('Content-Type', 'application/json');
|
|
117
|
-
res.end(JSON.stringify(rows));
|
|
118
|
-
} catch (error) {
|
|
119
|
-
console.error(error);
|
|
120
|
-
res.statusCode = 500;
|
|
121
|
-
res.setHeader('Content-Type', 'application/json');
|
|
122
|
-
res.end(JSON.stringify({ error: error.message }));
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
`;
|
|
126
|
-
|
|
127
|
-
// --- 3. SERVER.JS (Standalone Node Server) ---
|
|
128
|
-
// Server statis + API Handler
|
|
129
|
-
const serverJsContent = `
|
|
130
|
-
import http from 'http';
|
|
131
|
-
import fs from 'fs';
|
|
132
|
-
import path from 'path';
|
|
133
|
-
import apiHandler from './api.js';
|
|
134
|
-
|
|
135
|
-
const root = process.cwd();
|
|
136
|
-
|
|
137
|
-
const server = http.createServer(async (req, res) => {
|
|
138
|
-
const url = new URL(req.url, 'http://' + req.headers.host);
|
|
139
|
-
|
|
140
|
-
// API ROUTE
|
|
141
|
-
if (url.pathname === '/api') {
|
|
142
|
-
let body = '';
|
|
143
|
-
req.on('data', chunk => body += chunk);
|
|
144
|
-
req.on('end', () => {
|
|
145
|
-
req.body = body ? JSON.parse(body) : {};
|
|
146
|
-
apiHandler(req, res);
|
|
147
|
-
});
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// STATIC FILES
|
|
152
|
-
let filePath = path.join(root, url.pathname === '/' ? 'index.html' : url.pathname);
|
|
153
|
-
|
|
154
|
-
// SPA Fallback: If not file, serve index.html
|
|
155
|
-
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
|
156
|
-
filePath = path.join(root, 'index.html');
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const ext = path.extname(filePath);
|
|
160
|
-
const mime = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css' };
|
|
161
|
-
res.writeHead(200, { 'Content-Type': mime[ext] || 'application/octet-stream' });
|
|
162
|
-
fs.createReadStream(filePath).pipe(res);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
const port = process.env.PORT || 3000;
|
|
166
|
-
server.listen(port, () => console.log('🚀 Server running on port ' + port));
|
|
167
|
-
`;
|
|
168
|
-
|
|
169
|
-
// --- 4. BROWSER CORE (Polymorphic Client) ---
|
|
170
|
-
const browserCoreIndex = `
|
|
73
|
+
const browserCore = `
|
|
171
74
|
export class Controller {
|
|
172
|
-
constructor() { this.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
75
|
+
constructor() { this.params={}; }
|
|
76
|
+
async tampil(viewName, data={}) {
|
|
77
|
+
try {
|
|
78
|
+
const res = await fetch('/views/'+viewName+'.lmp');
|
|
79
|
+
if(!res.ok) throw new Error('View 404');
|
|
80
|
+
let html = await res.text();
|
|
81
|
+
|
|
82
|
+
const matchKulit = html.match(/<kulit>([\\s\\S]*?)<\\/kulit>/);
|
|
83
|
+
const matchIsi = html.match(/<isi>([\\s\\S]*?)<\\/isi>/);
|
|
84
|
+
const matchKlambi = html.match(/<klambi>([\\s\\S]*?)<\\/klambi>/);
|
|
85
|
+
|
|
86
|
+
let body = matchKulit?matchKulit[1]:'', script=matchIsi?matchIsi[1]:'', css=matchKlambi?matchKlambi[1]:'';
|
|
87
|
+
|
|
88
|
+
for(const [k,v] of Object.entries(data)) {
|
|
89
|
+
let s = typeof v === 'object' ? JSON.stringify(v) : v;
|
|
90
|
+
body = body.replace(new RegExp('{{\\\\s*'+k+'\\\\s*}}','g'), s);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
document.getElementById('app').innerHTML = body;
|
|
94
|
+
if(css && !document.getElementById('css-'+viewName)) {
|
|
95
|
+
const s = document.createElement('style'); s.id='css-'+viewName; s.textContent=css;
|
|
96
|
+
document.head.appendChild(s);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if(script) {
|
|
100
|
+
// Client-side transpile
|
|
101
|
+
const kamus = [
|
|
102
|
+
{f:/paten\\s/g,t:'const '}, {f:/ono\\s/g,t:'let '}, {f:/fungsi\\s/g,t:'function '},
|
|
103
|
+
{f:/nteni\\s/g,t:'await '}, {f:/mengko\\s/g,t:'async '}, {f:/balek\\s/g,t:'return '},
|
|
104
|
+
{f:/yen\\s*\\(/g,t:'if('}, {f:/liyane\\s/g,t:'else '}, {f:/kandani\\(/g,t:'console.log('},
|
|
105
|
+
{f:/aku->/g,t:'this.'}, {f:/aku\\./g,t:'this.'}
|
|
106
|
+
];
|
|
107
|
+
kamus.forEach(r => script = script.replace(r.f, r.t));
|
|
108
|
+
new Function(script)();
|
|
109
|
+
}
|
|
110
|
+
} catch(e) { document.getElementById('app').innerHTML = e.message; }
|
|
208
111
|
}
|
|
209
|
-
|
|
210
|
-
json(data) { document.getElementById('app').innerText = JSON.stringify(data, null, 2); }
|
|
211
112
|
}
|
|
113
|
+
export const Jalan = { routes:[], get:(p,a)=>Jalan.routes.push({p,a}) };
|
|
114
|
+
`;
|
|
212
115
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
});
|
|
224
|
-
|
|
116
|
+
const indexHtml = `<!DOCTYPE html>
|
|
117
|
+
<html lang="en">
|
|
118
|
+
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1.0"><title>LumpiaJS</title><link rel="stylesheet" href="/public/css/style.css"></head>
|
|
119
|
+
<body><div id="app">Loading...</div>
|
|
120
|
+
<script type="module">
|
|
121
|
+
import { Jalan } from '/routes/web.js';
|
|
122
|
+
async function navigate() {
|
|
123
|
+
const p = window.location.pathname;
|
|
124
|
+
let m = null, args = {};
|
|
125
|
+
for(let r of Jalan.routes) {
|
|
126
|
+
let reg = new RegExp('^'+r.p.replace(/{([a-zA-Z0-9_]+)}/g, '([^/]+)')+'$');
|
|
127
|
+
let res = p.match(reg);
|
|
128
|
+
if(res){ m=r; args=res.slice(1); break; }
|
|
225
129
|
}
|
|
130
|
+
if(m) {
|
|
131
|
+
const [cName, fName] = m.a.split('@');
|
|
132
|
+
try {
|
|
133
|
+
const mod = await import('/app/controllers/'+cName+'.js?'+Date.now());
|
|
134
|
+
const C = mod.default; const i = new C(); i.params=args;
|
|
135
|
+
await i[fName](...args);
|
|
136
|
+
} catch(e) { console.error(e); document.getElementById('app').innerHTML='Error'; }
|
|
137
|
+
} else { document.getElementById('app').innerHTML='404'; }
|
|
226
138
|
}
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
orderBy(c, d='ASC') { this.order = c+' '+d; return this; }
|
|
232
|
-
async get() {
|
|
233
|
-
let sql = 'SELECT * FROM ' + this.table;
|
|
234
|
-
if(this.conds.length) sql += ' WHERE ' + this.conds.join(' AND ');
|
|
235
|
-
if(this.order) sql += ' ORDER BY ' + this.order;
|
|
236
|
-
return await DB.query(sql, this.binds);
|
|
139
|
+
window.addEventListener('popstate', navigate);
|
|
140
|
+
document.body.addEventListener('click', e => {
|
|
141
|
+
if(e.target.tagName==='A' && e.target.href.startsWith(window.location.origin)) {
|
|
142
|
+
e.preventDefault(); history.pushState(null,'',e.target.href); navigate();
|
|
237
143
|
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
routes: [],
|
|
242
|
-
get: (p, a) => Jalan.routes.push({p, a, m:'GET'}),
|
|
243
|
-
post: (p, a) => Jalan.routes.push({p, a, m:'POST'})
|
|
244
|
-
};
|
|
245
|
-
`;
|
|
246
|
-
|
|
247
|
-
const indexHtmlContent = `<!DOCTYPE html>
|
|
248
|
-
<html lang="en">
|
|
249
|
-
<head>
|
|
250
|
-
<meta charset="UTF-8">
|
|
251
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
252
|
-
<title>LumpiaJS App</title>
|
|
253
|
-
<link rel="stylesheet" href="/public/css/style.css">
|
|
254
|
-
</head>
|
|
255
|
-
<body>
|
|
256
|
-
<div id="app">Loading...</div>
|
|
257
|
-
|
|
258
|
-
<script type="module">
|
|
259
|
-
import { Jalan } from '/routes/web.js';
|
|
260
|
-
|
|
261
|
-
async function navigate() {
|
|
262
|
-
const path = window.location.pathname;
|
|
263
|
-
let match = null, params = {};
|
|
264
|
-
for(let r of Jalan.routes) {
|
|
265
|
-
const regexStr = '^' + r.p.replace(/{([a-zA-Z0-9_]+)}/g, '([^/]+)') + '$';
|
|
266
|
-
const regex = new RegExp(regexStr);
|
|
267
|
-
const m = path.match(regex);
|
|
268
|
-
if(m) { match = r; params = m.slice(1); break; }
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if(match) {
|
|
272
|
-
const [cName, mName] = match.a.split('@');
|
|
273
|
-
try {
|
|
274
|
-
const module = await import('/app/controllers/' + cName + '.js?' + Date.now());
|
|
275
|
-
const Controller = module.default;
|
|
276
|
-
const ctrl = new Controller();
|
|
277
|
-
await ctrl[mName](...params);
|
|
278
|
-
} catch(e) {
|
|
279
|
-
document.getElementById('app').innerHTML = '<h1>Error</h1><p>' + e.message + '</p>';
|
|
280
|
-
}
|
|
281
|
-
} else {
|
|
282
|
-
document.getElementById('app').innerHTML = '<h1>404</h1>';
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
window.addEventListener('popstate', navigate);
|
|
287
|
-
document.body.addEventListener('click', e => {
|
|
288
|
-
if(e.target.tagName === 'A' && e.target.href.startsWith(window.location.origin)) {
|
|
289
|
-
e.preventDefault();
|
|
290
|
-
history.pushState(null, '', e.target.href);
|
|
291
|
-
navigate();
|
|
292
|
-
}
|
|
293
|
-
});
|
|
294
|
-
navigate();
|
|
295
|
-
</script>
|
|
296
|
-
</body>
|
|
297
|
-
</html>`;
|
|
298
|
-
|
|
144
|
+
});
|
|
145
|
+
navigate();
|
|
146
|
+
</script></body></html>`;
|
|
299
147
|
|
|
300
148
|
export function buildProject() {
|
|
301
149
|
const root = process.cwd();
|
|
302
150
|
const dist = path.join(root, 'dist');
|
|
303
151
|
const config = loadConfig(root);
|
|
304
|
-
|
|
305
|
-
console.log('🍳
|
|
306
|
-
|
|
152
|
+
|
|
153
|
+
console.log('🍳 Goreng Project (Mode Bahasa Semarangan)...');
|
|
307
154
|
if (fs.existsSync(dist)) fs.rmSync(dist, { recursive: true, force: true });
|
|
308
155
|
fs.mkdirSync(dist);
|
|
309
156
|
|
|
@@ -312,69 +159,15 @@ export function buildProject() {
|
|
|
312
159
|
spawnSync(cmd, ['tailwindcss', '-i', './aset/css/style.css', '-o', './public/css/style.css', '--minify'], { cwd: root, stdio: 'ignore', shell: true });
|
|
313
160
|
}
|
|
314
161
|
|
|
315
|
-
console.log('📂 Converting Code...');
|
|
316
162
|
processDirectory(path.join(root, 'app'), path.join(dist, 'app'));
|
|
317
163
|
processDirectory(path.join(root, 'routes'), path.join(dist, 'routes'));
|
|
318
|
-
|
|
319
|
-
fs.mkdirSync(path.join(dist, 'core'), { recursive: true });
|
|
320
|
-
fs.writeFileSync(path.join(dist, 'core', 'index.js'), browserCoreIndex);
|
|
321
|
-
|
|
322
164
|
fs.cpSync(path.join(root, 'views'), path.join(dist, 'views'), { recursive: true });
|
|
323
|
-
|
|
324
|
-
if (fs.existsSync(path.join(root, 'public'))) {
|
|
325
|
-
fs.cpSync(path.join(root, 'public'), path.join(dist, 'public'), { recursive: true });
|
|
326
|
-
}
|
|
165
|
+
if (fs.existsSync(path.join(root, 'public'))) fs.cpSync(path.join(root, 'public'), path.join(dist, 'public'), { recursive: true });
|
|
327
166
|
|
|
328
|
-
fs.
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
// 1. PHP Adapter
|
|
333
|
-
fs.writeFileSync(path.join(dist, 'api.php'), phpBridgeContent);
|
|
334
|
-
|
|
335
|
-
// 2. Node/Vercel Adapter
|
|
336
|
-
fs.writeFileSync(path.join(dist, 'api.js'), nodeBridgeContent);
|
|
337
|
-
fs.writeFileSync(path.join(dist, 'server.js'), serverJsContent);
|
|
338
|
-
fs.writeFileSync(path.join(dist, 'package.json'), JSON.stringify({
|
|
339
|
-
"type": "module",
|
|
340
|
-
"scripts": { "start": "node server.js" },
|
|
341
|
-
"dependencies": { "mysql2": "^3.0.0" }
|
|
342
|
-
}, null, 2));
|
|
343
|
-
|
|
344
|
-
// 3. Routing Rules
|
|
345
|
-
|
|
346
|
-
// .htaccess (Apache) -> Redirect /api ke api.php
|
|
347
|
-
fs.writeFileSync(path.join(dist, '.htaccess'), `
|
|
348
|
-
<IfModule mod_rewrite.c>
|
|
349
|
-
RewriteEngine On
|
|
350
|
-
RewriteBase /
|
|
351
|
-
|
|
352
|
-
# API Routing: /api -> api.php
|
|
353
|
-
RewriteRule ^api$ api.php [L]
|
|
354
|
-
|
|
355
|
-
# SPA Routing: Everything else -> index.html
|
|
356
|
-
RewriteCond %{REQUEST_FILENAME} !-f
|
|
357
|
-
RewriteCond %{REQUEST_FILENAME} !-d
|
|
358
|
-
RewriteRule . /index.html [L]
|
|
359
|
-
</IfModule>
|
|
360
|
-
`);
|
|
361
|
-
|
|
362
|
-
// vercel.json (Vercel) -> Redirect /api ke api.js
|
|
363
|
-
fs.writeFileSync(path.join(dist, 'vercel.json'), JSON.stringify({
|
|
364
|
-
"rewrites": [
|
|
365
|
-
{ "source": "/api", "destination": "/api.js" },
|
|
366
|
-
{ "source": "/(.*)", "destination": "/index.html" }
|
|
367
|
-
],
|
|
368
|
-
"functions": {
|
|
369
|
-
"api.js": { "includeFiles": "package.json" }
|
|
370
|
-
}
|
|
371
|
-
}, null, 2));
|
|
167
|
+
fs.mkdirSync(path.join(dist, 'core'), { recursive: true });
|
|
168
|
+
fs.writeFileSync(path.join(dist, 'core', 'lumpia.js'), browserCore);
|
|
169
|
+
fs.writeFileSync(path.join(dist, 'index.html'), indexHtml);
|
|
170
|
+
fs.writeFileSync(path.join(dist, '.htaccess'), `<IfModule mod_rewrite.c>\nRewriteEngine On\nRewriteBase /\nRewriteRule ^index\\.html$ - [L]\nRewriteCond %{REQUEST_FILENAME} !-f\nRewriteCond %{REQUEST_FILENAME} !-d\nRewriteRule . /index.html [L]\n</IfModule>`);
|
|
372
171
|
|
|
373
|
-
console.log('✅ Mateng! (
|
|
374
|
-
console.log('----------------------------------------------------');
|
|
375
|
-
console.log('📂 Folder "dist" ini UNIVERSAL:');
|
|
376
|
-
console.log(' - Hosting PHP (XAMPP/cPanel): Otomatis pake api.php (via .htaccess)');
|
|
377
|
-
console.log(' - Vercel: Otomatis pake api.js (via vercel.json)');
|
|
378
|
-
console.log(' - Node VPS: Otomatis pake api.js (via server.js)');
|
|
379
|
-
console.log('----------------------------------------------------');
|
|
172
|
+
console.log('✅ Mateng! (Support Bahasa Semarangan)');
|
|
380
173
|
}
|
package/lib/commands/create.js
CHANGED
|
@@ -4,155 +4,84 @@ import prompts from 'prompts';
|
|
|
4
4
|
|
|
5
5
|
const routesTemplate = `import { Jalan } from 'lumpiajs';
|
|
6
6
|
|
|
7
|
-
// Pakai gaya Laravel (->) enak to?
|
|
8
7
|
Jalan->get('/', 'HomeController@index');
|
|
9
|
-
Jalan->get('/
|
|
10
|
-
Jalan->get('/profile', 'HomeController@profile');
|
|
11
|
-
Jalan->get('/api/products', 'ProductController@index');
|
|
8
|
+
Jalan->get('/toko', 'ProductController@tampilBarang');
|
|
12
9
|
`;
|
|
13
10
|
|
|
14
|
-
const controllerTemplate = `import { Controller
|
|
11
|
+
const controllerTemplate = `import { Controller } from 'lumpiajs';
|
|
15
12
|
|
|
16
13
|
export default class HomeController extends Controller {
|
|
17
|
-
index() {
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
mengko index() {
|
|
15
|
+
// Pake Bahasa Semarangan, Lur!
|
|
16
|
+
paten pesen = 'Sugeng Rawuh di Website Statis!';
|
|
17
|
+
|
|
18
|
+
// 'aku' menggantikan 'this'
|
|
19
|
+
balek aku->tampil('home', {
|
|
20
|
+
message: pesen,
|
|
21
|
+
info: 'Dibuat dengan LumpiaJS'
|
|
24
22
|
});
|
|
25
23
|
}
|
|
26
|
-
|
|
27
|
-
async testDb() {
|
|
28
|
-
try {
|
|
29
|
-
// CONTOH QUERY ALA LARAVEL
|
|
30
|
-
// Pake tanda panah -> biar mantap
|
|
31
|
-
const result = await DB.table('users')
|
|
32
|
-
->limit(1)
|
|
33
|
-
->get();
|
|
34
|
-
|
|
35
|
-
// Raw Query
|
|
36
|
-
const raw = await DB->query('SELECT 1 + 1 AS solution');
|
|
37
|
-
|
|
38
|
-
return this->json({
|
|
39
|
-
status: 'Connected!',
|
|
40
|
-
sample_user: result,
|
|
41
|
-
math_check: raw[0].solution
|
|
42
|
-
});
|
|
43
|
-
} catch (e) {
|
|
44
|
-
return this->json({ error: e.message });
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
profile() {
|
|
49
|
-
return this->tampil('profile', { name: 'Loyal User' });
|
|
50
|
-
}
|
|
51
24
|
}
|
|
52
25
|
`;
|
|
53
26
|
|
|
54
|
-
const
|
|
55
|
-
const productData = [
|
|
56
|
-
{ id: 1, name: 'Lumpia Basah', price: 5000 },
|
|
57
|
-
{ id: 2, name: 'Lumpia Goreng', price: 6000 }
|
|
58
|
-
];
|
|
59
|
-
export default productData;
|
|
60
|
-
`;
|
|
61
|
-
|
|
62
|
-
const productControllerTemplate = `import { Controller, Model } from 'lumpiajs';
|
|
63
|
-
import ProductData from '../../app/models/Product.lmp';
|
|
27
|
+
const productControllerTemplate = `import { Controller } from 'lumpiajs';
|
|
64
28
|
|
|
65
29
|
export default class ProductController extends Controller {
|
|
66
|
-
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
30
|
+
mengko tampilBarang() {
|
|
31
|
+
// Contoh API
|
|
32
|
+
ono data = [];
|
|
33
|
+
|
|
34
|
+
jajal {
|
|
35
|
+
paten respon = nteni fetch('https://fakestoreapi.com/products?limit=3');
|
|
36
|
+
data = nteni respon.json();
|
|
37
|
+
kandani('Data sukses!');
|
|
38
|
+
} gagal (e) {
|
|
39
|
+
kandani(e);
|
|
40
|
+
}
|
|
71
41
|
|
|
72
|
-
|
|
42
|
+
balek aku->tampil('product', {
|
|
43
|
+
daftar: data.map(i => '<li>' + i.title + '</li>').join('')
|
|
44
|
+
});
|
|
73
45
|
}
|
|
74
46
|
}
|
|
75
47
|
`;
|
|
76
48
|
|
|
77
|
-
const
|
|
49
|
+
const homeViewTemplate = `<lump>
|
|
50
|
+
<klambi>
|
|
51
|
+
h1 { color: #d35400; text-align: center; }
|
|
52
|
+
</klambi>
|
|
78
53
|
<kulit>
|
|
79
|
-
<div
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
54
|
+
<div style="text-align: center; font-family: sans-serif; margin-top: 50px;">
|
|
55
|
+
<h1>{{ message }}</h1>
|
|
56
|
+
<p>{{ info }}</p>
|
|
57
|
+
<a href="/toko">Cek Toko Sebelah</a>
|
|
83
58
|
</div>
|
|
84
59
|
</kulit>
|
|
85
60
|
</lump>`;
|
|
86
61
|
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
DB_NAME="lumpia_db"
|
|
97
|
-
`;
|
|
98
|
-
|
|
99
|
-
// Helper for CSS/View generation (Collapsed for brevity but functional as before)
|
|
100
|
-
const tailwindConfigTemplate = `/** @type {import('tailwindcss').Config} */
|
|
101
|
-
module.exports = { content: ["./views/**/*.{html,js,lmp}"], theme: { extend: {}, }, plugins: [], }`;
|
|
102
|
-
const mainCssTemplate = `@tailwind base;\n@tailwind components;\n@tailwind utilities;\n`;
|
|
103
|
-
|
|
104
|
-
const generateHomeView = (style) => {
|
|
105
|
-
let css = '', html = '';
|
|
106
|
-
if (style === 'bootstrap') {
|
|
107
|
-
html = `
|
|
108
|
-
<div class="container mt-5">
|
|
109
|
-
<div class="card shadow">
|
|
110
|
-
<div class="card-body text-center">
|
|
111
|
-
<h1 class="text-primary">{{ message }}</h1>
|
|
112
|
-
<span class="badge bg-secondary mb-3">Env: {{ env }}</span>
|
|
113
|
-
<p>Created by: <strong>{{ author }}</strong></p>
|
|
114
|
-
<div class="mt-4">
|
|
115
|
-
<a href="/db-test" class="btn btn-warning">Test DB (Laravel Style)</a>
|
|
116
|
-
<a href="/profile" class="btn btn-link">Go to Profile</a>
|
|
117
|
-
</div>
|
|
118
|
-
</div>
|
|
119
|
-
</div>
|
|
120
|
-
</div>`;
|
|
121
|
-
} else if (style === 'tailwindcss') {
|
|
122
|
-
html = `
|
|
123
|
-
<div class="container mx-auto mt-10 p-5">
|
|
124
|
-
<div class="bg-white shadow-lg rounded-lg p-8 text-center border border-gray-200">
|
|
125
|
-
<h1 class="text-4xl font-bold text-orange-600 mb-4">{{ message }}</h1>
|
|
126
|
-
<span class="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mb-4">Env: {{ env }}</span>
|
|
127
|
-
<div class="mt-6 space-x-4">
|
|
128
|
-
<a href="/db-test" class="bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 rounded">Test DB</a>
|
|
129
|
-
<a href="/profile" class="text-blue-500 hover:text-blue-800">Go to Profile</a>
|
|
130
|
-
</div>
|
|
131
|
-
</div>
|
|
132
|
-
</div>`;
|
|
133
|
-
} else {
|
|
134
|
-
css = `h1{text-align:center} .box{text-align:center;margin-top:20px}`;
|
|
135
|
-
html = `<div class="box"><h1>{{ message }}</h1><p>Env: {{ env }}</p><a href="/db-test">Test DB</a> | <a href="/profile">Profile</a><br>(Check Controller for Laravel Syntax Demo)</div>`;
|
|
136
|
-
}
|
|
137
|
-
return `<lump><klambi>${css}</klambi><kulit>${html}</kulit><isi></isi></lump>`;
|
|
138
|
-
};
|
|
62
|
+
const productViewTemplate = `<lump>
|
|
63
|
+
<kulit>
|
|
64
|
+
<div style="padding: 20px; font-family: sans-serif;">
|
|
65
|
+
<h1>Daftar Barang</h1>
|
|
66
|
+
<ul>{{ daftar }}</ul>
|
|
67
|
+
<a href="/">Balik Omah</a>
|
|
68
|
+
</div>
|
|
69
|
+
</kulit>
|
|
70
|
+
</lump>`;
|
|
139
71
|
|
|
140
72
|
const generatePackageJson = (name, style) => {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
devDependencies
|
|
146
|
-
|
|
147
|
-
devDependencies["autoprefixer"] = "^10.4.0";
|
|
148
|
-
} else if (style === 'bootstrap') dependencies["bootstrap"] = "^5.3.0";
|
|
149
|
-
return JSON.stringify({ name, version: "1.0.0", main: "routes/web.lmp", type: "module", scripts, dependencies, devDependencies }, null, 2);
|
|
73
|
+
return JSON.stringify({
|
|
74
|
+
name, version: "1.0.0", type: "module",
|
|
75
|
+
scripts: { "start": "lumpia kukus", "build": "lumpia goreng" },
|
|
76
|
+
dependencies: { "lumpiajs": "latest" },
|
|
77
|
+
devDependencies: style==='tailwindcss'?{"tailwindcss":"^3.4.0"}:{}
|
|
78
|
+
}, null, 2);
|
|
150
79
|
};
|
|
151
80
|
|
|
152
81
|
export async function createProject(parameter) {
|
|
153
82
|
let projectName = parameter;
|
|
154
83
|
if (!projectName) {
|
|
155
|
-
const res = await prompts({ type: 'text', name: 'val', message: 'Jeneng project?', initial: 'my-app' });
|
|
84
|
+
const res = await prompts({ type: 'text', name: 'val', message: 'Jeneng project?', initial: 'my-lumpia-app' });
|
|
156
85
|
projectName = res.val;
|
|
157
86
|
}
|
|
158
87
|
if (!projectName) return;
|
|
@@ -162,42 +91,33 @@ export async function createProject(parameter) {
|
|
|
162
91
|
|
|
163
92
|
const styleRes = await prompts({
|
|
164
93
|
type: 'select', name: 'val', message: 'Styling?',
|
|
165
|
-
choices: [{title:'Vanilla',value:'none'},{title:'Tailwind',value:'tailwindcss'}
|
|
94
|
+
choices: [{title:'Vanilla',value:'none'},{title:'Tailwind',value:'tailwindcss'}],
|
|
166
95
|
initial: 0
|
|
167
96
|
});
|
|
168
|
-
const style = styleRes.val;
|
|
169
|
-
if (!style) return;
|
|
170
97
|
|
|
171
|
-
// Structure
|
|
172
98
|
fs.mkdirSync(root);
|
|
173
99
|
fs.mkdirSync(path.join(root, 'app', 'controllers'), { recursive: true });
|
|
174
|
-
fs.mkdirSync(path.join(root, 'app', 'models'), { recursive: true });
|
|
175
100
|
fs.mkdirSync(path.join(root, 'routes'));
|
|
176
101
|
fs.mkdirSync(path.join(root, 'views'));
|
|
177
102
|
fs.mkdirSync(path.join(root, 'aset', 'css'), { recursive: true });
|
|
178
103
|
fs.mkdirSync(path.join(root, 'public', 'css'), { recursive: true });
|
|
179
104
|
|
|
180
|
-
|
|
181
|
-
fs.writeFileSync(path.join(root, 'package.json'), generatePackageJson(projectName, style));
|
|
105
|
+
fs.writeFileSync(path.join(root, 'package.json'), generatePackageJson(projectName, styleRes.val));
|
|
182
106
|
fs.writeFileSync(path.join(root, '.gitignore'), `.lumpia\nnode_modules\n.env\n`);
|
|
183
|
-
fs.writeFileSync(path.join(root, '.
|
|
184
|
-
fs.writeFileSync(path.join(root, 'config.lmp'), JSON.stringify({ klambi: style }, null, 2));
|
|
107
|
+
fs.writeFileSync(path.join(root, 'config.lmp'), JSON.stringify({ klambi: styleRes.val }, null, 2));
|
|
185
108
|
|
|
186
109
|
fs.writeFileSync(path.join(root, 'routes', 'web.lmp'), routesTemplate);
|
|
187
110
|
fs.writeFileSync(path.join(root, 'app', 'controllers', 'HomeController.lmp'), controllerTemplate);
|
|
188
111
|
fs.writeFileSync(path.join(root, 'app', 'controllers', 'ProductController.lmp'), productControllerTemplate);
|
|
189
|
-
fs.writeFileSync(path.join(root, '
|
|
190
|
-
fs.writeFileSync(path.join(root, 'views', '
|
|
191
|
-
fs.writeFileSync(path.join(root, 'views', 'profile.lmp'), viewProfileTemplate);
|
|
112
|
+
fs.writeFileSync(path.join(root, 'views', 'home.lmp'), homeViewTemplate);
|
|
113
|
+
fs.writeFileSync(path.join(root, 'views', 'product.lmp'), productViewTemplate);
|
|
192
114
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
fs.writeFileSync(path.join(root, '
|
|
196
|
-
fs.writeFileSync(path.join(root, 'aset', 'css', 'style.css'), mainCssTemplate);
|
|
115
|
+
if (styleRes.val === 'tailwindcss') {
|
|
116
|
+
fs.writeFileSync(path.join(root, 'tailwind.config.js'), `module.exports={content:["./views/**/*.lmp"],theme:{extend:{}},plugins:[]}`);
|
|
117
|
+
fs.writeFileSync(path.join(root, 'aset', 'css', 'style.css'), '@tailwind base; @tailwind components; @tailwind utilities;');
|
|
197
118
|
} else {
|
|
198
|
-
fs.writeFileSync(path.join(root, 'aset', 'css', 'style.css'),
|
|
119
|
+
fs.writeFileSync(path.join(root, 'aset', 'css', 'style.css'), '/* CSS */');
|
|
199
120
|
}
|
|
200
121
|
|
|
201
122
|
console.log(`✅ Project "${projectName}" Ready!`);
|
|
202
|
-
console.log(`cd ${projectName} && npm install && lumpia kukus`);
|
|
203
123
|
}
|
package/lib/commands/serve.js
CHANGED
|
@@ -1,59 +1,57 @@
|
|
|
1
|
+
|
|
2
|
+
// KAMUS SEMARANGAN (Regex Replacement Rules)
|
|
3
|
+
const KAMUS = [
|
|
4
|
+
{ from: /paten\s/g, to: 'const ' },
|
|
5
|
+
{ from: /ono\s/g, to: 'let ' },
|
|
6
|
+
{ from: /fungsi\s/g, to: 'function ' },
|
|
7
|
+
{ from: /nteni\s/g, to: 'await ' },
|
|
8
|
+
{ from: /mengko\s/g, to: 'async ' },
|
|
9
|
+
{ from: /balek\s/g, to: 'return ' },
|
|
10
|
+
{ from: /yen\s*\(/g, to: 'if(' },
|
|
11
|
+
{ from: /liyane\s/g, to: 'else ' },
|
|
12
|
+
{ from: /jajal\s*\{/g, to: 'try {' },
|
|
13
|
+
{ from: /gagal\s*\(/g, to: 'catch(' },
|
|
14
|
+
{ from: /kandani\(/g, to: 'console.log(' },
|
|
15
|
+
{ from: /aku->/g, to: 'this.' },
|
|
16
|
+
{ from: /aku\./g, to: 'this.' },
|
|
17
|
+
{ from: /->/g, to: '.' }
|
|
18
|
+
];
|
|
19
|
+
|
|
1
20
|
import fs from 'fs';
|
|
2
21
|
import path from 'path';
|
|
3
22
|
import http from 'http';
|
|
4
23
|
import { spawn } from 'child_process';
|
|
5
|
-
import { routes } from '../core/Router.js';
|
|
6
|
-
import { renderLumpia } from '../core/View.js';
|
|
7
24
|
import { loadEnv } from '../core/Env.js';
|
|
8
25
|
import { loadConfig } from '../core/Config.js';
|
|
9
26
|
|
|
10
|
-
|
|
27
|
+
// ... Same helper functions (matchRoute, backgroundProcess, startTailwindWatcher) ...
|
|
11
28
|
|
|
12
|
-
// Helper to Match Routes
|
|
13
29
|
function matchRoute(definedRoute, method, pathname) {
|
|
14
30
|
if (definedRoute.method !== method) return null;
|
|
15
31
|
if (definedRoute.path === pathname) return { params: {} };
|
|
16
|
-
|
|
17
32
|
const paramNames = [];
|
|
18
33
|
const regexPath = definedRoute.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (match, name) => {
|
|
19
34
|
paramNames.push(name);
|
|
20
35
|
return '([^/]+)';
|
|
21
36
|
});
|
|
22
|
-
|
|
23
37
|
if (regexPath === definedRoute.path) return null;
|
|
24
38
|
const regex = new RegExp(`^${regexPath}$`);
|
|
25
39
|
const match = pathname.match(regex);
|
|
26
40
|
if (match) {
|
|
27
41
|
const params = {};
|
|
28
|
-
paramNames.forEach((name, index) => {
|
|
29
|
-
params[name] = match[index + 1];
|
|
30
|
-
});
|
|
42
|
+
paramNames.forEach((name, index) => { params[name] = match[index + 1]; });
|
|
31
43
|
return { params };
|
|
32
44
|
}
|
|
33
45
|
return null;
|
|
34
46
|
}
|
|
35
47
|
|
|
36
48
|
const backgroundProcess = [];
|
|
37
|
-
|
|
38
49
|
function startTailwindWatcher(root) {
|
|
39
|
-
console.log('🎨 TailwindCSS detected! Starting watcher...');
|
|
40
50
|
const cmd = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
41
|
-
const tailwind = spawn(cmd, [
|
|
42
|
-
'tailwindcss', '-i', './aset/css/style.css', '-o', './public/css/style.css', '--watch'
|
|
43
|
-
], { cwd: root, stdio: 'inherit', shell: true });
|
|
51
|
+
const tailwind = spawn(cmd, ['tailwindcss', '-i', './aset/css/style.css', '-o', './public/css/style.css', '--watch'], { cwd: root, stdio: 'ignore', shell: true });
|
|
44
52
|
backgroundProcess.push(tailwind);
|
|
45
53
|
}
|
|
46
54
|
|
|
47
|
-
function handleBootstrap(root) {
|
|
48
|
-
const bootDestDist = path.join(root, 'public', 'vendor', 'bootstrap');
|
|
49
|
-
const bootSrc = path.join(root, 'node_modules', 'bootstrap', 'dist');
|
|
50
|
-
if (fs.existsSync(bootSrc) && !fs.existsSync(bootDestDist)) {
|
|
51
|
-
console.log('📦 Menyalin library Bootstrap ke public...');
|
|
52
|
-
try { fs.cpSync(bootSrc, bootDestDist, { recursive: true }); } catch(e) {}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// LOADER UTAMA UNTUK .lmp (Controller/Model/Routes)
|
|
57
55
|
async function loadLumpiaModule(filePath) {
|
|
58
56
|
const originalContent = fs.readFileSync(filePath, 'utf-8');
|
|
59
57
|
const cacheDir = path.join(process.cwd(), '.lumpia', 'cache');
|
|
@@ -63,72 +61,45 @@ async function loadLumpiaModule(filePath) {
|
|
|
63
61
|
const flatName = relativePath.replace(/[\/\\]/g, '_').replace('.lmp', '.js');
|
|
64
62
|
const destPath = path.join(cacheDir, flatName);
|
|
65
63
|
|
|
66
|
-
// --- TRANSPILE LOGIC ---
|
|
67
64
|
let transcoded = originalContent;
|
|
68
|
-
|
|
69
|
-
// 1. Replace Imports: .lmp -> .js
|
|
70
65
|
transcoded = transcoded.replace(/from\s+['"](.+?)\.lmp['"]/g, "from '$1.js'");
|
|
71
66
|
|
|
72
|
-
// 2. SYNTAX LARAVEL -> JS (Fitur Request User: "->")
|
|
73
|
-
// Mengubah tanda panah "->" menjadi titik "."
|
|
74
|
-
// Hati-hati: Kita coba sebisa mungkin tidak mengubah "->" yang ada di dalam string.
|
|
75
|
-
// Regex ini mencari "->" yang TIDAK diapit kutip (Simpel, mungkin tidak 100% sempurna tapi cukup untuk have fun)
|
|
76
|
-
// Atau kita brute force saja asalkan user tahu resikonya.
|
|
77
|
-
// Kita gunakan pendekatan brute replace tapi hindari arrow function "=>" (aman karena beda karakter)
|
|
78
|
-
|
|
79
|
-
// Replace "->" dengan "."
|
|
80
67
|
transcoded = transcoded.split('\n').map(line => {
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
// Note: Ini akan mengubah string "go -> to" menjadi "go . to".
|
|
86
|
-
// Untuk framework "Have Fun", ini fitur, bukan bug. XD
|
|
87
|
-
return line.replace(/->/g, '.');
|
|
68
|
+
let l = line;
|
|
69
|
+
if (l.trim().startsWith('//')) return l;
|
|
70
|
+
KAMUS.forEach(rule => l = l.replace(rule.from, rule.to));
|
|
71
|
+
return l;
|
|
88
72
|
}).join('\n');
|
|
89
73
|
|
|
90
74
|
fs.writeFileSync(destPath, transcoded);
|
|
91
|
-
|
|
92
|
-
// 4. Import file .js
|
|
93
75
|
const module = await import('file://' + destPath + '?t=' + Date.now());
|
|
94
76
|
return module;
|
|
95
77
|
}
|
|
96
78
|
|
|
97
|
-
|
|
98
79
|
export async function serveProject() {
|
|
99
80
|
const root = process.cwd();
|
|
100
|
-
|
|
101
81
|
const routesFile = path.join(root, 'routes', 'web.lmp');
|
|
102
|
-
if (!fs.existsSync(routesFile)) return console.log("❌
|
|
82
|
+
if (!fs.existsSync(routesFile)) return console.log("❌ Missing routes/web.lmp");
|
|
103
83
|
|
|
104
84
|
const env = loadEnv(root);
|
|
105
85
|
const config = loadConfig(root);
|
|
106
|
-
|
|
107
86
|
if (config.klambi === 'tailwindcss') startTailwindWatcher(root);
|
|
108
|
-
else if (config.klambi === 'bootstrap') handleBootstrap(root);
|
|
109
87
|
|
|
110
88
|
try {
|
|
111
89
|
await loadLumpiaModule(routesFile);
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
console.log(`✨ Syntax Mode: PHP/Laravel Style (->) is enabled in .lmp files!`);
|
|
90
|
+
const activeRoutes = global.LumpiaRouter || [];
|
|
91
|
+
console.log(`🛣️ Routes registered: ${activeRoutes.length}`);
|
|
92
|
+
console.log(`✨ Mode: Semarangan (paten, ono, fungsi, aku)`);
|
|
116
93
|
|
|
117
94
|
const server = http.createServer(async (req, res) => {
|
|
118
95
|
const method = req.method;
|
|
119
96
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
120
97
|
const pathname = url.pathname;
|
|
121
98
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const publicMap = {
|
|
125
|
-
'/css/': path.join(root, 'public', 'css'),
|
|
126
|
-
'/vendor/': path.join(root, 'public', 'vendor')
|
|
127
|
-
};
|
|
99
|
+
const publicMap = { '/css/': path.join(root, 'public', 'css'), '/vendor/': path.join(root, 'public', 'vendor') };
|
|
128
100
|
for (const [prefix, localPath] of Object.entries(publicMap)) {
|
|
129
101
|
if (pathname.startsWith(prefix)) {
|
|
130
|
-
const
|
|
131
|
-
const filePath = path.join(localPath, relativePath);
|
|
102
|
+
const filePath = path.join(localPath, pathname.slice(prefix.length));
|
|
132
103
|
if (fs.existsSync(filePath) && fs.lstatSync(filePath).isFile()) {
|
|
133
104
|
const ext = path.extname(filePath);
|
|
134
105
|
const mime = ext === '.css' ? 'text/css' : (ext === '.js' ? 'text/javascript' : 'application/octet-stream');
|
|
@@ -139,71 +110,39 @@ export async function serveProject() {
|
|
|
139
110
|
}
|
|
140
111
|
}
|
|
141
112
|
|
|
142
|
-
let match = null;
|
|
143
|
-
|
|
144
|
-
for (const route of
|
|
113
|
+
let match = null, params = {};
|
|
114
|
+
const currentRoutes = global.LumpiaRouter || [];
|
|
115
|
+
for (const route of currentRoutes) {
|
|
145
116
|
const result = matchRoute(route, method, pathname);
|
|
146
|
-
if (result) {
|
|
147
|
-
match = route;
|
|
148
|
-
params = result.params;
|
|
149
|
-
break;
|
|
150
|
-
}
|
|
117
|
+
if (result) { match = route; params = result.params; break; }
|
|
151
118
|
}
|
|
152
119
|
|
|
153
120
|
if (match) {
|
|
154
121
|
try {
|
|
155
|
-
const [
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
if (!fs.existsSync(controllerPath)) throw new Error(`Controller ${controllerName} not found!`);
|
|
122
|
+
const [cName, mName] = match.action.split('@');
|
|
123
|
+
const cPath = path.join(root, 'app', 'controllers', cName + '.lmp');
|
|
124
|
+
if (!fs.existsSync(cPath)) throw new Error('Controller Not Found');
|
|
159
125
|
|
|
160
|
-
const module = await loadLumpiaModule(
|
|
161
|
-
const
|
|
162
|
-
const instance = new
|
|
126
|
+
const module = await loadLumpiaModule(cPath);
|
|
127
|
+
const Ctrl = module.default;
|
|
128
|
+
const instance = new Ctrl();
|
|
129
|
+
instance.env = env; instance.params = params; instance.config = config;
|
|
163
130
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (typeof instance[methodName] !== 'function') throw new Error(`Method ${methodName} missing`);
|
|
169
|
-
|
|
170
|
-
const args = Object.values(params);
|
|
171
|
-
const result = await instance[methodName](...args);
|
|
172
|
-
|
|
173
|
-
if (result.type === 'html') {
|
|
174
|
-
res.writeHead(200, {'Content-Type': 'text/html'});
|
|
175
|
-
res.end(result.content);
|
|
176
|
-
} else if (result.type === 'json') {
|
|
177
|
-
res.writeHead(200, {'Content-Type': 'application/json'});
|
|
131
|
+
const result = await instance[mName](...Object.values(params));
|
|
132
|
+
|
|
133
|
+
if (result && result.type) {
|
|
134
|
+
res.writeHead(200, {'Content-Type': result.type==='json'?'application/json':'text/html'});
|
|
178
135
|
res.end(result.content);
|
|
179
136
|
} else {
|
|
180
|
-
res.writeHead(200, {'Content-Type': 'text/plain'});
|
|
181
137
|
res.end(String(result));
|
|
182
138
|
}
|
|
183
139
|
} catch (e) {
|
|
184
|
-
|
|
185
|
-
const errorMsg = env.APP_DEBUG === 'true' ? `<pre>${e.stack}</pre>` : `<h1>Server Error</h1>`;
|
|
186
|
-
res.writeHead(500, {'Content-Type': 'text/html'});
|
|
187
|
-
res.end(errorMsg);
|
|
140
|
+
res.writeHead(500); res.end(`<pre>${e.stack}</pre>`);
|
|
188
141
|
}
|
|
189
142
|
} else {
|
|
190
|
-
res.writeHead(404
|
|
191
|
-
res.end('<h1>404 Not Found</h1>');
|
|
143
|
+
res.writeHead(404); res.end('404 Not Found');
|
|
192
144
|
}
|
|
193
145
|
});
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
server.listen(port, () => {
|
|
197
|
-
console.log(`🚀 Server running at ${env.BASE_URL || 'http://localhost:3000'}`);
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
process.on('SIGINT', () => {
|
|
201
|
-
backgroundProcess.forEach(p => p.kill());
|
|
202
|
-
try { fs.rmSync(path.join(root, '.lumpia'), { recursive: true, force: true }); } catch(e){}
|
|
203
|
-
process.exit();
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
} catch (err) {
|
|
207
|
-
console.error('Fatal Error:', err);
|
|
208
|
-
}
|
|
146
|
+
server.listen(3000, () => console.log('🚀 Server: http://localhost:3000'));
|
|
147
|
+
} catch (e) { console.error(e); }
|
|
209
148
|
}
|