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 +36 -114
- package/bin/lumpia.js +2 -7
- package/index.js +3 -1
- package/lib/commands/build.js +246 -0
- package/lib/commands/create.js +146 -99
- package/lib/commands/serve.js +109 -65
- package/lib/core/Config.js +20 -0
- package/lib/core/DB.js +139 -0
- package/lib/core/View.js +21 -8
- package/package.json +4 -2
- package/templates/gitignore.txt +3 -0
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
|
-
##
|
|
7
|
+
## 🦄 Fitur Unik: Laravel Syntax di JavaScript! (`->`)
|
|
9
8
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
##
|
|
17
|
+
## 🏗️ Cara Deploy ke Production (Server Asli)
|
|
41
18
|
|
|
42
|
-
|
|
19
|
+
Ini yang sering ditanyain: **"Mas, file mana yang harus saya upload ke hosting?"**
|
|
43
20
|
|
|
44
|
-
|
|
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
|
-
|
|
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
|
-
|
|
75
|
-
Jalan.get("/", "HomeController@index");
|
|
31
|
+
_(Atau: `lumpia build`)_
|
|
76
32
|
|
|
77
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
```
|
|
35
|
+
- Mentranspile sintaks `->` menjadi JS standard.
|
|
36
|
+
- Mengkompilasi CSS (minify Tailwind/Bootstrap).
|
|
37
|
+
- Menyiapkan folder `dist` yang siap saji.
|
|
86
38
|
|
|
87
|
-
### 2.
|
|
39
|
+
### 2. Upload ke Server
|
|
88
40
|
|
|
89
|
-
|
|
41
|
+
Setelah digoreng, akan muncul folder **`dist`**.
|
|
90
42
|
|
|
91
|
-
|
|
92
|
-
|
|
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.
|
|
46
|
+
### 3. Install & Start di Server
|
|
110
47
|
|
|
111
|
-
|
|
48
|
+
Di panel hosting (Terminal/SSH) atau VPS:
|
|
112
49
|
|
|
113
|
-
```
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
54
|
+
# Install dependencies (LumpiaJS core, mysql driver, dll)
|
|
55
|
+
npm install --production
|
|
122
56
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
// Model.use(data).dimana('harga', '<', 5000).kabeh();
|
|
57
|
+
# Jalankan Aplikasi
|
|
58
|
+
npm start
|
|
126
59
|
```
|
|
127
60
|
|
|
128
61
|
---
|
|
129
62
|
|
|
130
|
-
##
|
|
63
|
+
## 🗄️ Database
|
|
131
64
|
|
|
132
|
-
|
|
65
|
+
Database (MySQL) itu **SERVICE**, bukan file. Jadi:
|
|
133
66
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/lib/commands/create.js
CHANGED
|
@@ -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
|
-
//
|
|
8
|
-
Jalan
|
|
9
|
-
Jalan
|
|
10
|
-
Jalan
|
|
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
|
-
//
|
|
18
|
-
//
|
|
19
|
-
return this
|
|
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
|
|
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.
|
|
63
|
+
import ProductData from '../../app/models/Product.lmp';
|
|
44
64
|
|
|
45
65
|
export default class ProductController extends Controller {
|
|
46
66
|
index() {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
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
|
-
|
|
121
|
-
|
|
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
|
-
|
|
124
|
-
|
|
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))
|
|
161
|
+
if (fs.existsSync(root)) { console.log('❌ Folder Exists'); return; }
|
|
130
162
|
|
|
131
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
fs.writeFileSync(path.join(root, '
|
|
144
|
-
fs.writeFileSync(path.join(root, '
|
|
145
|
-
fs.writeFileSync(path.join(root, '
|
|
146
|
-
fs.writeFileSync(path.join(root, '
|
|
147
|
-
|
|
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(
|
|
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
|
}
|
package/lib/commands/serve.js
CHANGED
|
@@ -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
|
|
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];
|
|
29
|
+
params[name] = match[index + 1];
|
|
36
30
|
});
|
|
37
31
|
return { params };
|
|
38
32
|
}
|
|
39
|
-
|
|
40
33
|
return null;
|
|
41
34
|
}
|
|
42
35
|
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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
|
-
|
|
70
|
-
console.log(`🐛 Debug Mode: ${env.APP_DEBUG}`);
|
|
105
|
+
const config = loadConfig(root);
|
|
71
106
|
|
|
72
|
-
|
|
73
|
-
|
|
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
|
-
|
|
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 + '.
|
|
156
|
+
const controllerPath = path.join(root, 'app', 'controllers', controllerName + '.lmp');
|
|
106
157
|
|
|
107
|
-
if (!fs.existsSync(controllerPath)) throw new Error(`Controller ${controllerName} not found
|
|
158
|
+
if (!fs.existsSync(controllerPath)) throw new Error(`Controller ${controllerName} not found!`);
|
|
108
159
|
|
|
109
|
-
|
|
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;
|
|
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
|
|
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
|
-
|
|
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
|
|
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>/);
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
}
|