lumpiajs 1.0.7 → 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 +24 -128
- package/bin/lumpia.js +2 -7
- package/index.js +3 -1
- package/lib/bridge/api.php +59 -0
- package/lib/commands/build.js +380 -0
- package/lib/commands/create.js +146 -99
- package/lib/commands/serve.js +109 -65
- package/lib/core/Config.js +20 -0
- package/lib/core/DB.js +139 -0
- package/lib/core/DBClient.js +54 -0
- package/lib/core/View.js +21 -8
- package/package.json +4 -2
- package/templates/gitignore.txt +3 -0
package/README.md
CHANGED
|
@@ -1,151 +1,47 @@
|
|
|
1
1
|
# 🥟 LumpiaJS
|
|
2
2
|
|
|
3
|
-
**Bahasa Pemrograman Web dengan Kearifan Lokal Semarangan.**
|
|
4
|
-
Framework ini hadir sebagai solusi "Have Fun" bagi developer. Meskipun gayanya santai, LumpiaJS mengadopsi arsitektur **MVC (Model-View-Controller)** yang mirip banget sama framework sebelah (uhuk, Laravel). Jadi kamu bisa pakai struktur yang profesional!
|
|
3
|
+
**Bahasa Pemrograman Web dengan Kearifan Lokal Semarangan.**
|
|
5
4
|
|
|
6
5
|
---
|
|
7
6
|
|
|
8
|
-
##
|
|
7
|
+
## 🦄 Deployment Ajaib (Universal)
|
|
9
8
|
|
|
10
|
-
|
|
9
|
+
Ini fitur andalan LumpiaJS. Satu folder build (`dist`) bisa jalan di mana saja tanpa ubah kodingan.
|
|
11
10
|
|
|
12
|
-
|
|
11
|
+
User (Browser) akan menembak alamat `/api`.
|
|
13
12
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
```
|
|
13
|
+
- Jika di **Hosting PHP**, server otomatis mengarahkan ke `api.php`.
|
|
14
|
+
- Jika di **Vercel/Node**, server otomatis mengarahkan ke `api.js`.
|
|
17
15
|
|
|
18
|
-
|
|
16
|
+
### 1. Build Project
|
|
19
17
|
|
|
20
18
|
```bash
|
|
21
|
-
lumpia
|
|
19
|
+
lumpia goreng
|
|
22
20
|
```
|
|
23
21
|
|
|
24
|
-
|
|
22
|
+
### 2. Panduan Deploy
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
cd warung-ku && npm install
|
|
28
|
-
```
|
|
24
|
+
**A. Hosting PHP / XAMPP (Apache)**
|
|
29
25
|
|
|
30
|
-
|
|
26
|
+
1. Copy `dist` ke server.
|
|
27
|
+
2. Edit **`api.php`** (Isi config database).
|
|
28
|
+
3. Selesai.
|
|
29
|
+
_Server otomatis pakai `.htaccess` untuk routing._
|
|
31
30
|
|
|
32
|
-
|
|
33
|
-
lumpia kukus
|
|
34
|
-
```
|
|
31
|
+
**B. Vercel (Gratis)**
|
|
35
32
|
|
|
36
|
-
|
|
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._
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
## ⚙️ Konfigurasi Environment (.env)
|
|
38
|
+
**C. VPS (Node.js)**
|
|
41
39
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
APP_ENV="local"
|
|
47
|
-
APP_DEBUG="true"
|
|
48
|
-
```
|
|
40
|
+
1. Upload `dist`.
|
|
41
|
+
2. `npm install`
|
|
42
|
+
3. `npm start`
|
|
43
|
+
_Node akan menjalankan `server.js`._
|
|
49
44
|
|
|
50
45
|
---
|
|
51
46
|
|
|
52
|
-
## 🏗️ Struktur Project (Standar MVC)
|
|
53
|
-
|
|
54
|
-
```
|
|
55
|
-
warung-ku/
|
|
56
|
-
├── app/
|
|
57
|
-
│ ├── controllers/ # Otak Logika (Controller)
|
|
58
|
-
│ └── models/ # Pengolah Data (Model)
|
|
59
|
-
├── routes/
|
|
60
|
-
│ └── web.js # Rute URL (Jalur Akses)
|
|
61
|
-
├── views/ # Tampilan (.lmp)
|
|
62
|
-
├── .env # File Konfigurasi (Environment)
|
|
63
|
-
├── package.json
|
|
64
|
-
└── ...
|
|
65
|
-
```
|
|
66
|
-
|
|
67
|
-
### 1. Routes (`routes/web.js`)
|
|
68
|
-
|
|
69
|
-
Mengatur jalur URL. Sekarang sudah support parameter dinamis seperti Laravel!
|
|
70
|
-
|
|
71
|
-
```javascript
|
|
72
|
-
import { Jalan } from "lumpiajs";
|
|
73
|
-
|
|
74
|
-
// Basic GET
|
|
75
|
-
Jalan.get("/", "HomeController@index");
|
|
76
|
-
|
|
77
|
-
// API (POST/PUT/DELETE)
|
|
78
|
-
Jalan.post("/api/products", "ProductController@store");
|
|
79
|
-
Jalan.put("/api/products/{id}", "ProductController@update");
|
|
80
|
-
Jalan.delete("/api/products/{id}", "ProductController@destroy");
|
|
81
|
-
|
|
82
|
-
// Dynamic Route dengan Parameter
|
|
83
|
-
Jalan.get("/produk/{id}", "ProductController@show");
|
|
84
|
-
Jalan.get("/kategori/{slug}", "CategoryController@index");
|
|
85
|
-
```
|
|
86
|
-
|
|
87
|
-
### 2. Controllers (`app/controllers`)
|
|
88
|
-
|
|
89
|
-
Otak dari aplikasimu. Parameter dari route (misal `{id}`) otomatis masuk jadi argumen fungsi.
|
|
90
|
-
|
|
91
|
-
```javascript
|
|
92
|
-
import { Controller } from "lumpiajs";
|
|
93
|
-
|
|
94
|
-
export default class ProductController extends Controller {
|
|
95
|
-
// Menangkap parameter {id} dari route
|
|
96
|
-
show(id) {
|
|
97
|
-
return this.json({
|
|
98
|
-
pesan: "Menampilkan produk dengan ID: " + id,
|
|
99
|
-
id: id,
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
index() {
|
|
104
|
-
return this.tampil("home", { env: this.env.APP_ENV });
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
```
|
|
108
|
-
|
|
109
|
-
### 3. Views (`views`)
|
|
110
|
-
|
|
111
|
-
File berekstensi `.lmp`.
|
|
112
|
-
|
|
113
|
-
```html
|
|
114
|
-
<lump>
|
|
115
|
-
<klambi> h1 { color: red; } </klambi>
|
|
116
|
-
<kulit> <h1>{{ pesan }}</h1> </kulit>
|
|
117
|
-
<isi> gawe sapa() { alert("Halo!"); } </isi>
|
|
118
|
-
</lump>
|
|
119
|
-
```
|
|
120
|
-
|
|
121
|
-
### 4. Models (`app/models`)
|
|
122
|
-
|
|
123
|
-
```javascript
|
|
124
|
-
import { Model } from "lumpiajs";
|
|
125
|
-
// Model.use(data).dimana('harga', '<', 5000).kabeh();
|
|
126
|
-
```
|
|
127
|
-
|
|
128
|
-
---
|
|
129
|
-
|
|
130
|
-
## 🧐 Kamus Bahasa (Transpiler)
|
|
131
|
-
|
|
132
|
-
Gunakan istilah Semarangan ini di dalam tag `<isi>` file `.lmp`:
|
|
133
|
-
|
|
134
|
-
| Bahasa Semarangan | JavaScript Asli |
|
|
135
|
-
| :---------------- | :-------------- |
|
|
136
|
-
| `ono` | `let` |
|
|
137
|
-
| `paten` | `const` |
|
|
138
|
-
| `gawe` | `function` |
|
|
139
|
-
| `yen` | `if` |
|
|
140
|
-
| `liyane` | `else` |
|
|
141
|
-
| `mandek` | `return` |
|
|
142
|
-
| `ora` | `!` |
|
|
143
|
-
|
|
144
|
-
---
|
|
145
|
-
|
|
146
|
-
## ⚠️ DISCLAIMER
|
|
147
|
-
|
|
148
|
-
**LumpiaJS ini adalah project "Have Fun" & Eksperimen Semata.**
|
|
149
|
-
Gunakan dengan resiko ditanggung sendiri (_Use at your own risk_).
|
|
150
|
-
|
|
151
47
|
_Dibuat dengan ❤️ dan 🥟 dari Semarang._
|
package/bin/lumpia.js
CHANGED
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { createProject } from '../lib/commands/create.js';
|
|
3
3
|
import { serveProject } from '../lib/commands/serve.js';
|
|
4
|
+
import { buildProject } from '../lib/commands/build.js'; // Import build
|
|
4
5
|
|
|
5
6
|
async function main() {
|
|
6
7
|
const args = process.argv.slice(2);
|
|
7
8
|
const perintah = args[0];
|
|
8
9
|
const parameter = args[1];
|
|
9
10
|
|
|
10
|
-
// ALIAS LIST
|
|
11
|
-
// create-project : buka-cabang
|
|
12
|
-
// serve : dodolan, kukus
|
|
13
|
-
// build : goreng
|
|
14
|
-
|
|
15
11
|
if (perintah === 'create-project' || perintah === 'buka-cabang') {
|
|
16
12
|
createProject(parameter);
|
|
17
13
|
}
|
|
@@ -19,8 +15,7 @@ async function main() {
|
|
|
19
15
|
serveProject();
|
|
20
16
|
}
|
|
21
17
|
else if (perintah === 'goreng' || perintah === 'build') {
|
|
22
|
-
|
|
23
|
-
console.log(" Silakan gunake: lumpia kukus (atau lumpia serve)");
|
|
18
|
+
buildProject(); // Activate functionality
|
|
24
19
|
}
|
|
25
20
|
else {
|
|
26
21
|
console.log('Perintah ora dikenal / Command not recognized.');
|
package/index.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
export { Jalan, routes } from './lib/core/Router.js';
|
|
3
|
-
export { LumpiaModel as Model } from './lib/core/Model.js';
|
|
3
|
+
export { LumpiaModel as Model } from './lib/core/Model.js'; // Model (Array / Static)
|
|
4
|
+
export { DB } from './lib/core/DB.js'; // DB (MySQL)
|
|
4
5
|
export { Controller } from './lib/core/Controller.js';
|
|
5
6
|
export { loadEnv } from './lib/core/Env.js';
|
|
7
|
+
export { loadConfig } from './lib/core/Config.js';
|
|
@@ -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
|
+
?>
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import { spawnSync } from 'child_process';
|
|
5
|
+
import { loadConfig } from '../core/Config.js';
|
|
6
|
+
|
|
7
|
+
// --- HELPERS ---
|
|
8
|
+
function transpileContent(content) {
|
|
9
|
+
let code = content;
|
|
10
|
+
code = code.replace(/from\s+['"](.+?)\.lmp['"]/g, "from '$1.js'");
|
|
11
|
+
code = code.split('\n').map(line => {
|
|
12
|
+
if (line.trim().startsWith('//')) return line;
|
|
13
|
+
return line.replace(/->/g, '.');
|
|
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'");
|
|
19
|
+
return code;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function processDirectory(source, dest) {
|
|
23
|
+
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
24
|
+
|
|
25
|
+
const items = fs.readdirSync(source);
|
|
26
|
+
items.forEach(item => {
|
|
27
|
+
const srcPath = path.join(source, item);
|
|
28
|
+
const destPath = path.join(dest, item);
|
|
29
|
+
const stat = fs.statSync(srcPath);
|
|
30
|
+
|
|
31
|
+
if (stat.isDirectory()) {
|
|
32
|
+
processDirectory(srcPath, destPath);
|
|
33
|
+
} else {
|
|
34
|
+
if (item.endsWith('.lmp')) {
|
|
35
|
+
const content = fs.readFileSync(srcPath, 'utf8');
|
|
36
|
+
const jsContent = transpileContent(content);
|
|
37
|
+
const jsDest = destPath.replace('.lmp', '.js');
|
|
38
|
+
fs.writeFileSync(jsDest, jsContent);
|
|
39
|
+
} else if (item.endsWith('.js')) {
|
|
40
|
+
const content = fs.readFileSync(srcPath, 'utf8');
|
|
41
|
+
const jsContent = transpileContent(content);
|
|
42
|
+
fs.writeFileSync(destPath, jsContent);
|
|
43
|
+
} else {
|
|
44
|
+
fs.copyFileSync(srcPath, destPath);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
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 = `
|
|
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 = `
|
|
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);
|
|
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
|
+
}
|
|
212
|
+
|
|
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; }
|
|
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
|
+
|
|
299
|
+
|
|
300
|
+
export function buildProject() {
|
|
301
|
+
const root = process.cwd();
|
|
302
|
+
const dist = path.join(root, 'dist');
|
|
303
|
+
const config = loadConfig(root);
|
|
304
|
+
|
|
305
|
+
console.log('🍳 Mulai Menggoreng (Universal Hybrid Build)...');
|
|
306
|
+
|
|
307
|
+
if (fs.existsSync(dist)) fs.rmSync(dist, { recursive: true, force: true });
|
|
308
|
+
fs.mkdirSync(dist);
|
|
309
|
+
|
|
310
|
+
if (config.klambi === 'tailwindcss') {
|
|
311
|
+
const cmd = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
312
|
+
spawnSync(cmd, ['tailwindcss', '-i', './aset/css/style.css', '-o', './public/css/style.css', '--minify'], { cwd: root, stdio: 'ignore', shell: true });
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
console.log('📂 Converting Code...');
|
|
316
|
+
processDirectory(path.join(root, 'app'), path.join(dist, 'app'));
|
|
317
|
+
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
|
+
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
|
+
}
|
|
327
|
+
|
|
328
|
+
fs.writeFileSync(path.join(dist, 'index.html'), indexHtmlContent);
|
|
329
|
+
|
|
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));
|
|
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));
|
|
372
|
+
|
|
373
|
+
console.log('✅ Mateng! (Universal Build)');
|
|
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('----------------------------------------------------');
|
|
380
|
+
}
|