lumpiajs 1.0.8 → 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 +39 -47
- package/lib/bridge/api.php +59 -0
- package/lib/commands/build.js +129 -202
- package/lib/commands/create.js +58 -138
- package/lib/commands/serve.js +51 -112
- package/lib/core/DBClient.js +54 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,72 +1,64 @@
|
|
|
1
1
|
# 🥟 LumpiaJS
|
|
2
2
|
|
|
3
|
-
**Bahasa Pemrograman Web dengan Kearifan Lokal Semarangan
|
|
3
|
+
**"Bahasa Pemrograman Web dengan Kearifan Lokal Semarangan."**
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
## 🦄 Fitur Unik: Laravel Syntax di JavaScript! (`->`)
|
|
8
|
-
|
|
9
|
-
```javascript
|
|
10
|
-
// Valid di LumpiaJS (.lmp)
|
|
11
|
-
const users = await DB.table('users')->where('active', 1)->get();
|
|
12
|
-
Jalan->get('/', 'HomeController@index');
|
|
13
|
-
```
|
|
5
|
+
Framework Static SPA 100% Client-Side. Coding pakai bahasa sehari-hari.
|
|
14
6
|
|
|
15
7
|
---
|
|
16
8
|
|
|
17
|
-
##
|
|
18
|
-
|
|
19
|
-
Ini yang sering ditanyain: **"Mas, file mana yang harus saya upload ke hosting?"**
|
|
9
|
+
## 🗣️ Kamus Bahasa
|
|
20
10
|
|
|
21
|
-
|
|
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 |
|
|
22
21
|
|
|
23
|
-
|
|
22
|
+
_Plus fitur **Laravel Syntax**: `aku->tampil()`._
|
|
24
23
|
|
|
25
|
-
|
|
24
|
+
**Contoh Coding (`HomeController.lmp`):**
|
|
26
25
|
|
|
27
|
-
```
|
|
28
|
-
|
|
26
|
+
```javascript
|
|
27
|
+
export default class HomeController extends Controller {
|
|
28
|
+
mengko index() {
|
|
29
|
+
paten pesan = 'Halo Lur!';
|
|
30
|
+
|
|
31
|
+
// Panggil fungsi view
|
|
32
|
+
balek aku->tampil('home', { msg: pesan });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
29
35
|
```
|
|
30
36
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
Sistem akan memasak projectmu:
|
|
34
|
-
|
|
35
|
-
- Mentranspile sintaks `->` menjadi JS standard.
|
|
36
|
-
- Mengkompilasi CSS (minify Tailwind/Bootstrap).
|
|
37
|
-
- Menyiapkan folder `dist` yang siap saji.
|
|
38
|
-
|
|
39
|
-
### 2. Upload ke Server
|
|
40
|
-
|
|
41
|
-
Setelah digoreng, akan muncul folder **`dist`**.
|
|
42
|
-
|
|
43
|
-
👉 **HANYA ISI FOLDER `dist`** inilah yang perlu kamu upload ke server.
|
|
44
|
-
(Isinya: `server.js`, `package.json`, `.env`, folder `app`, `routes`, `views`, `public`)
|
|
37
|
+
---
|
|
45
38
|
|
|
46
|
-
|
|
39
|
+
## 🚀 Cara Pakai
|
|
47
40
|
|
|
48
|
-
|
|
41
|
+
**1. Install**
|
|
49
42
|
|
|
50
43
|
```bash
|
|
51
|
-
|
|
52
|
-
|
|
44
|
+
npm install -g lumpiajs
|
|
45
|
+
```
|
|
53
46
|
|
|
54
|
-
|
|
55
|
-
npm install --production
|
|
47
|
+
**2. Buat Project & Develop**
|
|
56
48
|
|
|
57
|
-
|
|
58
|
-
|
|
49
|
+
```bash
|
|
50
|
+
lumpia create-project warung-ku
|
|
51
|
+
cd warung-ku && npm install
|
|
52
|
+
lumpia kukus
|
|
59
53
|
```
|
|
60
54
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
## 🗄️ Database
|
|
55
|
+
**3. Build Static (Goreng)**
|
|
64
56
|
|
|
65
|
-
|
|
57
|
+
```bash
|
|
58
|
+
lumpia goreng
|
|
59
|
+
```
|
|
66
60
|
|
|
67
|
-
|
|
68
|
-
2. Import file .sql itu ke database di server production kamu.
|
|
69
|
-
3. Edit file `.env` yang sudah diupload, sesuaikan `DB_HOST`, `DB_USER`, `DB_PASSWORD` dengan credential server.
|
|
61
|
+
Upload folder `dist` kemana saja (Hosting Biasa/GitHub Pages).
|
|
70
62
|
|
|
71
63
|
---
|
|
72
64
|
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
<?php
|
|
2
|
+
header("Access-Control-Allow-Origin: *");
|
|
3
|
+
header("Access-Control-Allow-Headers: Content-Type");
|
|
4
|
+
header("Content-Type: application/json");
|
|
5
|
+
|
|
6
|
+
// CONFIG
|
|
7
|
+
$host = "localhost";
|
|
8
|
+
$user = "root";
|
|
9
|
+
$pass = "";
|
|
10
|
+
$db = "lumpia_db";
|
|
11
|
+
|
|
12
|
+
// Koneksi
|
|
13
|
+
$conn = new mysqli($host, $user, $pass, $db);
|
|
14
|
+
if ($conn->connect_error) {
|
|
15
|
+
die(json_encode(["status" => "error", "message" => "Connection failed: " . $conn->connect_error]));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Ambil Query dari Request
|
|
19
|
+
$input = json_decode(file_get_contents('php://input'), true);
|
|
20
|
+
$sql = $input['sql'] ?? '';
|
|
21
|
+
$params = $input['params'] ?? [];
|
|
22
|
+
|
|
23
|
+
if (empty($sql)) {
|
|
24
|
+
echo json_encode(["status" => "error", "message" => "No SQL provided"]);
|
|
25
|
+
exit;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// SECURITY: Basic SQL Injection prevention?
|
|
29
|
+
// NO, karena ini adalah 'bridge' untuk client-side DB.table().
|
|
30
|
+
// User framework bertanggung jawab atas query-nya via binding params.
|
|
31
|
+
// TAPI INI SANGAT BERBAHAYA JIKA DIEKPOS KE PUBLIK TANPA AUTH.
|
|
32
|
+
// Untuk 'Have Fun' framework, kita biarkan dulu, tapi kasih warning.
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
$stmt = $conn->prepare($sql);
|
|
36
|
+
if($params) {
|
|
37
|
+
$types = str_repeat("s", count($params)); // Asumsikan string semua biar aman
|
|
38
|
+
$stmt->bind_param($types, ...$params);
|
|
39
|
+
}
|
|
40
|
+
$stmt->execute();
|
|
41
|
+
$result = $stmt->get_result();
|
|
42
|
+
|
|
43
|
+
$data = [];
|
|
44
|
+
if($result) {
|
|
45
|
+
while ($row = $result->fetch_assoc()) {
|
|
46
|
+
$data[] = $row;
|
|
47
|
+
}
|
|
48
|
+
} else {
|
|
49
|
+
// Non-select query
|
|
50
|
+
$data = ["affected_rows" => $stmt->affected_rows];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
echo json_encode(["status" => "success", "data" => $data]);
|
|
54
|
+
} catch (Exception $e) {
|
|
55
|
+
echo json_encode(["status" => "error", "message" => $e->getMessage()]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
$conn->close();
|
|
59
|
+
?>
|
package/lib/commands/build.js
CHANGED
|
@@ -1,49 +1,68 @@
|
|
|
1
|
-
import fs from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { spawnSync } from 'child_process';
|
|
4
|
-
import { loadConfig } from '../core/Config.js';
|
|
5
1
|
|
|
6
|
-
//
|
|
7
|
-
|
|
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) {
|
|
8
22
|
let code = content;
|
|
9
|
-
|
|
23
|
+
|
|
24
|
+
// Import .lmp -> .js
|
|
10
25
|
code = code.replace(/from\s+['"](.+?)\.lmp['"]/g, "from '$1.js'");
|
|
11
|
-
|
|
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
|
+
|
|
12
32
|
code = code.split('\n').map(line => {
|
|
13
|
-
|
|
14
|
-
|
|
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;
|
|
15
40
|
}).join('\n');
|
|
41
|
+
|
|
16
42
|
return code;
|
|
17
43
|
}
|
|
18
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
|
+
|
|
19
53
|
function processDirectory(source, dest) {
|
|
20
54
|
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
21
|
-
|
|
22
|
-
const items = fs.readdirSync(source);
|
|
23
|
-
items.forEach(item => {
|
|
55
|
+
fs.readdirSync(source).forEach(item => {
|
|
24
56
|
const srcPath = path.join(source, item);
|
|
25
57
|
const destPath = path.join(dest, item);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
if (stat.isDirectory()) {
|
|
58
|
+
if (fs.statSync(srcPath).isDirectory()) {
|
|
29
59
|
processDirectory(srcPath, destPath);
|
|
30
60
|
} else {
|
|
31
|
-
if (item.endsWith('.lmp')) {
|
|
32
|
-
// Transpile .lmp to .js
|
|
61
|
+
if (item.endsWith('.lmp') || item.endsWith('.js')) {
|
|
33
62
|
const content = fs.readFileSync(srcPath, 'utf8');
|
|
34
|
-
const jsContent =
|
|
35
|
-
const
|
|
36
|
-
fs.writeFileSync(
|
|
37
|
-
} else if (item.endsWith('.js') || item.endsWith('.json') || item.endsWith('.css') || item.endsWith('.html')) {
|
|
38
|
-
// Copy as is (but maybe transpile .js too for "->" support if mixed?)
|
|
39
|
-
// For safety, let's also transpile .js files just in case user used "->" there
|
|
40
|
-
if (item.endsWith('.js')) {
|
|
41
|
-
const content = fs.readFileSync(srcPath, 'utf8');
|
|
42
|
-
const jsContent = transpileContent(content);
|
|
43
|
-
fs.writeFileSync(destPath, jsContent);
|
|
44
|
-
} else {
|
|
45
|
-
fs.copyFileSync(srcPath, destPath);
|
|
46
|
-
}
|
|
63
|
+
const jsContent = transpileSemarangan(content);
|
|
64
|
+
const finalDest = destPath.replace('.lmp', '.js');
|
|
65
|
+
fs.writeFileSync(finalDest, jsContent);
|
|
47
66
|
} else {
|
|
48
67
|
fs.copyFileSync(srcPath, destPath);
|
|
49
68
|
}
|
|
@@ -51,196 +70,104 @@ function processDirectory(source, dest) {
|
|
|
51
70
|
});
|
|
52
71
|
}
|
|
53
72
|
|
|
54
|
-
const
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
paramNames.push(name);
|
|
73
|
-
return '([^/]+)';
|
|
74
|
-
});
|
|
75
|
-
if (regexPath === definedRoute.path) return null;
|
|
76
|
-
const regex = new RegExp('^' + regexPath + '$');
|
|
77
|
-
const match = pathname.match(regex);
|
|
78
|
-
if (match) {
|
|
79
|
-
const params = {};
|
|
80
|
-
paramNames.forEach((name, index) => params[name] = match[index + 1]);
|
|
81
|
-
return { params };
|
|
82
|
-
}
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
async function start() {
|
|
87
|
-
// 1. Load Routes (Compiled JS)
|
|
88
|
-
const routesUrl = path.join(root, 'routes', 'web.js');
|
|
89
|
-
if (fs.existsSync(routesUrl)) {
|
|
90
|
-
await import('file://' + routesUrl);
|
|
91
|
-
} else {
|
|
92
|
-
console.error("❌ Error: routes/web.js not found in build!");
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// 2. Start Server
|
|
96
|
-
const server = http.createServer(async (req, res) => {
|
|
97
|
-
const method = req.method;
|
|
98
|
-
const url = new URL(req.url, 'http://' + req.headers.host);
|
|
99
|
-
const pathname = url.pathname;
|
|
100
|
-
|
|
101
|
-
// Static Files
|
|
102
|
-
const publicMap = {
|
|
103
|
-
'/css/': path.join(root, 'public', 'css'),
|
|
104
|
-
'/vendor/': path.join(root, 'public', 'vendor')
|
|
105
|
-
};
|
|
106
|
-
for (const [prefix, localPath] of Object.entries(publicMap)) {
|
|
107
|
-
if (pathname.startsWith(prefix)) {
|
|
108
|
-
const relativePath = pathname.slice(prefix.length);
|
|
109
|
-
const filePath = path.join(localPath, relativePath);
|
|
110
|
-
if (fs.existsSync(filePath) && fs.lstatSync(filePath).isFile()) {
|
|
111
|
-
const ext = path.extname(filePath);
|
|
112
|
-
const mime = ext === '.css' ? 'text/css' : (ext === '.js' ? 'text/javascript' : 'application/octet-stream');
|
|
113
|
-
res.writeHead(200, {'Content-Type': mime});
|
|
114
|
-
res.end(fs.readFileSync(filePath));
|
|
115
|
-
return;
|
|
116
|
-
}
|
|
73
|
+
const browserCore = `
|
|
74
|
+
export class Controller {
|
|
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);
|
|
117
91
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
for (const route of routes) {
|
|
124
|
-
const result = matchRoute(route, method, pathname);
|
|
125
|
-
if (result) {
|
|
126
|
-
match = route;
|
|
127
|
-
params = result.params;
|
|
128
|
-
break;
|
|
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);
|
|
129
97
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
instance.env = env;
|
|
142
|
-
instance.params = params;
|
|
143
|
-
instance.config = config;
|
|
144
|
-
|
|
145
|
-
const result = await instance[methodName](...Object.values(params));
|
|
146
|
-
if (result.type === 'html') {
|
|
147
|
-
res.writeHead(200, {'Content-Type': 'text/html'});
|
|
148
|
-
res.end(result.content);
|
|
149
|
-
} else if (result.type === 'json') {
|
|
150
|
-
res.writeHead(200, {'Content-Type': 'application/json'});
|
|
151
|
-
res.end(result.content);
|
|
152
|
-
} else {
|
|
153
|
-
res.writeHead(200, {'Content-Type': 'text/plain'});
|
|
154
|
-
res.end(String(result));
|
|
155
|
-
}
|
|
156
|
-
} catch (e) {
|
|
157
|
-
console.error(e);
|
|
158
|
-
res.writeHead(500, {'Content-Type': 'text/html'});
|
|
159
|
-
res.end('<h1>500 Server Error</h1>');
|
|
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)();
|
|
160
109
|
}
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
res.end('404 Not Found');
|
|
164
|
-
}
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
const port = env.PORT || 3000;
|
|
168
|
-
server.listen(port, () => {
|
|
169
|
-
console.log('🚀 Production Server running on port ' + port);
|
|
170
|
-
});
|
|
110
|
+
} catch(e) { document.getElementById('app').innerHTML = e.message; }
|
|
111
|
+
}
|
|
171
112
|
}
|
|
172
|
-
|
|
173
|
-
start();
|
|
113
|
+
export const Jalan = { routes:[], get:(p,a)=>Jalan.routes.push({p,a}) };
|
|
174
114
|
`;
|
|
175
115
|
|
|
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; }
|
|
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'; }
|
|
138
|
+
}
|
|
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();
|
|
143
|
+
}
|
|
144
|
+
});
|
|
145
|
+
navigate();
|
|
146
|
+
</script></body></html>`;
|
|
147
|
+
|
|
176
148
|
export function buildProject() {
|
|
177
149
|
const root = process.cwd();
|
|
178
150
|
const dist = path.join(root, 'dist');
|
|
179
151
|
const config = loadConfig(root);
|
|
180
|
-
|
|
181
|
-
console.log('🍳
|
|
182
|
-
|
|
183
|
-
// 1. Cleanup Old Dist
|
|
184
|
-
if (fs.existsSync(dist)) {
|
|
185
|
-
fs.rmSync(dist, { recursive: true, force: true });
|
|
186
|
-
}
|
|
152
|
+
|
|
153
|
+
console.log('🍳 Goreng Project (Mode Bahasa Semarangan)...');
|
|
154
|
+
if (fs.existsSync(dist)) fs.rmSync(dist, { recursive: true, force: true });
|
|
187
155
|
fs.mkdirSync(dist);
|
|
188
156
|
|
|
189
|
-
// 2. Build Assets (Tailwind)
|
|
190
157
|
if (config.klambi === 'tailwindcss') {
|
|
191
|
-
console.log('🎨 Compiling Tailwind CSS (Minified)...');
|
|
192
158
|
const cmd = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
193
|
-
spawnSync(cmd, ['tailwindcss', '-i', './aset/css/style.css', '-o', './public/css/style.css', '--minify'], {
|
|
194
|
-
cwd: root, stdio: 'inherit', shell: true
|
|
195
|
-
});
|
|
159
|
+
spawnSync(cmd, ['tailwindcss', '-i', './aset/css/style.css', '-o', './public/css/style.css', '--minify'], { cwd: root, stdio: 'ignore', shell: true });
|
|
196
160
|
}
|
|
197
161
|
|
|
198
|
-
// 3. Copy & Transpile Code
|
|
199
|
-
console.log('📂 Copying & Transpiling (.lmp -> .js)...');
|
|
200
162
|
processDirectory(path.join(root, 'app'), path.join(dist, 'app'));
|
|
201
163
|
processDirectory(path.join(root, 'routes'), path.join(dist, 'routes'));
|
|
202
|
-
processDirectory(path.join(root, 'views'), path.join(dist, 'views')); // Views .lmp usually not imported, but kept as is? OR Transpiled?
|
|
203
|
-
// Wait, View.js reads raw .lmp file content. So views should strictly be copied AS IS, or renamed to .lmp but content untouched generally?
|
|
204
|
-
// Actually View.js `renderLumpia` expects file path.
|
|
205
|
-
// Let's COPY views folder AS IS (recursive copy), no renaming extensions usually needed for View Engine unless we change View.js to look for .html?
|
|
206
|
-
// Lumpia View Engine expects `<lump>` tags.
|
|
207
|
-
// Let's just copy views folder using simple copy to ensure .lmp extension stays for view engine to find it.
|
|
208
|
-
|
|
209
|
-
// RE-DO views copy: FORCE copy only
|
|
210
|
-
// Overwrite the 'processDirectory' for views to be simple copy
|
|
211
164
|
fs.cpSync(path.join(root, 'views'), path.join(dist, 'views'), { recursive: true });
|
|
165
|
+
if (fs.existsSync(path.join(root, 'public'))) fs.cpSync(path.join(root, 'public'), path.join(dist, 'public'), { recursive: true });
|
|
212
166
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
// 5. Configs & Env
|
|
219
|
-
fs.copyFileSync(path.join(root, 'package.json'), path.join(dist, 'package.json'));
|
|
220
|
-
fs.copyFileSync(path.join(root, 'config.lmp'), path.join(dist, 'config.lmp'));
|
|
221
|
-
if (fs.existsSync(path.join(root, '.env'))) {
|
|
222
|
-
fs.copyFileSync(path.join(root, '.env'), path.join(dist, '.env'));
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// 6. Generate Standalone Server Entry
|
|
226
|
-
fs.writeFileSync(path.join(dist, 'server.js'), serverScript);
|
|
227
|
-
|
|
228
|
-
// 7. Update package.json in dist
|
|
229
|
-
const pkg = JSON.parse(fs.readFileSync(path.join(dist, 'package.json'), 'utf8'));
|
|
230
|
-
pkg.scripts = {
|
|
231
|
-
"start": "node server.js"
|
|
232
|
-
};
|
|
233
|
-
// Ensure lumpiajs dependency is preserved
|
|
234
|
-
fs.writeFileSync(path.join(dist, 'package.json'), JSON.stringify(pkg, 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>`);
|
|
235
171
|
|
|
236
|
-
console.log('✅ Mateng! (
|
|
237
|
-
console.log('----------------------------------------------------');
|
|
238
|
-
console.log('🎁 Yang harus dikirim ke Server (Production):');
|
|
239
|
-
console.log(' 📂 Folder: dist/');
|
|
240
|
-
console.log('');
|
|
241
|
-
console.log('👉 Cara Deploy:');
|
|
242
|
-
console.log(' 1. Upload isi folder "dist" ke server.');
|
|
243
|
-
console.log(' 2. Jalankan "npm install --production" di server.');
|
|
244
|
-
console.log(' 3. Jalankan "npm start".');
|
|
245
|
-
console.log('----------------------------------------------------');
|
|
172
|
+
console.log('✅ Mateng! (Support Bahasa Semarangan)');
|
|
246
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
|
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export class DB {
|
|
2
|
+
static async query(sql, params = []) {
|
|
3
|
+
// Cek mode: Node.js (Server) atau Browser (Client - Build HTML)
|
|
4
|
+
if (typeof window === 'undefined') {
|
|
5
|
+
// --- SERVER SIDE (NODE) ---
|
|
6
|
+
// Import native mysql driver dynamically to avoid bundle errors in browser
|
|
7
|
+
const { createPool } = await import('mysql2/promise');
|
|
8
|
+
// ... logic koneksi nodejs lama ...
|
|
9
|
+
return []; // placeholder
|
|
10
|
+
} else {
|
|
11
|
+
// --- CLIENT SIDE (BROWSER) ---
|
|
12
|
+
// Tembak ke file PHP Bridge
|
|
13
|
+
const response = await fetch('/api.php', {
|
|
14
|
+
method: 'POST',
|
|
15
|
+
headers: { 'Content-Type': 'application/json' },
|
|
16
|
+
body: JSON.stringify({ sql, params })
|
|
17
|
+
});
|
|
18
|
+
const json = await response.json();
|
|
19
|
+
if(json.status === 'error') throw new Error(json.message);
|
|
20
|
+
return json.data;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static table(name) {
|
|
25
|
+
return new QueryBuilder(name);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class QueryBuilder {
|
|
30
|
+
constructor(table) {
|
|
31
|
+
this.tableName = table;
|
|
32
|
+
this.conditions = [];
|
|
33
|
+
this.bindings = [];
|
|
34
|
+
this.selects = '*';
|
|
35
|
+
this.limitVal = null;
|
|
36
|
+
this.orderByRaw = null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
select(fields) { this.selects = fields; return this; }
|
|
40
|
+
where(col, op, val) { if (val === undefined) { val = op; op = '='; } this.conditions.push(`${col} ${op} ?`); this.bindings.push(val); return this; }
|
|
41
|
+
orderBy(col, dir='ASC') { this.orderByRaw = `${col} ${dir}`; return this; }
|
|
42
|
+
take(n) { this.limitVal = n; return this; }
|
|
43
|
+
|
|
44
|
+
async get() {
|
|
45
|
+
let sql = `SELECT ${this.selects} FROM ${this.tableName}`;
|
|
46
|
+
if (this.conditions.length > 0) sql += ' WHERE ' + this.conditions.join(' AND ');
|
|
47
|
+
if (this.orderByRaw) sql += ' ORDER BY ' + this.orderByRaw;
|
|
48
|
+
if (this.limitVal) sql += ' LIMIT ' + this.limitVal;
|
|
49
|
+
|
|
50
|
+
return await DB.query(sql, this.bindings);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ... insert/update/delete logic similar ...
|
|
54
|
+
}
|