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 CHANGED
@@ -4,69 +4,43 @@
4
4
 
5
5
  ---
6
6
 
7
- ## 🦄 Fitur Unik: Laravel Syntax di JavaScript! (`->`)
7
+ ## 🦄 Deployment Ajaib (Universal)
8
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
- ```
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
- Ini yang sering ditanyain: **"Mas, file mana yang harus saya upload ke hosting?"**
11
+ User (Browser) akan menembak alamat `/api`.
20
12
 
21
- Tenang, LumpiaJS punya fitur **Goreng** (Build) biar kamu nggak bingung.
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. Goreng Project (Build)
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
- _(Atau: `lumpia build`)_
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
- Setelah digoreng, akan muncul folder **`dist`**.
24
+ **A. Hosting PHP / XAMPP (Apache)**
42
25
 
43
- 👉 **HANYA ISI FOLDER `dist`** inilah yang perlu kamu upload ke server.
44
- (Isinya: `server.js`, `package.json`, `.env`, folder `app`, `routes`, `views`, `public`)
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
- ### 3. Install & Start di Server
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
- ## 🗄️ Database
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
- Database (MySQL) itu **SERVICE**, bukan file. Jadi:
38
+ **C. VPS (Node.js)**
66
39
 
67
- 1. Export database dari localhost (phpMyAdmin -> Export .sql).
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.
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
+ ?>
@@ -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
- // Transpiler Logic (Sama kayak serve.js tapi ini permanen ke disk)
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') || 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
- }
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
- const serverScript = `
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 { routes, Jalan } from 'lumpiajs/lib/core/Router.js';
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
- const env = loadEnv(root);
64
- const config = loadConfig(root);
65
-
66
- // ROUTE MATCHER (Copied from Core)
67
- function matchRoute(definedRoute, method, pathname) {
68
- if (definedRoute.method !== method) return null;
69
- if (definedRoute.path === pathname) return { params: {} };
70
- const paramNames = [];
71
- const regexPath = definedRoute.path.replace(/\\{([a-zA-Z0-9_]+)\\}/g, (match, name) => {
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 };
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
- 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!");
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
- // 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
- }
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
- // Routing
121
- let match = null;
122
- let params = {};
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;
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
- if (match) {
133
- try {
134
- const [controllerName, methodName] = match.action.split('@');
135
- const controllerPath = path.join(root, 'app', 'controllers', controllerName + '.js');
136
- if (!fs.existsSync(controllerPath)) throw new Error('Controller ' + controllerName + ' not found');
137
-
138
- const module = await import('file://' + controllerPath);
139
- const ControllerClass = module.default;
140
- const instance = new ControllerClass();
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
- } catch (e) {
157
- console.error(e);
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
- const port = env.PORT || 3000;
168
- server.listen(port, () => {
169
- console.log('🚀 Production Server running on port ' + port);
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 (Building Project)...');
305
+ console.log('🍳 Mulai Menggoreng (Universal Hybrid Build)...');
182
306
 
183
- // 1. Cleanup Old Dist
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
- // 3. Copy & Transpile Code
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
- // RE-DO views copy: FORCE copy only
210
- // Overwrite the 'processDirectory' for views to be simple copy
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
- // 4. Copy Static Assets
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
- // 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
- }
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
- // 6. Generate Standalone Server Entry
226
- fs.writeFileSync(path.join(dist, 'server.js'), serverScript);
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
- // 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));
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 Finished)');
373
+ console.log('✅ Mateng! (Universal Build)');
237
374
  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".');
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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lumpiajs",
3
- "version": "1.0.8",
3
+ "version": "1.0.9",
4
4
  "description": "Bahasa Pemrograman Semarangan",
5
5
  "type": "module",
6
6
  "main": "index.js",