lumpiajs 1.0.8 → 1.0.9
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 +22 -48
- package/lib/bridge/api.php +59 -0
- package/lib/commands/build.js +296 -162
- package/lib/core/DBClient.js +54 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,69 +4,43 @@
|
|
|
4
4
|
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
## 🦄
|
|
7
|
+
## 🦄 Deployment Ajaib (Universal)
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
// Valid di LumpiaJS (.lmp)
|
|
11
|
-
const users = await DB.table('users')->where('active', 1)->get();
|
|
12
|
-
Jalan->get('/', 'HomeController@index');
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
## 🏗️ Cara Deploy ke Production (Server Asli)
|
|
9
|
+
Ini fitur andalan LumpiaJS. Satu folder build (`dist`) bisa jalan di mana saja tanpa ubah kodingan.
|
|
18
10
|
|
|
19
|
-
|
|
11
|
+
User (Browser) akan menembak alamat `/api`.
|
|
20
12
|
|
|
21
|
-
|
|
13
|
+
- Jika di **Hosting PHP**, server otomatis mengarahkan ke `api.php`.
|
|
14
|
+
- Jika di **Vercel/Node**, server otomatis mengarahkan ke `api.js`.
|
|
22
15
|
|
|
23
|
-
### 1.
|
|
24
|
-
|
|
25
|
-
Jalankan perintah ini di komputermu:
|
|
16
|
+
### 1. Build Project
|
|
26
17
|
|
|
27
18
|
```bash
|
|
28
19
|
lumpia goreng
|
|
29
20
|
```
|
|
30
21
|
|
|
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
|
|
22
|
+
### 2. Panduan Deploy
|
|
40
23
|
|
|
41
|
-
|
|
24
|
+
**A. Hosting PHP / XAMPP (Apache)**
|
|
42
25
|
|
|
43
|
-
|
|
44
|
-
|
|
26
|
+
1. Copy `dist` ke server.
|
|
27
|
+
2. Edit **`api.php`** (Isi config database).
|
|
28
|
+
3. Selesai.
|
|
29
|
+
_Server otomatis pakai `.htaccess` untuk routing._
|
|
45
30
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
Di panel hosting (Terminal/SSH) atau VPS:
|
|
49
|
-
|
|
50
|
-
```bash
|
|
51
|
-
# Masuk ke folder yang barusan diupload
|
|
52
|
-
cd /path/to/your/app
|
|
53
|
-
|
|
54
|
-
# Install dependencies (LumpiaJS core, mysql driver, dll)
|
|
55
|
-
npm install --production
|
|
56
|
-
|
|
57
|
-
# Jalankan Aplikasi
|
|
58
|
-
npm start
|
|
59
|
-
```
|
|
60
|
-
|
|
61
|
-
---
|
|
31
|
+
**B. Vercel (Gratis)**
|
|
62
32
|
|
|
63
|
-
|
|
33
|
+
1. Drag `dist` ke Vercel (atau push git).
|
|
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._
|
|
64
37
|
|
|
65
|
-
|
|
38
|
+
**C. VPS (Node.js)**
|
|
66
39
|
|
|
67
|
-
1.
|
|
68
|
-
2.
|
|
69
|
-
3.
|
|
40
|
+
1. Upload `dist`.
|
|
41
|
+
2. `npm install`
|
|
42
|
+
3. `npm start`
|
|
43
|
+
_Node akan menjalankan `server.js`._
|
|
70
44
|
|
|
71
45
|
---
|
|
72
46
|
|
|
@@ -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,18 +1,21 @@
|
|
|
1
|
+
|
|
1
2
|
import fs from 'fs';
|
|
2
3
|
import path from 'path';
|
|
3
4
|
import { spawnSync } from 'child_process';
|
|
4
5
|
import { loadConfig } from '../core/Config.js';
|
|
5
6
|
|
|
6
|
-
//
|
|
7
|
+
// --- HELPERS ---
|
|
7
8
|
function transpileContent(content) {
|
|
8
9
|
let code = content;
|
|
9
|
-
// 1. Import: .lmp -> .js
|
|
10
10
|
code = code.replace(/from\s+['"](.+?)\.lmp['"]/g, "from '$1.js'");
|
|
11
|
-
// 2. Syntax: "->" -> "."
|
|
12
11
|
code = code.split('\n').map(line => {
|
|
13
12
|
if (line.trim().startsWith('//')) return line;
|
|
14
13
|
return line.replace(/->/g, '.');
|
|
15
14
|
}).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'");
|
|
16
19
|
return code;
|
|
17
20
|
}
|
|
18
21
|
|
|
@@ -29,21 +32,14 @@ function processDirectory(source, dest) {
|
|
|
29
32
|
processDirectory(srcPath, destPath);
|
|
30
33
|
} else {
|
|
31
34
|
if (item.endsWith('.lmp')) {
|
|
32
|
-
// Transpile .lmp to .js
|
|
33
35
|
const content = fs.readFileSync(srcPath, 'utf8');
|
|
34
36
|
const jsContent = transpileContent(content);
|
|
35
37
|
const jsDest = destPath.replace('.lmp', '.js');
|
|
36
38
|
fs.writeFileSync(jsDest, jsContent);
|
|
37
|
-
} else if (item.endsWith('.js')
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
}
|
|
39
|
+
} else if (item.endsWith('.js')) {
|
|
40
|
+
const content = fs.readFileSync(srcPath, 'utf8');
|
|
41
|
+
const jsContent = transpileContent(content);
|
|
42
|
+
fs.writeFileSync(destPath, jsContent);
|
|
47
43
|
} else {
|
|
48
44
|
fs.copyFileSync(srcPath, destPath);
|
|
49
45
|
}
|
|
@@ -51,196 +47,334 @@ function processDirectory(source, dest) {
|
|
|
51
47
|
});
|
|
52
48
|
}
|
|
53
49
|
|
|
54
|
-
|
|
50
|
+
// --- 1. PHP BACKEND ADAPTER ---
|
|
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 = `
|
|
55
130
|
import http from 'http';
|
|
56
131
|
import fs from 'fs';
|
|
57
132
|
import path from 'path';
|
|
58
|
-
import
|
|
59
|
-
import { loadEnv } from 'lumpiajs/lib/core/Env.js';
|
|
60
|
-
import { loadConfig } from 'lumpiajs/lib/core/Config.js';
|
|
133
|
+
import apiHandler from './api.js';
|
|
61
134
|
|
|
62
135
|
const root = process.cwd();
|
|
63
|
-
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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 };
|
|
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;
|
|
82
149
|
}
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
150
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
console.error("❌ Error: routes/web.js not found in build!");
|
|
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');
|
|
93
157
|
}
|
|
94
158
|
|
|
95
|
-
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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 = `
|
|
171
|
+
export class Controller {
|
|
172
|
+
constructor() { this.env = {}; this.params = {}; }
|
|
173
|
+
|
|
174
|
+
async tampil(viewName, data = {}) {
|
|
175
|
+
const response = await fetch('/views/' + viewName + '.lmp');
|
|
176
|
+
let html = await response.text();
|
|
177
|
+
|
|
178
|
+
const matchKulit = html.match(/<kulit>([\\s\\S]*?)<\\/kulit>/);
|
|
179
|
+
const matchIsi = html.match(/<isi>([\\s\\S]*?)<\\/isi>/);
|
|
180
|
+
const matchKlambi = html.match(/<klambi>([\\s\\S]*?)<\\/klambi>/);
|
|
181
|
+
|
|
182
|
+
let body = matchKulit ? matchKulit[1] : '';
|
|
183
|
+
let script = matchIsi ? matchIsi[1] : '';
|
|
184
|
+
let css = matchKlambi ? matchKlambi[1] : '';
|
|
185
|
+
|
|
186
|
+
for (const [key, value] of Object.entries(data)) {
|
|
187
|
+
const regex = new RegExp('{{\\\\s*' + key + '\\\\s*}}', 'g');
|
|
188
|
+
body = body.replace(regex, value);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
document.getElementById('app').innerHTML = body;
|
|
192
|
+
|
|
193
|
+
if(css) {
|
|
194
|
+
const style = document.createElement('style');
|
|
195
|
+
style.textContent = css;
|
|
196
|
+
document.head.appendChild(style);
|
|
118
197
|
}
|
|
198
|
+
|
|
199
|
+
const dict = [
|
|
200
|
+
{ asal: /ono\\s/g, jadi: 'let ' }, { asal: /paten\\s/g, jadi: 'const ' },
|
|
201
|
+
{ asal: /gawe\\s/g, jadi: 'function ' }, { asal: /yen\\s/g, jadi: 'if ' },
|
|
202
|
+
{ asal: /liyane/g, jadi: 'else' }, { asal: /mandek;/g, jadi: 'return;' },
|
|
203
|
+
{ asal: /ora\\s/g, jadi: '!' },
|
|
204
|
+
];
|
|
205
|
+
dict.forEach(k => script = script.replace(k.asal, k.jadi));
|
|
206
|
+
|
|
207
|
+
try { new Function(script)(); } catch(e) { console.error("Error script <isi>:", e); }
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
json(data) { document.getElementById('app').innerText = JSON.stringify(data, null, 2); }
|
|
211
|
+
}
|
|
119
212
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
213
|
+
// POLYMORPHIC DB CLIENT
|
|
214
|
+
export class DB {
|
|
215
|
+
static table(name) { return new QueryBuilder(name); }
|
|
216
|
+
static async query(sql, params) {
|
|
217
|
+
// Tembak ke endpoint /api
|
|
218
|
+
// Server (Apache/Vercel/Node) yang akan nentuin diteruske ke api.php atau api.js
|
|
219
|
+
const res = await fetch('/api', {
|
|
220
|
+
method: 'POST',
|
|
221
|
+
headers: {'Content-Type': 'application/json'},
|
|
222
|
+
body: JSON.stringify({sql, params})
|
|
223
|
+
});
|
|
224
|
+
return await res.json();
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
class QueryBuilder {
|
|
229
|
+
constructor(table) { this.table = table; this.conds = []; this.binds = []; }
|
|
230
|
+
where(c, o, v) { if(v===undefined){v=o;o='=';} this.conds.push(c+' '+o+' ?'); this.binds.push(v); return this; }
|
|
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);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
export const Jalan = {
|
|
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; }
|
|
129
269
|
}
|
|
130
|
-
}
|
|
131
270
|
|
|
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));
|
|
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>';
|
|
155
280
|
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
res.writeHead(500, {'Content-Type': 'text/html'});
|
|
159
|
-
res.end('<h1>500 Server Error</h1>');
|
|
281
|
+
} else {
|
|
282
|
+
document.getElementById('app').innerHTML = '<h1>404</h1>';
|
|
160
283
|
}
|
|
161
|
-
} else {
|
|
162
|
-
res.writeHead(404);
|
|
163
|
-
res.end('404 Not Found');
|
|
164
284
|
}
|
|
165
|
-
});
|
|
166
285
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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>`;
|
|
172
298
|
|
|
173
|
-
start();
|
|
174
|
-
`;
|
|
175
299
|
|
|
176
300
|
export function buildProject() {
|
|
177
301
|
const root = process.cwd();
|
|
178
302
|
const dist = path.join(root, 'dist');
|
|
179
303
|
const config = loadConfig(root);
|
|
180
304
|
|
|
181
|
-
console.log('🍳 Mulai Menggoreng (
|
|
305
|
+
console.log('🍳 Mulai Menggoreng (Universal Hybrid Build)...');
|
|
182
306
|
|
|
183
|
-
|
|
184
|
-
if (fs.existsSync(dist)) {
|
|
185
|
-
fs.rmSync(dist, { recursive: true, force: true });
|
|
186
|
-
}
|
|
307
|
+
if (fs.existsSync(dist)) fs.rmSync(dist, { recursive: true, force: true });
|
|
187
308
|
fs.mkdirSync(dist);
|
|
188
309
|
|
|
189
|
-
// 2. Build Assets (Tailwind)
|
|
190
310
|
if (config.klambi === 'tailwindcss') {
|
|
191
|
-
console.log('🎨 Compiling Tailwind CSS (Minified)...');
|
|
192
311
|
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
|
-
});
|
|
312
|
+
spawnSync(cmd, ['tailwindcss', '-i', './aset/css/style.css', '-o', './public/css/style.css', '--minify'], { cwd: root, stdio: 'ignore', shell: true });
|
|
196
313
|
}
|
|
197
314
|
|
|
198
|
-
|
|
199
|
-
console.log('📂 Copying & Transpiling (.lmp -> .js)...');
|
|
315
|
+
console.log('📂 Converting Code...');
|
|
200
316
|
processDirectory(path.join(root, 'app'), path.join(dist, 'app'));
|
|
201
317
|
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
318
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
fs.cpSync(path.join(root, 'views'), path.join(dist, 'views'), { recursive: true });
|
|
319
|
+
fs.mkdirSync(path.join(dist, 'core'), { recursive: true });
|
|
320
|
+
fs.writeFileSync(path.join(dist, 'core', 'index.js'), browserCoreIndex);
|
|
212
321
|
|
|
213
|
-
|
|
322
|
+
fs.cpSync(path.join(root, 'views'), path.join(dist, 'views'), { recursive: true });
|
|
323
|
+
|
|
214
324
|
if (fs.existsSync(path.join(root, 'public'))) {
|
|
215
325
|
fs.cpSync(path.join(root, 'public'), path.join(dist, 'public'), { recursive: true });
|
|
216
326
|
}
|
|
327
|
+
|
|
328
|
+
fs.writeFileSync(path.join(dist, 'index.html'), indexHtmlContent);
|
|
217
329
|
|
|
218
|
-
//
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
330
|
+
// --- GENERATE ALL ADAPTERS ---
|
|
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));
|
|
224
343
|
|
|
225
|
-
//
|
|
226
|
-
|
|
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
|
+
`);
|
|
227
361
|
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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));
|
|
235
372
|
|
|
236
|
-
console.log('✅ Mateng! (Build
|
|
373
|
+
console.log('✅ Mateng! (Universal Build)');
|
|
237
374
|
console.log('----------------------------------------------------');
|
|
238
|
-
console.log('
|
|
239
|
-
console.log('
|
|
240
|
-
console.log('');
|
|
241
|
-
console.log('
|
|
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".');
|
|
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)');
|
|
245
379
|
console.log('----------------------------------------------------');
|
|
246
380
|
}
|
|
@@ -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
|
+
}
|