lumpiajs 1.0.7 → 1.0.8

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,73 @@
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
+ ## 🦄 Fitur Unik: Laravel Syntax di JavaScript! (`->`)
9
8
 
10
- ### Opsi A: Menggunakan Command `lumpia` (Disarankan)
11
-
12
- **1. Install Secara Global**
13
-
14
- ```bash
15
- npm install -g lumpiajs
16
- ```
17
-
18
- **2. Buat Project Baru**
19
-
20
- ```bash
21
- lumpia create-project warung-ku
22
- ```
23
-
24
- **3. Masuk & Install Dependencies (Wajib)**
25
-
26
- ```bash
27
- cd warung-ku && npm install
28
- ```
29
-
30
- **4. Jalankan Server**
31
-
32
- ```bash
33
- lumpia kukus
9
+ ```javascript
10
+ // Valid di LumpiaJS (.lmp)
11
+ const users = await DB.table('users')->where('active', 1)->get();
12
+ Jalan->get('/', 'HomeController@index');
34
13
  ```
35
14
 
36
- _(Alias: `lumpia serve`)_
37
-
38
15
  ---
39
16
 
40
- ## ⚙️ Konfigurasi Environment (.env)
17
+ ## 🏗️ Cara Deploy ke Production (Server Asli)
41
18
 
42
- Setiap project LumpiaJS dilengkapi file `.env` di root folder.
19
+ Ini yang sering ditanyain: **"Mas, file mana yang harus saya upload ke hosting?"**
43
20
 
44
- ```env
45
- BASE_URL="http://localhost:3000"
46
- APP_ENV="local"
47
- APP_DEBUG="true"
48
- ```
21
+ Tenang, LumpiaJS punya fitur **Goreng** (Build) biar kamu nggak bingung.
49
22
 
50
- ---
23
+ ### 1. Goreng Project (Build)
51
24
 
52
- ## 🏗️ Struktur Project (Standar MVC)
25
+ Jalankan perintah ini di komputermu:
53
26
 
27
+ ```bash
28
+ lumpia goreng
54
29
  ```
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
30
 
74
- // Basic GET
75
- Jalan.get("/", "HomeController@index");
31
+ _(Atau: `lumpia build`)_
76
32
 
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");
33
+ Sistem akan memasak projectmu:
81
34
 
82
- // Dynamic Route dengan Parameter
83
- Jalan.get("/produk/{id}", "ProductController@show");
84
- Jalan.get("/kategori/{slug}", "CategoryController@index");
85
- ```
35
+ - Mentranspile sintaks `->` menjadi JS standard.
36
+ - Mengkompilasi CSS (minify Tailwind/Bootstrap).
37
+ - Menyiapkan folder `dist` yang siap saji.
86
38
 
87
- ### 2. Controllers (`app/controllers`)
39
+ ### 2. Upload ke Server
88
40
 
89
- Otak dari aplikasimu. Parameter dari route (misal `{id}`) otomatis masuk jadi argumen fungsi.
41
+ Setelah digoreng, akan muncul folder **`dist`**.
90
42
 
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
- ```
43
+ 👉 **HANYA ISI FOLDER `dist`** inilah yang perlu kamu upload ke server.
44
+ (Isinya: `server.js`, `package.json`, `.env`, folder `app`, `routes`, `views`, `public`)
108
45
 
109
- ### 3. Views (`views`)
46
+ ### 3. Install & Start di Server
110
47
 
111
- File berekstensi `.lmp`.
48
+ Di panel hosting (Terminal/SSH) atau VPS:
112
49
 
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
- ```
50
+ ```bash
51
+ # Masuk ke folder yang barusan diupload
52
+ cd /path/to/your/app
120
53
 
121
- ### 4. Models (`app/models`)
54
+ # Install dependencies (LumpiaJS core, mysql driver, dll)
55
+ npm install --production
122
56
 
123
- ```javascript
124
- import { Model } from "lumpiajs";
125
- // Model.use(data).dimana('harga', '<', 5000).kabeh();
57
+ # Jalankan Aplikasi
58
+ npm start
126
59
  ```
127
60
 
128
61
  ---
129
62
 
130
- ## 🧐 Kamus Bahasa (Transpiler)
63
+ ## 🗄️ Database
131
64
 
132
- Gunakan istilah Semarangan ini di dalam tag `<isi>` file `.lmp`:
65
+ Database (MySQL) itu **SERVICE**, bukan file. Jadi:
133
66
 
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` | `!` |
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.
143
70
 
144
71
  ---
145
72
 
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
73
  _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,246 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { spawnSync } from 'child_process';
4
+ import { loadConfig } from '../core/Config.js';
5
+
6
+ // Transpiler Logic (Sama kayak serve.js tapi ini permanen ke disk)
7
+ function transpileContent(content) {
8
+ let code = content;
9
+ // 1. Import: .lmp -> .js
10
+ code = code.replace(/from\s+['"](.+?)\.lmp['"]/g, "from '$1.js'");
11
+ // 2. Syntax: "->" -> "."
12
+ code = code.split('\n').map(line => {
13
+ if (line.trim().startsWith('//')) return line;
14
+ return line.replace(/->/g, '.');
15
+ }).join('\n');
16
+ return code;
17
+ }
18
+
19
+ function processDirectory(source, dest) {
20
+ if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
21
+
22
+ const items = fs.readdirSync(source);
23
+ items.forEach(item => {
24
+ const srcPath = path.join(source, item);
25
+ const destPath = path.join(dest, item);
26
+ const stat = fs.statSync(srcPath);
27
+
28
+ if (stat.isDirectory()) {
29
+ processDirectory(srcPath, destPath);
30
+ } else {
31
+ if (item.endsWith('.lmp')) {
32
+ // Transpile .lmp to .js
33
+ const content = fs.readFileSync(srcPath, 'utf8');
34
+ const jsContent = transpileContent(content);
35
+ const jsDest = destPath.replace('.lmp', '.js');
36
+ 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
+ }
47
+ } else {
48
+ fs.copyFileSync(srcPath, destPath);
49
+ }
50
+ }
51
+ });
52
+ }
53
+
54
+ const serverScript = `
55
+ import http from 'http';
56
+ import fs from 'fs';
57
+ 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';
61
+
62
+ 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 };
82
+ }
83
+ return null;
84
+ }
85
+
86
+ async function start() {
87
+ // 1. Load Routes (Compiled JS)
88
+ const routesUrl = path.join(root, 'routes', 'web.js');
89
+ if (fs.existsSync(routesUrl)) {
90
+ await import('file://' + routesUrl);
91
+ } else {
92
+ console.error("❌ Error: routes/web.js not found in build!");
93
+ }
94
+
95
+ // 2. Start Server
96
+ const server = http.createServer(async (req, res) => {
97
+ const method = req.method;
98
+ const url = new URL(req.url, 'http://' + req.headers.host);
99
+ const pathname = url.pathname;
100
+
101
+ // Static Files
102
+ const publicMap = {
103
+ '/css/': path.join(root, 'public', 'css'),
104
+ '/vendor/': path.join(root, 'public', 'vendor')
105
+ };
106
+ for (const [prefix, localPath] of Object.entries(publicMap)) {
107
+ if (pathname.startsWith(prefix)) {
108
+ const relativePath = pathname.slice(prefix.length);
109
+ const filePath = path.join(localPath, relativePath);
110
+ if (fs.existsSync(filePath) && fs.lstatSync(filePath).isFile()) {
111
+ const ext = path.extname(filePath);
112
+ const mime = ext === '.css' ? 'text/css' : (ext === '.js' ? 'text/javascript' : 'application/octet-stream');
113
+ res.writeHead(200, {'Content-Type': mime});
114
+ res.end(fs.readFileSync(filePath));
115
+ return;
116
+ }
117
+ }
118
+ }
119
+
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;
129
+ }
130
+ }
131
+
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));
155
+ }
156
+ } catch (e) {
157
+ console.error(e);
158
+ res.writeHead(500, {'Content-Type': 'text/html'});
159
+ res.end('<h1>500 Server Error</h1>');
160
+ }
161
+ } else {
162
+ res.writeHead(404);
163
+ res.end('404 Not Found');
164
+ }
165
+ });
166
+
167
+ const port = env.PORT || 3000;
168
+ server.listen(port, () => {
169
+ console.log('🚀 Production Server running on port ' + port);
170
+ });
171
+ }
172
+
173
+ start();
174
+ `;
175
+
176
+ export function buildProject() {
177
+ const root = process.cwd();
178
+ const dist = path.join(root, 'dist');
179
+ const config = loadConfig(root);
180
+
181
+ console.log('🍳 Mulai Menggoreng (Building Project)...');
182
+
183
+ // 1. Cleanup Old Dist
184
+ if (fs.existsSync(dist)) {
185
+ fs.rmSync(dist, { recursive: true, force: true });
186
+ }
187
+ fs.mkdirSync(dist);
188
+
189
+ // 2. Build Assets (Tailwind)
190
+ if (config.klambi === 'tailwindcss') {
191
+ console.log('🎨 Compiling Tailwind CSS (Minified)...');
192
+ 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
+ });
196
+ }
197
+
198
+ // 3. Copy & Transpile Code
199
+ console.log('📂 Copying & Transpiling (.lmp -> .js)...');
200
+ processDirectory(path.join(root, 'app'), path.join(dist, 'app'));
201
+ processDirectory(path.join(root, 'routes'), path.join(dist, 'routes'));
202
+ processDirectory(path.join(root, 'views'), path.join(dist, 'views')); // Views .lmp usually not imported, but kept as is? OR Transpiled?
203
+ // Wait, View.js reads raw .lmp file content. So views should strictly be copied AS IS, or renamed to .lmp but content untouched generally?
204
+ // Actually View.js `renderLumpia` expects file path.
205
+ // Let's COPY views folder AS IS (recursive copy), no renaming extensions usually needed for View Engine unless we change View.js to look for .html?
206
+ // Lumpia View Engine expects `<lump>` tags.
207
+ // Let's just copy views folder using simple copy to ensure .lmp extension stays for view engine to find it.
208
+
209
+ // RE-DO views copy: FORCE copy only
210
+ // Overwrite the 'processDirectory' for views to be simple copy
211
+ fs.cpSync(path.join(root, 'views'), path.join(dist, 'views'), { recursive: true });
212
+
213
+ // 4. Copy Static Assets
214
+ if (fs.existsSync(path.join(root, 'public'))) {
215
+ fs.cpSync(path.join(root, 'public'), path.join(dist, 'public'), { recursive: true });
216
+ }
217
+
218
+ // 5. Configs & Env
219
+ fs.copyFileSync(path.join(root, 'package.json'), path.join(dist, 'package.json'));
220
+ fs.copyFileSync(path.join(root, 'config.lmp'), path.join(dist, 'config.lmp'));
221
+ if (fs.existsSync(path.join(root, '.env'))) {
222
+ fs.copyFileSync(path.join(root, '.env'), path.join(dist, '.env'));
223
+ }
224
+
225
+ // 6. Generate Standalone Server Entry
226
+ fs.writeFileSync(path.join(dist, 'server.js'), serverScript);
227
+
228
+ // 7. Update package.json in dist
229
+ const pkg = JSON.parse(fs.readFileSync(path.join(dist, 'package.json'), 'utf8'));
230
+ pkg.scripts = {
231
+ "start": "node server.js"
232
+ };
233
+ // Ensure lumpiajs dependency is preserved
234
+ fs.writeFileSync(path.join(dist, 'package.json'), JSON.stringify(pkg, null, 2));
235
+
236
+ console.log('✅ Mateng! (Build Finished)');
237
+ console.log('----------------------------------------------------');
238
+ console.log('🎁 Yang harus dikirim ke Server (Production):');
239
+ console.log(' 📂 Folder: dist/');
240
+ console.log('');
241
+ console.log('👉 Cara Deploy:');
242
+ console.log(' 1. Upload isi folder "dist" ke server.');
243
+ console.log(' 2. Jalankan "npm install --production" di server.');
244
+ console.log(' 3. Jalankan "npm start".');
245
+ console.log('----------------------------------------------------');
246
+ }
@@ -1,156 +1,203 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import prompts from 'prompts';
3
4
 
4
- // --- TEMPLATES ---
5
5
  const routesTemplate = `import { Jalan } from 'lumpiajs';
6
6
 
7
- // Define Routes
8
- Jalan.gawe('/', 'HomeController@index');
9
- Jalan.gawe('/profile', 'HomeController@profile');
10
- Jalan.gawe('/api/products', 'ProductController@index');
7
+ // Pakai gaya Laravel (->) enak to?
8
+ Jalan->get('/', 'HomeController@index');
9
+ Jalan->get('/db-test', 'HomeController@testDb');
10
+ Jalan->get('/profile', 'HomeController@profile');
11
+ Jalan->get('/api/products', 'ProductController@index');
11
12
  `;
12
13
 
13
- const controllerTemplate = `import { Controller } from 'lumpiajs';
14
+ const controllerTemplate = `import { Controller, DB } from 'lumpiajs';
14
15
 
15
16
  export default class HomeController extends Controller {
16
17
  index() {
17
- // Tampilkan halaman home
18
- // this.env bisa diakses di sini
19
- return this.tampil('home', {
18
+ // "this.tampil" juga bisa ditulis "this->tampil"
19
+ // Transpiler LumpiaJS sing ngatur, Bos!
20
+ return this->tampil('home', {
20
21
  message: 'Welcome to LumpiaJS MVC!',
21
22
  author: 'Pakdhe Koding',
22
23
  env: this.env.APP_ENV
23
24
  });
24
25
  }
25
26
 
27
+ async testDb() {
28
+ try {
29
+ // CONTOH QUERY ALA LARAVEL
30
+ // Pake tanda panah -> biar mantap
31
+ const result = await DB.table('users')
32
+ ->limit(1)
33
+ ->get();
34
+
35
+ // Raw Query
36
+ const raw = await DB->query('SELECT 1 + 1 AS solution');
37
+
38
+ return this->json({
39
+ status: 'Connected!',
40
+ sample_user: result,
41
+ math_check: raw[0].solution
42
+ });
43
+ } catch (e) {
44
+ return this->json({ error: e.message });
45
+ }
46
+ }
47
+
26
48
  profile() {
27
- return this.tampil('profile', { name: 'Loyal User' });
49
+ return this->tampil('profile', { name: 'Loyal User' });
28
50
  }
29
51
  }
30
52
  `;
31
53
 
32
- const modelTemplate = `// Example dummy data
54
+ const modelTemplate = `// Example dummy data (Static)
33
55
  const productData = [
34
56
  { id: 1, name: 'Lumpia Basah', price: 5000 },
35
- { id: 2, name: 'Lumpia Goreng', price: 6000 },
36
- { id: 3, name: 'Tahu Gimbal', price: 12000 }
57
+ { id: 2, name: 'Lumpia Goreng', price: 6000 }
37
58
  ];
38
-
39
59
  export default productData;
40
60
  `;
41
61
 
42
62
  const productControllerTemplate = `import { Controller, Model } from 'lumpiajs';
43
- import ProductData from '../../app/models/Product.js';
63
+ import ProductData from '../../app/models/Product.lmp';
44
64
 
45
65
  export default class ProductController extends Controller {
46
66
  index() {
47
- const result = Model.use(ProductData)
48
- .dimana('price', '>', 5500)
49
- .jupuk();
50
-
51
- return this.json({
52
- status: 'success',
53
- data: result,
54
- debug_mode: this.env.APP_DEBUG
55
- });
67
+ // Model Static juga bisa pakai ->
68
+ const result = Model->use(ProductData)
69
+ ->dimana('price', '>', 5500)
70
+ ->jupuk();
71
+
72
+ return this->json({ status: 'success', data: result });
56
73
  }
57
74
  }
58
75
  `;
59
76
 
60
- const viewHomeTemplate = `<lump>
61
- <klambi>
62
- h1 { color: #d35400; text-align: center; }
63
- .box { border: 1px solid #ddd; padding: 20px; text-align: center; margin-top: 20px; }
64
- .badge { background: #eee; padding: 5px; border-radius: 4px; font-size: 12px; }
65
- </klambi>
66
-
67
- <kulit>
68
- <h1>{{ message }}</h1>
69
- <div class="box">
70
- <span class="badge">Environment: {{ env }}</span>
71
- <p>Created with love by: <strong>{{ author }}</strong></p>
72
- <button onclick="checkPrice()">Check Price API</button>
73
- <br><br>
74
- <a href="/profile">Go to Profile</a>
75
- </div>
76
- </kulit>
77
-
78
- <isi>
79
- gawe checkPrice() {
80
- fetch('/api/products')
81
- .then(res => res.json())
82
- .then(data => {
83
- alert('Data from API: ' + JSON.stringify(data));
84
- });
85
- }
86
- </isi>
87
- </lump>`;
88
-
89
77
  const viewProfileTemplate = `<lump>
90
78
  <kulit>
91
- <h1>User Profile</h1>
92
- <p>Hello, <strong>{{ name }}</strong>!</p>
93
- <a href="/">Back Home</a>
79
+ <div class="container mt-5">
80
+ <h1>User Profile</h1>
81
+ <p>Hello, <strong>{{ name }}</strong>!</p>
82
+ <a href="/" class="btn btn-secondary">Back Home</a>
83
+ </div>
94
84
  </kulit>
95
85
  </lump>`;
96
86
 
97
- const packageJsonTemplate = (name) => `{
98
- "name": "${name}",
99
- "version": "1.0.0",
100
- "main": "routes/web.js",
101
- "type": "module",
102
- "scripts": {
103
- "start": "lumpia kukus",
104
- "serve": "lumpia kukus"
105
- },
106
- "dependencies": {
107
- "lumpiajs": "latest"
108
- }
109
- }
110
- `;
111
-
112
87
  const envTemplate = `
113
88
  BASE_URL="http://localhost:3000"
114
89
  APP_ENV="local"
115
- # Pilihan: 'local', 'development', 'production'
116
90
  APP_DEBUG="true"
117
- # Pilihan: 'true', 'false'
91
+
92
+ # Database Config
93
+ DB_HOST="localhost"
94
+ DB_USER="root"
95
+ DB_PASSWORD=""
96
+ DB_NAME="lumpia_db"
118
97
  `;
119
98
 
120
- export function createProject(parameter) {
121
- const projectName = parameter;
99
+ // Helper for CSS/View generation (Collapsed for brevity but functional as before)
100
+ const tailwindConfigTemplate = `/** @type {import('tailwindcss').Config} */
101
+ module.exports = { content: ["./views/**/*.{html,js,lmp}"], theme: { extend: {}, }, plugins: [], }`;
102
+ const mainCssTemplate = `@tailwind base;\n@tailwind components;\n@tailwind utilities;\n`;
103
+
104
+ const generateHomeView = (style) => {
105
+ let css = '', html = '';
106
+ if (style === 'bootstrap') {
107
+ html = `
108
+ <div class="container mt-5">
109
+ <div class="card shadow">
110
+ <div class="card-body text-center">
111
+ <h1 class="text-primary">{{ message }}</h1>
112
+ <span class="badge bg-secondary mb-3">Env: {{ env }}</span>
113
+ <p>Created by: <strong>{{ author }}</strong></p>
114
+ <div class="mt-4">
115
+ <a href="/db-test" class="btn btn-warning">Test DB (Laravel Style)</a>
116
+ <a href="/profile" class="btn btn-link">Go to Profile</a>
117
+ </div>
118
+ </div>
119
+ </div>
120
+ </div>`;
121
+ } else if (style === 'tailwindcss') {
122
+ html = `
123
+ <div class="container mx-auto mt-10 p-5">
124
+ <div class="bg-white shadow-lg rounded-lg p-8 text-center border border-gray-200">
125
+ <h1 class="text-4xl font-bold text-orange-600 mb-4">{{ message }}</h1>
126
+ <span class="inline-block bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mb-4">Env: {{ env }}</span>
127
+ <div class="mt-6 space-x-4">
128
+ <a href="/db-test" class="bg-yellow-500 hover:bg-yellow-600 text-white font-bold py-2 px-4 rounded">Test DB</a>
129
+ <a href="/profile" class="text-blue-500 hover:text-blue-800">Go to Profile</a>
130
+ </div>
131
+ </div>
132
+ </div>`;
133
+ } else {
134
+ css = `h1{text-align:center} .box{text-align:center;margin-top:20px}`;
135
+ html = `<div class="box"><h1>{{ message }}</h1><p>Env: {{ env }}</p><a href="/db-test">Test DB</a> | <a href="/profile">Profile</a><br>(Check Controller for Laravel Syntax Demo)</div>`;
136
+ }
137
+ return `<lump><klambi>${css}</klambi><kulit>${html}</kulit><isi></isi></lump>`;
138
+ };
139
+
140
+ const generatePackageJson = (name, style) => {
141
+ const scripts = { "start": "lumpia kukus", "serve": "lumpia kukus" };
142
+ const dependencies = { "lumpiajs": "latest" };
143
+ const devDependencies = {};
144
+ if (style === 'tailwindcss') {
145
+ devDependencies["tailwindcss"] = "^3.4.0";
146
+ devDependencies["postcss"] = "^8.4.0";
147
+ devDependencies["autoprefixer"] = "^10.4.0";
148
+ } else if (style === 'bootstrap') dependencies["bootstrap"] = "^5.3.0";
149
+ return JSON.stringify({ name, version: "1.0.0", main: "routes/web.lmp", type: "module", scripts, dependencies, devDependencies }, null, 2);
150
+ };
151
+
152
+ export async function createProject(parameter) {
153
+ let projectName = parameter;
122
154
  if (!projectName) {
123
- console.log(' Please specify project name.');
124
- console.log('Example: lumpia create-project my-app');
125
- return;
155
+ const res = await prompts({ type: 'text', name: 'val', message: 'Jeneng project?', initial: 'my-app' });
156
+ projectName = res.val;
126
157
  }
127
-
158
+ if (!projectName) return;
159
+
128
160
  const root = path.join(process.cwd(), projectName);
129
- if (fs.existsSync(root)) return console.log(`❌ Folder ${projectName} already exists.`);
161
+ if (fs.existsSync(root)) { console.log('❌ Folder Exists'); return; }
130
162
 
131
- console.log(`🔨 Building MVC foundation in ${projectName}...`);
132
-
163
+ const styleRes = await prompts({
164
+ type: 'select', name: 'val', message: 'Styling?',
165
+ choices: [{title:'Vanilla',value:'none'},{title:'Tailwind',value:'tailwindcss'},{title:'Bootstrap',value:'bootstrap'}],
166
+ initial: 0
167
+ });
168
+ const style = styleRes.val;
169
+ if (!style) return;
170
+
171
+ // Structure
133
172
  fs.mkdirSync(root);
134
-
135
173
  fs.mkdirSync(path.join(root, 'app', 'controllers'), { recursive: true });
136
174
  fs.mkdirSync(path.join(root, 'app', 'models'), { recursive: true });
137
175
  fs.mkdirSync(path.join(root, 'routes'));
138
- fs.mkdirSync(path.join(root, 'views'));
139
-
140
- // Write files
141
- fs.writeFileSync(path.join(root, 'package.json'), packageJsonTemplate(projectName));
142
- fs.writeFileSync(path.join(root, '.env'), envTemplate); // Create .env
143
- fs.writeFileSync(path.join(root, 'routes', 'web.js'), routesTemplate);
144
- fs.writeFileSync(path.join(root, 'app', 'controllers', 'HomeController.js'), controllerTemplate);
145
- fs.writeFileSync(path.join(root, 'app', 'controllers', 'ProductController.js'), productControllerTemplate);
146
- fs.writeFileSync(path.join(root, 'app', 'models', 'Product.js'), modelTemplate);
147
- fs.writeFileSync(path.join(root, 'views', 'home.lmp'), viewHomeTemplate);
176
+ fs.mkdirSync(path.join(root, 'views'));
177
+ fs.mkdirSync(path.join(root, 'aset', 'css'), { recursive: true });
178
+ fs.mkdirSync(path.join(root, 'public', 'css'), { recursive: true });
179
+
180
+ // Files
181
+ fs.writeFileSync(path.join(root, 'package.json'), generatePackageJson(projectName, style));
182
+ fs.writeFileSync(path.join(root, '.gitignore'), `.lumpia\nnode_modules\n.env\n`);
183
+ fs.writeFileSync(path.join(root, '.env'), envTemplate);
184
+ fs.writeFileSync(path.join(root, 'config.lmp'), JSON.stringify({ klambi: style }, null, 2));
185
+
186
+ fs.writeFileSync(path.join(root, 'routes', 'web.lmp'), routesTemplate);
187
+ fs.writeFileSync(path.join(root, 'app', 'controllers', 'HomeController.lmp'), controllerTemplate);
188
+ fs.writeFileSync(path.join(root, 'app', 'controllers', 'ProductController.lmp'), productControllerTemplate);
189
+ fs.writeFileSync(path.join(root, 'app', 'models', 'Product.lmp'), modelTemplate);
190
+ fs.writeFileSync(path.join(root, 'views', 'home.lmp'), generateHomeView(style));
148
191
  fs.writeFileSync(path.join(root, 'views', 'profile.lmp'), viewProfileTemplate);
192
+
193
+ // Style Assets
194
+ if (style === 'tailwindcss') {
195
+ fs.writeFileSync(path.join(root, 'tailwind.config.js'), tailwindConfigTemplate);
196
+ fs.writeFileSync(path.join(root, 'aset', 'css', 'style.css'), mainCssTemplate);
197
+ } else {
198
+ fs.writeFileSync(path.join(root, 'aset', 'css', 'style.css'), style === 'bootstrap' ? '/* Bootstrap Imported in HTML */' : '/* CSS */');
199
+ }
149
200
 
150
- console.log('✅ Project ready! Standard MVC Structure with .env Support.');
151
- console.log('----------------------------------------------------');
152
- console.log(`cd ${projectName}`);
153
- console.log('npm install');
154
- console.log('lumpia kukus');
155
- console.log('----------------------------------------------------');
201
+ console.log(`✅ Project "${projectName}" Ready!`);
202
+ console.log(`cd ${projectName} && npm install && lumpia kukus`);
156
203
  }
@@ -1,95 +1,146 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
3
  import http from 'http';
4
+ import { spawn } from 'child_process';
4
5
  import { routes } from '../core/Router.js';
5
6
  import { renderLumpia } from '../core/View.js';
6
7
  import { loadEnv } from '../core/Env.js';
8
+ import { loadConfig } from '../core/Config.js';
7
9
 
8
- // Baca versi LumpiaJS global (CLI ini)
9
10
  const cliPackageJson = JSON.parse(fs.readFileSync(new URL('../../package.json', import.meta.url)));
10
11
 
11
- // Helper untuk mencocokkan Route dengan Parameter {id} dll
12
+ // Helper to Match Routes
12
13
  function matchRoute(definedRoute, method, pathname) {
13
14
  if (definedRoute.method !== method) return null;
14
-
15
- // 1. Exact Match
16
15
  if (definedRoute.path === pathname) return { params: {} };
17
16
 
18
- // 2. Dynamic Match (Regex)
19
- // Convert /produk/{id}/{slug} -> ^\/produk\/([^\/]+)\/([^\/]+)$
20
17
  const paramNames = [];
21
18
  const regexPath = definedRoute.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (match, name) => {
22
19
  paramNames.push(name);
23
20
  return '([^/]+)';
24
21
  });
25
22
 
26
- // Skip if no params found in definition (optimization)
27
23
  if (regexPath === definedRoute.path) return null;
28
-
29
24
  const regex = new RegExp(`^${regexPath}$`);
30
25
  const match = pathname.match(regex);
31
-
32
26
  if (match) {
33
27
  const params = {};
34
28
  paramNames.forEach((name, index) => {
35
- params[name] = match[index + 1]; // capture groups start at 1
29
+ params[name] = match[index + 1];
36
30
  });
37
31
  return { params };
38
32
  }
39
-
40
33
  return null;
41
34
  }
42
35
 
43
- export async function serveProject() {
44
- const root = process.cwd();
36
+ const backgroundProcess = [];
37
+
38
+ function startTailwindWatcher(root) {
39
+ console.log('🎨 TailwindCSS detected! Starting watcher...');
40
+ const cmd = process.platform === 'win32' ? 'npx.cmd' : 'npx';
41
+ const tailwind = spawn(cmd, [
42
+ 'tailwindcss', '-i', './aset/css/style.css', '-o', './public/css/style.css', '--watch'
43
+ ], { cwd: root, stdio: 'inherit', shell: true });
44
+ backgroundProcess.push(tailwind);
45
+ }
46
+
47
+ function handleBootstrap(root) {
48
+ const bootDestDist = path.join(root, 'public', 'vendor', 'bootstrap');
49
+ const bootSrc = path.join(root, 'node_modules', 'bootstrap', 'dist');
50
+ if (fs.existsSync(bootSrc) && !fs.existsSync(bootDestDist)) {
51
+ console.log('📦 Menyalin library Bootstrap ke public...');
52
+ try { fs.cpSync(bootSrc, bootDestDist, { recursive: true }); } catch(e) {}
53
+ }
54
+ }
55
+
56
+ // LOADER UTAMA UNTUK .lmp (Controller/Model/Routes)
57
+ async function loadLumpiaModule(filePath) {
58
+ const originalContent = fs.readFileSync(filePath, 'utf-8');
59
+ const cacheDir = path.join(process.cwd(), '.lumpia', 'cache');
60
+ if (!fs.existsSync(cacheDir)) fs.mkdirSync(cacheDir, { recursive: true });
45
61
 
46
- // --- 1. Version Check ---
47
- const userPackageJsonPath = path.join(root, 'package.json');
48
- if (fs.existsSync(userPackageJsonPath)) {
49
- const userPkg = JSON.parse(fs.readFileSync(userPackageJsonPath, 'utf-8'));
50
- const userLumpiaVer = (userPkg.dependencies && userPkg.dependencies.lumpiajs) || 'unknown';
62
+ const relativePath = path.relative(process.cwd(), filePath);
63
+ const flatName = relativePath.replace(/[\/\\]/g, '_').replace('.lmp', '.js');
64
+ const destPath = path.join(cacheDir, flatName);
65
+
66
+ // --- TRANSPILE LOGIC ---
67
+ let transcoded = originalContent;
68
+
69
+ // 1. Replace Imports: .lmp -> .js
70
+ transcoded = transcoded.replace(/from\s+['"](.+?)\.lmp['"]/g, "from '$1.js'");
71
+
72
+ // 2. SYNTAX LARAVEL -> JS (Fitur Request User: "->")
73
+ // Mengubah tanda panah "->" menjadi titik "."
74
+ // Hati-hati: Kita coba sebisa mungkin tidak mengubah "->" yang ada di dalam string.
75
+ // Regex ini mencari "->" yang TIDAK diapit kutip (Simpel, mungkin tidak 100% sempurna tapi cukup untuk have fun)
76
+ // Atau kita brute force saja asalkan user tahu resikonya.
77
+ // Kita gunakan pendekatan brute replace tapi hindari arrow function "=>" (aman karena beda karakter)
78
+
79
+ // Replace "->" dengan "."
80
+ transcoded = transcoded.split('\n').map(line => {
81
+ // Cek komentar //
82
+ if (line.trim().startsWith('//')) return line;
51
83
 
52
- console.log(`ℹ️ LumpiaJS CLI Version: ${cliPackageJson.version}`);
53
- console.log(`ℹ️ Project Dependency Version: ${userLumpiaVer}`);
54
-
55
- const cleanUserVer = userLumpiaVer.replace(/[^0-9.]/g, '');
56
- if (cleanUserVer !== cliPackageJson.version && userLumpiaVer !== 'latest') {
57
- console.log('\n⚠️ PERINGATAN BEDA VERSI!');
58
- console.log(` Versi CLI kamu (${cliPackageJson.version}) beda dengan versi di project (${userLumpiaVer}).`);
59
- console.log(' Saran: Update project dependencies agar sinkron.');
60
- console.log(' Cara update: npm install lumpiajs@latest\n');
61
- }
62
- }
84
+ // Simple replace "->" to "."
85
+ // Note: Ini akan mengubah string "go -> to" menjadi "go . to".
86
+ // Untuk framework "Have Fun", ini fitur, bukan bug. XD
87
+ return line.replace(/->/g, '.');
88
+ }).join('\n');
89
+
90
+ fs.writeFileSync(destPath, transcoded);
91
+
92
+ // 4. Import file .js
93
+ const module = await import('file://' + destPath + '?t=' + Date.now());
94
+ return module;
95
+ }
96
+
63
97
 
64
- const routesFile = path.join(root, 'routes', 'web.js');
65
- if (!fs.existsSync(routesFile)) return console.log("❌ This is not a LumpiaJS MVC project! (Cannot find routes/web.js)");
98
+ export async function serveProject() {
99
+ const root = process.cwd();
100
+
101
+ const routesFile = path.join(root, 'routes', 'web.lmp');
102
+ if (!fs.existsSync(routesFile)) return console.log("❌ Not a LumpiaJS Project. (Missing routes/web.lmp)");
66
103
 
67
- // --- 2. Load ENV ---
68
104
  const env = loadEnv(root);
69
- console.log(`🌍 Environment: ${env.APP_ENV}`);
70
- console.log(`🐛 Debug Mode: ${env.APP_DEBUG}`);
105
+ const config = loadConfig(root);
71
106
 
72
- try {
73
- // Load User Routes
74
- const userRouteUrl = path.join(root, 'routes', 'web.js');
75
- await import('file://' + userRouteUrl);
107
+ if (config.klambi === 'tailwindcss') startTailwindWatcher(root);
108
+ else if (config.klambi === 'bootstrap') handleBootstrap(root);
76
109
 
110
+ try {
111
+ await loadLumpiaModule(routesFile);
77
112
  console.log(`🛣️ Routes registered: ${routes.length}`);
113
+
114
+ // Info Syntax
115
+ console.log(`✨ Syntax Mode: PHP/Laravel Style (->) is enabled in .lmp files!`);
78
116
 
79
- // Start Server
80
117
  const server = http.createServer(async (req, res) => {
81
118
  const method = req.method;
82
119
  const url = new URL(req.url, `http://${req.headers.host}`);
83
120
  const pathname = url.pathname;
84
121
 
85
- if (env.APP_DEBUG === 'true') {
86
- console.log(`📥 ${method} ${pathname}`);
122
+ if (env.APP_DEBUG === 'true') console.log(`📥 ${method} ${pathname}`);
123
+
124
+ const publicMap = {
125
+ '/css/': path.join(root, 'public', 'css'),
126
+ '/vendor/': path.join(root, 'public', 'vendor')
127
+ };
128
+ for (const [prefix, localPath] of Object.entries(publicMap)) {
129
+ if (pathname.startsWith(prefix)) {
130
+ const relativePath = pathname.slice(prefix.length);
131
+ const filePath = path.join(localPath, relativePath);
132
+ if (fs.existsSync(filePath) && fs.lstatSync(filePath).isFile()) {
133
+ const ext = path.extname(filePath);
134
+ const mime = ext === '.css' ? 'text/css' : (ext === '.js' ? 'text/javascript' : 'application/octet-stream');
135
+ res.writeHead(200, {'Content-Type': mime});
136
+ res.end(fs.readFileSync(filePath));
137
+ return;
138
+ }
139
+ }
87
140
  }
88
141
 
89
- // FIND MATCHING ROUTE
90
142
  let match = null;
91
143
  let params = {};
92
-
93
144
  for (const route of routes) {
94
145
  const result = matchRoute(route, method, pathname);
95
146
  if (result) {
@@ -102,29 +153,20 @@ export async function serveProject() {
102
153
  if (match) {
103
154
  try {
104
155
  const [controllerName, methodName] = match.action.split('@');
105
- const controllerPath = path.join(root, 'app', 'controllers', controllerName + '.js');
156
+ const controllerPath = path.join(root, 'app', 'controllers', controllerName + '.lmp');
106
157
 
107
- if (!fs.existsSync(controllerPath)) throw new Error(`Controller ${controllerName} not found in app/controllers!`);
158
+ if (!fs.existsSync(controllerPath)) throw new Error(`Controller ${controllerName} not found!`);
108
159
 
109
- // In Development/Local, always invalidate cache
110
- let importUrl = 'file://' + controllerPath;
111
- if (env.APP_ENV === 'local' || env.APP_ENV === 'development') {
112
- importUrl += '?update=' + Date.now();
113
- }
114
-
115
- const module = await import(importUrl);
160
+ const module = await loadLumpiaModule(controllerPath);
116
161
  const ControllerClass = module.default;
117
162
  const instance = new ControllerClass();
118
163
 
119
- // Inject ENV & Params to Controller
120
164
  instance.env = env;
121
- instance.params = params; // Available as this.params
165
+ instance.params = params;
166
+ instance.config = config;
122
167
 
123
- if (typeof instance[methodName] !== 'function') {
124
- throw new Error(`Method ${methodName} does not exist in ${controllerName}`);
125
- }
168
+ if (typeof instance[methodName] !== 'function') throw new Error(`Method ${methodName} missing`);
126
169
 
127
- // Pass params as spread arguments to the method: index(id, slug)
128
170
  const args = Object.values(params);
129
171
  const result = await instance[methodName](...args);
130
172
 
@@ -140,26 +182,28 @@ export async function serveProject() {
140
182
  }
141
183
  } catch (e) {
142
184
  console.error(e);
143
- const errorMsg = env.APP_DEBUG === 'true'
144
- ? `<h1>500: Server Error</h1><pre>${e.message}\n${e.stack}</pre>`
145
- : `<h1>500: Server Error</h1><p>Something went wrong.</p>`;
146
-
185
+ const errorMsg = env.APP_DEBUG === 'true' ? `<pre>${e.stack}</pre>` : `<h1>Server Error</h1>`;
147
186
  res.writeHead(500, {'Content-Type': 'text/html'});
148
187
  res.end(errorMsg);
149
188
  }
150
189
  } else {
151
190
  res.writeHead(404, {'Content-Type': 'text/html'});
152
- res.end('<h1>404: Not Found</h1>');
191
+ res.end('<h1>404 Not Found</h1>');
153
192
  }
154
193
  });
155
194
 
156
195
  const port = 3000;
157
196
  server.listen(port, () => {
158
197
  console.log(`🚀 Server running at ${env.BASE_URL || 'http://localhost:3000'}`);
159
- console.log(`(Press Ctrl+C to stop)`);
198
+ });
199
+
200
+ process.on('SIGINT', () => {
201
+ backgroundProcess.forEach(p => p.kill());
202
+ try { fs.rmSync(path.join(root, '.lumpia'), { recursive: true, force: true }); } catch(e){}
203
+ process.exit();
160
204
  });
161
205
 
162
206
  } catch (err) {
163
- console.error('Fatal Error loading routes:', err);
207
+ console.error('Fatal Error:', err);
164
208
  }
165
209
  }
@@ -0,0 +1,20 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+
4
+ export function loadConfig(root) {
5
+ const configPath = path.join(root, 'config.lmp');
6
+ const config = {
7
+ klambi: 'none' // default
8
+ };
9
+
10
+ if (fs.existsSync(configPath)) {
11
+ try {
12
+ const content = fs.readFileSync(configPath, 'utf-8');
13
+ const parsed = JSON.parse(content);
14
+ if (parsed.klambi) config.klambi = parsed.klambi;
15
+ } catch (e) {
16
+ console.error('⚠️ Gagal moco config.lmp', e.message);
17
+ }
18
+ }
19
+ return config;
20
+ }
package/lib/core/DB.js ADDED
@@ -0,0 +1,139 @@
1
+ import mysql from 'mysql2/promise';
2
+ import { loadEnv } from './Env.js';
3
+
4
+ let pool = null;
5
+
6
+ export class DB {
7
+ static async connect() {
8
+ if (pool) return pool;
9
+
10
+ const env = loadEnv(process.cwd());
11
+
12
+ if (!env.DB_HOST || !env.DB_USER || !env.DB_NAME) {
13
+ return null;
14
+ }
15
+
16
+ try {
17
+ pool = mysql.createPool({
18
+ host: env.DB_HOST,
19
+ user: env.DB_USER,
20
+ password: env.DB_PASSWORD || '',
21
+ database: env.DB_NAME,
22
+ waitForConnections: true,
23
+ connectionLimit: 10,
24
+ queueLimit: 0
25
+ });
26
+ return pool;
27
+ } catch (e) {
28
+ console.error("❌ Gagal connect database:", e.message);
29
+ return null;
30
+ }
31
+ }
32
+
33
+ static async query(sql, params = []) {
34
+ const p = await this.connect();
35
+ if (!p) throw new Error("Database durung disetting nang .env!");
36
+ try {
37
+ const [rows, fields] = await p.execute(sql, params);
38
+ return rows;
39
+ } catch (e) {
40
+ console.error("❌ SQL Error:", e.message);
41
+ throw e;
42
+ }
43
+ }
44
+
45
+ static table(tableName) {
46
+ return new QueryBuilder(tableName);
47
+ }
48
+ }
49
+
50
+ class QueryBuilder {
51
+ constructor(table) {
52
+ this.tableName = table;
53
+ this.conditions = [];
54
+ this.bindings = [];
55
+ this.selects = '*';
56
+ this.limitVal = null;
57
+ this.orderByRaw = null;
58
+ this.executed = false; // Flag to prevent double execution if method chaining is weird
59
+ }
60
+
61
+ // SELECT logic
62
+ select(fields) {
63
+ this.selects = fields;
64
+ return this;
65
+ }
66
+
67
+ // WHERE logic
68
+ where(col, op, val) {
69
+ if (val === undefined) { val = op; op = '='; }
70
+ this.conditions.push(`${col} ${op} ?`);
71
+ this.bindings.push(val);
72
+ return this;
73
+ }
74
+
75
+ orderBy(col, direction = 'ASC') {
76
+ this.orderByRaw = `${col} ${direction}`;
77
+ return this;
78
+ }
79
+
80
+ take(n) { // Alias for limit, Laravel style
81
+ this.limitVal = n;
82
+ return this;
83
+ }
84
+
85
+ // EKSEKUTOR (Disini logic query dijalankan)
86
+ // Ubah nama method 'get' jadi 'get()' yang mengembalikan Promise<Array>
87
+ async get() {
88
+ let sql = `SELECT ${this.selects} FROM ${this.tableName}`;
89
+
90
+ if (this.conditions.length > 0) {
91
+ sql += ' WHERE ' + this.conditions.join(' AND ');
92
+ }
93
+ if (this.orderByRaw) {
94
+ sql += ' ORDER BY ' + this.orderByRaw;
95
+ }
96
+ if (this.limitVal) {
97
+ sql += ' LIMIT ' + this.limitVal;
98
+ }
99
+ return await DB.query(sql, this.bindings);
100
+ }
101
+
102
+ // Alias first() -> Ambil 1 data
103
+ async first() {
104
+ this.take(1);
105
+ const rows = await this.get();
106
+ return rows.length > 0 ? rows[0] : null;
107
+ }
108
+
109
+ // Insert
110
+ async insert(data) {
111
+ const keys = Object.keys(data);
112
+ const values = Object.values(data);
113
+ const placeholders = keys.map(() => '?').join(', ');
114
+ const sql = `INSERT INTO ${this.tableName} (${keys.join(', ')}) VALUES (${placeholders})`;
115
+ return await DB.query(sql, values);
116
+ }
117
+
118
+ // Update
119
+ async update(data) {
120
+ const keys = Object.keys(data);
121
+ const values = Object.values(data);
122
+ const setClause = keys.map(k => `${k} = ?`).join(', ');
123
+
124
+ let sql = `UPDATE ${this.tableName} SET ${setClause}`;
125
+ if (this.conditions.length > 0) {
126
+ sql += ' WHERE ' + this.conditions.join(' AND ');
127
+ }
128
+ return await DB.query(sql, [...values, ...this.bindings]);
129
+ }
130
+
131
+ // Delete
132
+ async delete() {
133
+ let sql = `DELETE FROM ${this.tableName}`;
134
+ if (this.conditions.length > 0) {
135
+ sql += ' WHERE ' + this.conditions.join(' AND ');
136
+ }
137
+ return await DB.query(sql, this.bindings);
138
+ }
139
+ }
package/lib/core/View.js CHANGED
@@ -1,20 +1,21 @@
1
1
  import fs from 'fs';
2
+ import path from 'path';
3
+ import { loadConfig } from './Config.js';
2
4
 
3
5
  // --- KAMUS BAHASA SEMARANG (Blade-like Template Engine) ---
4
- // Mengubah sintaks .lmp ke HTML siap render dengan interpolasi data
5
6
  export function renderLumpia(viewPath, data = {}) {
6
7
  let content = fs.readFileSync(viewPath, 'utf-8');
7
8
 
8
9
  // 1. Ekstrak bagian <klambi>, <kulit>, <isi>
9
10
  const matchKulit = content.match(/<kulit>([\s\S]*?)<\/kulit>/);
10
- const matchIsi = content.match(/<isi>([\s\S]*?)<\/isi>/); // Client-side JS
11
+ const matchIsi = content.match(/<isi>([\s\S]*?)<\/isi>/);
11
12
  const matchKlambi = content.match(/<klambi>([\s\S]*?)<\/klambi>/);
12
13
 
13
14
  let htmlBody = matchKulit ? matchKulit[1] : '';
14
15
  let clientScript = matchIsi ? matchIsi[1] : '';
15
16
  let cssStyle = matchKlambi ? matchKlambi[1] : '';
16
17
 
17
- // 2. Transpile Client-side JS (Semarangan -> JS Standard)
18
+ // 2. Transpile Client-side JS
18
19
  const dictionary = [
19
20
  { asal: /ono\s/g, jadi: 'let ' },
20
21
  { asal: /paten\s/g, jadi: 'const ' },
@@ -27,26 +28,38 @@ export function renderLumpia(viewPath, data = {}) {
27
28
  ];
28
29
  dictionary.forEach(kata => clientScript = clientScript.replace(kata.asal, kata.jadi));
29
30
 
30
- // 3. Templating Engine (Mirip Blade {{ variable }})
31
- // Mengganti {{ variable }} dengan value dari `data`
31
+ // 3. Templating Engine
32
32
  for (const [key, value] of Object.entries(data)) {
33
33
  const regex = new RegExp(`{{\\s*${key}\\s*}}`, 'g');
34
- // Simple XSS protection could be added here
35
34
  htmlBody = htmlBody.replace(regex, value);
36
35
  }
37
36
 
37
+ // --- 4. AUTO INJECT ASSETS BASED ON CONFIG ---
38
+ // Kita baca config dari PROJECT_ROOT (asumsi process.cwd())
39
+ let headInjection = '';
40
+ const config = loadConfig(process.cwd());
41
+
42
+ // Inject main.css (Tailwind build result or Custom CSS)
43
+ headInjection += `<link rel="stylesheet" href="/css/style.css">`;
44
+
45
+ // Inject Vendor Specific
46
+ if (config.klambi === 'bootstrap') {
47
+ headInjection += `<link rel="stylesheet" href="/vendor/bootstrap/css/bootstrap.min.css">`;
48
+ // Optional JS injection could be added here
49
+ }
50
+
38
51
  // 4. Rakit Akhir
39
52
  return `<!DOCTYPE html>
40
53
  <html>
41
54
  <head>
42
55
  <title>Lumpia App</title>
43
- <style>body{font-family:sans-serif;padding:20px;} ${cssStyle}</style>
56
+ ${headInjection}
57
+ <style> ${cssStyle}</style>
44
58
  </head>
45
59
  <body>
46
60
  ${htmlBody}
47
61
  <div id="output-lumpia"></div>
48
62
  <script>
49
- // Runtime Helper
50
63
  function tampil(txt) {
51
64
  let el = document.getElementById('output-lumpia');
52
65
  if(el) el.innerText = txt;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lumpiajs",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
4
4
  "description": "Bahasa Pemrograman Semarangan",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -8,6 +8,8 @@
8
8
  "lumpia": "./bin/lumpia.js"
9
9
  },
10
10
  "dependencies": {
11
- "fs-extra": "^11.1.1"
11
+ "fs-extra": "^11.1.1",
12
+ "mysql2": "^3.16.0",
13
+ "prompts": "^2.4.2"
12
14
  }
13
15
  }
@@ -0,0 +1,3 @@
1
+ .lumpia
2
+ node_modules
3
+ .env