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 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
- ## 🚀 Panduan Instalasi & Penggunaan (Lengkap)
7
+ ## 🦄 Deployment Ajaib (Universal)
9
8
 
10
- ### Opsi A: Menggunakan Command `lumpia` (Disarankan)
9
+ Ini fitur andalan LumpiaJS. Satu folder build (`dist`) bisa jalan di mana saja tanpa ubah kodingan.
11
10
 
12
- **1. Install Secara Global**
11
+ User (Browser) akan menembak alamat `/api`.
13
12
 
14
- ```bash
15
- npm install -g lumpiajs
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
- **2. Buat Project Baru**
16
+ ### 1. Build Project
19
17
 
20
18
  ```bash
21
- lumpia create-project warung-ku
19
+ lumpia goreng
22
20
  ```
23
21
 
24
- **3. Masuk & Install Dependencies (Wajib)**
22
+ ### 2. Panduan Deploy
25
23
 
26
- ```bash
27
- cd warung-ku && npm install
28
- ```
24
+ **A. Hosting PHP / XAMPP (Apache)**
29
25
 
30
- **4. Jalankan Server**
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
- ```bash
33
- lumpia kukus
34
- ```
31
+ **B. Vercel (Gratis)**
35
32
 
36
- _(Alias: `lumpia serve`)_
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
- Setiap project LumpiaJS dilengkapi file `.env` di root folder.
43
-
44
- ```env
45
- BASE_URL="http://localhost:3000"
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
- console.log("🚧 Fitur 'goreng' (build) saiki wis otomatis digabung karo 'kukus' (serve) via JIT Compiler MVC.");
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
+ }