lumpiajs 1.0.9 → 1.0.11
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 +43 -25
- package/lib/commands/build.js +162 -307
- package/lib/commands/create.js +58 -138
- package/lib/commands/serve.js +69 -112
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,46 +1,64 @@
|
|
|
1
1
|
# 🥟 LumpiaJS
|
|
2
2
|
|
|
3
|
-
**Bahasa Pemrograman Web dengan Kearifan Lokal Semarangan
|
|
3
|
+
**"Bahasa Pemrograman Web dengan Kearifan Lokal Semarangan."**
|
|
4
|
+
|
|
5
|
+
Framework Static SPA 100% Client-Side. Coding pakai bahasa sehari-hari.
|
|
4
6
|
|
|
5
7
|
---
|
|
6
8
|
|
|
7
|
-
##
|
|
9
|
+
## 🗣️ Kamus Bahasa
|
|
8
10
|
|
|
9
|
-
|
|
11
|
+
| Semarangan | JS Asli | Arti |
|
|
12
|
+
| :------------ | :------------ | :-------------------- |
|
|
13
|
+
| **`aku`** | `this` | Diri Sendiri (Object) |
|
|
14
|
+
| **`fungsi`** | `function` | Fungsi |
|
|
15
|
+
| **`paten`** | `const` | Konstan |
|
|
16
|
+
| **`ono`** | `let` | Ada / Variabel |
|
|
17
|
+
| **`mengko`** | `async` | Nanti (Async) |
|
|
18
|
+
| **`nteni`** | `await` | Tunggu (Await) |
|
|
19
|
+
| **`balek`** | `return` | Kembali |
|
|
20
|
+
| **`kandani`** | `console.log` | Bilangi |
|
|
10
21
|
|
|
11
|
-
|
|
22
|
+
_Plus fitur **Laravel Syntax**: `aku->tampil()`._
|
|
12
23
|
|
|
13
|
-
|
|
14
|
-
- Jika di **Vercel/Node**, server otomatis mengarahkan ke `api.js`.
|
|
24
|
+
**Contoh Coding (`HomeController.lmp`):**
|
|
15
25
|
|
|
16
|
-
|
|
26
|
+
```javascript
|
|
27
|
+
export default class HomeController extends Controller {
|
|
28
|
+
mengko index() {
|
|
29
|
+
paten pesan = 'Halo Lur!';
|
|
17
30
|
|
|
18
|
-
|
|
19
|
-
|
|
31
|
+
// Panggil fungsi view
|
|
32
|
+
balek aku->tampil('home', { msg: pesan });
|
|
33
|
+
}
|
|
34
|
+
}
|
|
20
35
|
```
|
|
21
36
|
|
|
22
|
-
|
|
37
|
+
---
|
|
23
38
|
|
|
24
|
-
|
|
39
|
+
## 🚀 Cara Pakai
|
|
25
40
|
|
|
26
|
-
1.
|
|
27
|
-
2. Edit **`api.php`** (Isi config database).
|
|
28
|
-
3. Selesai.
|
|
29
|
-
_Server otomatis pakai `.htaccess` untuk routing._
|
|
41
|
+
**1. Install**
|
|
30
42
|
|
|
31
|
-
|
|
43
|
+
```bash
|
|
44
|
+
npm install -g lumpiajs
|
|
45
|
+
```
|
|
32
46
|
|
|
33
|
-
|
|
34
|
-
2. Set Environment Variables di Vercel (`DB_HOST`, `DB_USER`, dll).
|
|
35
|
-
3. Selesai.
|
|
36
|
-
_Vercel otomatis baca `vercel.json` dan pakai `api.js` sebagai serverless function._
|
|
47
|
+
**2. Buat Project & Develop**
|
|
37
48
|
|
|
38
|
-
|
|
49
|
+
```bash
|
|
50
|
+
lumpia create-project warung-ku
|
|
51
|
+
cd warung-ku && npm install
|
|
52
|
+
lumpia kukus
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
**3. Build Static (Goreng)**
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
lumpia goreng
|
|
59
|
+
```
|
|
39
60
|
|
|
40
|
-
|
|
41
|
-
2. `npm install`
|
|
42
|
-
3. `npm start`
|
|
43
|
-
_Node akan menjalankan `server.js`._
|
|
61
|
+
Upload folder `dist` kemana saja (Hosting Biasa/GitHub Pages).
|
|
44
62
|
|
|
45
63
|
---
|
|
46
64
|
|
package/lib/commands/build.js
CHANGED
|
@@ -1,45 +1,75 @@
|
|
|
1
1
|
|
|
2
|
+
// ... (KAMUS dan Helper functions sama seperti sebelumnya) ...
|
|
3
|
+
// Saya akan fokus update browserCore dan indexHtml
|
|
4
|
+
|
|
5
|
+
const KAMUS = [
|
|
6
|
+
{ from: /paten\s/g, to: 'const ' },
|
|
7
|
+
{ from: /ono\s/g, to: 'let ' },
|
|
8
|
+
{ from: /fungsi\s/g, to: 'function ' },
|
|
9
|
+
{ from: /nteni\s/g, to: 'await ' },
|
|
10
|
+
{ from: /mengko\s/g, to: 'async ' },
|
|
11
|
+
{ from: /balek\s/g, to: 'return ' },
|
|
12
|
+
{ from: /yen\s*\(/g, to: 'if(' },
|
|
13
|
+
{ from: /liyane\s/g, to: 'else ' },
|
|
14
|
+
{ from: /jajal\s*\{/g, to: 'try {' },
|
|
15
|
+
{ from: /gagal\s*\(/g, to: 'catch(' },
|
|
16
|
+
{ from: /kandani\(/g, to: 'console.log(' },
|
|
17
|
+
{ from: /aku->/g, to: 'this.' },
|
|
18
|
+
{ from: /aku\./g, to: 'this.' },
|
|
19
|
+
{ from: /->/g, to: '.' }
|
|
20
|
+
];
|
|
21
|
+
|
|
2
22
|
import fs from 'fs';
|
|
3
23
|
import path from 'path';
|
|
4
24
|
import { spawnSync } from 'child_process';
|
|
5
25
|
import { loadConfig } from '../core/Config.js';
|
|
6
26
|
|
|
7
|
-
|
|
8
|
-
function transpileContent(content) {
|
|
27
|
+
function transpileSemarangan(content) {
|
|
9
28
|
let code = content;
|
|
10
29
|
code = code.replace(/from\s+['"](.+?)\.lmp['"]/g, "from '$1.js'");
|
|
30
|
+
code = code.replace(/from\s+['"]lumpiajs['"]/g, "from './core/lumpia.js'"); // FIX: Relative import to core
|
|
31
|
+
|
|
32
|
+
// Handle imports agar path-nya relatif browser freindly
|
|
33
|
+
// Jika import './...' -> aman.
|
|
34
|
+
// Jika import '/...' -> bahaya kalau di subfolder.
|
|
35
|
+
|
|
11
36
|
code = code.split('\n').map(line => {
|
|
12
|
-
|
|
13
|
-
|
|
37
|
+
let l = line;
|
|
38
|
+
if (l.trim().startsWith('//')) return l;
|
|
39
|
+
KAMUS.forEach(rule => { l = l.replace(rule.from, rule.to); });
|
|
40
|
+
return l;
|
|
14
41
|
}).join('\n');
|
|
15
|
-
|
|
16
|
-
// Rewrite imports for Browser
|
|
17
|
-
code = code.replace(/from\s+['"]lumpiajs['"]/g, "from '/core/index.js'");
|
|
18
|
-
code = code.replace(/from\s+['"]lumpiajs\/lib\/(.+?)['"]/g, "from '/core/$1'");
|
|
19
42
|
return code;
|
|
20
43
|
}
|
|
21
44
|
|
|
22
|
-
function processDirectory(source, dest) {
|
|
45
|
+
function processDirectory(source, dest, rootDepth = 0) {
|
|
23
46
|
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
24
|
-
|
|
25
|
-
const items = fs.readdirSync(source);
|
|
26
|
-
items.forEach(item => {
|
|
47
|
+
fs.readdirSync(source).forEach(item => {
|
|
27
48
|
const srcPath = path.join(source, item);
|
|
28
49
|
const destPath = path.join(dest, item);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
if (stat.isDirectory()) {
|
|
32
|
-
processDirectory(srcPath, destPath);
|
|
50
|
+
if (fs.statSync(srcPath).isDirectory()) {
|
|
51
|
+
processDirectory(srcPath, destPath, rootDepth + 1);
|
|
33
52
|
} else {
|
|
34
|
-
if (item.endsWith('.lmp')) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
53
|
+
if (item.endsWith('.lmp') || item.endsWith('.js')) {
|
|
54
|
+
let content = fs.readFileSync(srcPath, 'utf8');
|
|
55
|
+
|
|
56
|
+
// ADJUST IMPORT PATHS FOR BROWSER (CRITICAL!)
|
|
57
|
+
// Controller/Route ada di kedalaman tertentu. Core ada di /dist/core.
|
|
58
|
+
// Kita harus rewrite "from 'lumpiajs'" menjadi path relative yang benar ke core.
|
|
59
|
+
// Misal dari /app/controllers (depth 2) -> '../../core/lumpia.js'
|
|
60
|
+
|
|
61
|
+
// Hitung relative backstep
|
|
62
|
+
let backSteps = '../'.repeat(rootDepth);
|
|
63
|
+
// Karena structure dist:
|
|
64
|
+
// dist/routes/web.js (depth 1) -> butuh '../core/lumpia.js'
|
|
65
|
+
// dist/app/controllers/Home.js (depth 2) -> butuh '../../core/lumpia.js'
|
|
66
|
+
// Jadi logicnya benar.
|
|
67
|
+
|
|
68
|
+
content = content.replace(/from\s+['"]lumpiajs['"]/g, `from '${backSteps}../core/lumpia.js'`);
|
|
69
|
+
|
|
70
|
+
content = transpileSemarangan(content);
|
|
71
|
+
const finalDest = destPath.replace('.lmp', '.js');
|
|
72
|
+
fs.writeFileSync(finalDest, content);
|
|
43
73
|
} else {
|
|
44
74
|
fs.copyFileSync(srcPath, destPath);
|
|
45
75
|
}
|
|
@@ -47,247 +77,119 @@ function processDirectory(source, dest) {
|
|
|
47
77
|
});
|
|
48
78
|
}
|
|
49
79
|
|
|
50
|
-
|
|
51
|
-
const phpBridgeContent = `<?php
|
|
52
|
-
header("Access-Control-Allow-Origin: *");
|
|
53
|
-
header("Access-Control-Allow-Headers: Content-Type");
|
|
54
|
-
header("Content-Type: application/json");
|
|
55
|
-
|
|
56
|
-
// ⚠️ EDIT CONFIG INI
|
|
57
|
-
$host = "localhost";
|
|
58
|
-
$user = "root";
|
|
59
|
-
$pass = "";
|
|
60
|
-
$db = "lumpia_db";
|
|
61
|
-
|
|
62
|
-
$conn = new mysqli($host, $user, $pass, $db);
|
|
63
|
-
if ($conn->connect_error) die(json_encode(["error" => $conn->connect_error]));
|
|
64
|
-
|
|
65
|
-
$input = json_decode(file_get_contents('php://input'), true);
|
|
66
|
-
if (!$input) exit;
|
|
67
|
-
|
|
68
|
-
try {
|
|
69
|
-
$stmt = $conn->prepare($input['sql']);
|
|
70
|
-
if($input['params']) {
|
|
71
|
-
$types = str_repeat("s", count($input['params']));
|
|
72
|
-
$stmt->bind_param($types, ...$input['params']);
|
|
73
|
-
}
|
|
74
|
-
$stmt->execute();
|
|
75
|
-
$res = $stmt->get_result();
|
|
76
|
-
$data = $res ? $res->fetch_all(MYSQLI_ASSOC) : ["affected" => $stmt->affected_rows];
|
|
77
|
-
echo json_encode($data);
|
|
78
|
-
} catch (Exception $e) {
|
|
79
|
-
http_response_code(500);
|
|
80
|
-
echo json_encode(["error" => $e->getMessage()]);
|
|
81
|
-
}
|
|
82
|
-
$conn->close();
|
|
83
|
-
?>`;
|
|
84
|
-
|
|
85
|
-
// --- 2. NODE.JS / VERCEL BACKEND ADAPTER ---
|
|
86
|
-
// Ini file 'api.js' yang akan dijalankan oleh Vercel atau Server.js local
|
|
87
|
-
const nodeBridgeContent = `
|
|
88
|
-
import { createPool } from 'mysql2/promise';
|
|
89
|
-
|
|
90
|
-
// ⚠️ CONFIG DARI ENV (Vercel/Node style)
|
|
91
|
-
const pool = createPool({
|
|
92
|
-
host: process.env.DB_HOST || 'localhost',
|
|
93
|
-
user: process.env.DB_USER || 'root',
|
|
94
|
-
password: process.env.DB_PASSWORD || '',
|
|
95
|
-
database: process.env.DB_NAME || 'lumpia_db',
|
|
96
|
-
waitForConnections: true,
|
|
97
|
-
connectionLimit: 10
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
export default async function handler(req, res) {
|
|
101
|
-
// Vercel / Express handler signature
|
|
102
|
-
if (req.method !== 'POST') {
|
|
103
|
-
res.statusCode = 405;
|
|
104
|
-
return res.end('Method Not Allowed');
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
try {
|
|
108
|
-
// Parsing body helper
|
|
109
|
-
let body = req.body;
|
|
110
|
-
if (typeof body === 'string') body = JSON.parse(body); // if raw string
|
|
111
|
-
|
|
112
|
-
const { sql, params } = body;
|
|
113
|
-
const [rows] = await pool.execute(sql, params);
|
|
114
|
-
|
|
115
|
-
res.statusCode = 200;
|
|
116
|
-
res.setHeader('Content-Type', 'application/json');
|
|
117
|
-
res.end(JSON.stringify(rows));
|
|
118
|
-
} catch (error) {
|
|
119
|
-
console.error(error);
|
|
120
|
-
res.statusCode = 500;
|
|
121
|
-
res.setHeader('Content-Type', 'application/json');
|
|
122
|
-
res.end(JSON.stringify({ error: error.message }));
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
`;
|
|
126
|
-
|
|
127
|
-
// --- 3. SERVER.JS (Standalone Node Server) ---
|
|
128
|
-
// Server statis + API Handler
|
|
129
|
-
const serverJsContent = `
|
|
130
|
-
import http from 'http';
|
|
131
|
-
import fs from 'fs';
|
|
132
|
-
import path from 'path';
|
|
133
|
-
import apiHandler from './api.js';
|
|
134
|
-
|
|
135
|
-
const root = process.cwd();
|
|
136
|
-
|
|
137
|
-
const server = http.createServer(async (req, res) => {
|
|
138
|
-
const url = new URL(req.url, 'http://' + req.headers.host);
|
|
139
|
-
|
|
140
|
-
// API ROUTE
|
|
141
|
-
if (url.pathname === '/api') {
|
|
142
|
-
let body = '';
|
|
143
|
-
req.on('data', chunk => body += chunk);
|
|
144
|
-
req.on('end', () => {
|
|
145
|
-
req.body = body ? JSON.parse(body) : {};
|
|
146
|
-
apiHandler(req, res);
|
|
147
|
-
});
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// STATIC FILES
|
|
152
|
-
let filePath = path.join(root, url.pathname === '/' ? 'index.html' : url.pathname);
|
|
153
|
-
|
|
154
|
-
// SPA Fallback: If not file, serve index.html
|
|
155
|
-
if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) {
|
|
156
|
-
filePath = path.join(root, 'index.html');
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const ext = path.extname(filePath);
|
|
160
|
-
const mime = { '.html': 'text/html', '.js': 'text/javascript', '.css': 'text/css' };
|
|
161
|
-
res.writeHead(200, { 'Content-Type': mime[ext] || 'application/octet-stream' });
|
|
162
|
-
fs.createReadStream(filePath).pipe(res);
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
const port = process.env.PORT || 3000;
|
|
166
|
-
server.listen(port, () => console.log('🚀 Server running on port ' + port));
|
|
167
|
-
`;
|
|
168
|
-
|
|
169
|
-
// --- 4. BROWSER CORE (Polymorphic Client) ---
|
|
170
|
-
const browserCoreIndex = `
|
|
80
|
+
const browserCore = `
|
|
171
81
|
export class Controller {
|
|
172
|
-
constructor() { this.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
body =
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
document.
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// POLYMORPHIC DB CLIENT
|
|
214
|
-
export class DB {
|
|
215
|
-
static table(name) { return new QueryBuilder(name); }
|
|
216
|
-
static async query(sql, params) {
|
|
217
|
-
// Tembak ke endpoint /api
|
|
218
|
-
// Server (Apache/Vercel/Node) yang akan nentuin diteruske ke api.php atau api.js
|
|
219
|
-
const res = await fetch('/api', {
|
|
220
|
-
method: 'POST',
|
|
221
|
-
headers: {'Content-Type': 'application/json'},
|
|
222
|
-
body: JSON.stringify({sql, params})
|
|
223
|
-
});
|
|
224
|
-
return await res.json();
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
class QueryBuilder {
|
|
229
|
-
constructor(table) { this.table = table; this.conds = []; this.binds = []; }
|
|
230
|
-
where(c, o, v) { if(v===undefined){v=o;o='=';} this.conds.push(c+' '+o+' ?'); this.binds.push(v); return this; }
|
|
231
|
-
orderBy(c, d='ASC') { this.order = c+' '+d; return this; }
|
|
232
|
-
async get() {
|
|
233
|
-
let sql = 'SELECT * FROM ' + this.table;
|
|
234
|
-
if(this.conds.length) sql += ' WHERE ' + this.conds.join(' AND ');
|
|
235
|
-
if(this.order) sql += ' ORDER BY ' + this.order;
|
|
236
|
-
return await DB.query(sql, this.binds);
|
|
82
|
+
constructor() { this.params={}; }
|
|
83
|
+
async tampil(viewName, data={}) {
|
|
84
|
+
try {
|
|
85
|
+
// Fetch view relative to root (we assume <base> tag is set or we detect root)
|
|
86
|
+
// Biar aman, kita cari 'views' relatif terhadap posisi script ini? Gak bisa.
|
|
87
|
+
// Kita asumsi deployment di root atau subfolder dengan <base> tag HTML yang benar.
|
|
88
|
+
|
|
89
|
+
const res = await fetch('views/'+viewName+'.lmp');
|
|
90
|
+
if(!res.ok) throw new Error('View 404: ' + viewName);
|
|
91
|
+
let html = await res.text();
|
|
92
|
+
|
|
93
|
+
// ... (Parsing logic same as before) ...
|
|
94
|
+
const matchKulit = html.match(/<kulit>([\\s\\S]*?)<\\/kulit>/);
|
|
95
|
+
const matchIsi = html.match(/<isi>([\\s\\S]*?)<\\/isi>/);
|
|
96
|
+
const matchKlambi = html.match(/<klambi>([\\s\\S]*?)<\\/klambi>/);
|
|
97
|
+
|
|
98
|
+
let body = matchKulit?matchKulit[1]:'', script=matchIsi?matchIsi[1]:'', css=matchKlambi?matchKlambi[1]:'';
|
|
99
|
+
|
|
100
|
+
for(const [k,v] of Object.entries(data)) {
|
|
101
|
+
let s = typeof v === 'object' ? JSON.stringify(v) : v;
|
|
102
|
+
body = body.replace(new RegExp('{{\\\\s*'+k+'\\\\s*}}','g'), s);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
document.getElementById('app').innerHTML = body;
|
|
106
|
+
if(css && !document.getElementById('css-'+viewName)) {
|
|
107
|
+
const s = document.createElement('style'); s.id='css-'+viewName; s.textContent=css;
|
|
108
|
+
document.head.appendChild(s);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if(script) {
|
|
112
|
+
const kamus = [
|
|
113
|
+
{f:/paten\\s/g,t:'const '}, {f:/ono\\s/g,t:'let '}, {f:/fungsi\\s/g,t:'function '},
|
|
114
|
+
{f:/nteni\\s/g,t:'await '}, {f:/mengko\\s/g,t:'async '}, {f:/balek\\s/g,t:'return '},
|
|
115
|
+
{f:/yen\\s*\\(/g,t:'if('}, {f:/liyane\\s/g,t:'else '}, {f:/kandani\\(/g,t:'console.log('},
|
|
116
|
+
{f:/aku->/g,t:'this.'}, {f:/aku\\./g,t:'this.'}
|
|
117
|
+
];
|
|
118
|
+
kamus.forEach(r => script = script.replace(r.f, r.t));
|
|
119
|
+
try { new Function(script)(); } catch(e){ console.error(e); }
|
|
120
|
+
}
|
|
121
|
+
} catch(e) { document.getElementById('app').innerHTML = e.message; }
|
|
237
122
|
}
|
|
238
123
|
}
|
|
239
|
-
|
|
240
|
-
export const Jalan = {
|
|
241
|
-
routes: [],
|
|
242
|
-
get: (p, a) => Jalan.routes.push({p, a, m:'GET'}),
|
|
243
|
-
post: (p, a) => Jalan.routes.push({p, a, m:'POST'})
|
|
244
|
-
};
|
|
124
|
+
export const Jalan = { routes:[], get:(p,a)=>Jalan.routes.push({p,a}) };
|
|
245
125
|
`;
|
|
246
126
|
|
|
247
|
-
const
|
|
127
|
+
const indexHtml = `<!DOCTYPE html>
|
|
248
128
|
<html lang="en">
|
|
249
129
|
<head>
|
|
250
130
|
<meta charset="UTF-8">
|
|
251
|
-
<meta name="viewport" content="width=device-width,
|
|
252
|
-
<title>LumpiaJS
|
|
253
|
-
|
|
131
|
+
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
|
132
|
+
<title>LumpiaJS</title>
|
|
133
|
+
<!-- BASE HREF AUTOMATIC DETECTION MAGIC -->
|
|
134
|
+
<script>
|
|
135
|
+
// Set base href to current folder so imports work in subfolders
|
|
136
|
+
// But for SPA routing, this might fight with History API.
|
|
137
|
+
// Let's rely on relative imports './' everywhere.
|
|
138
|
+
// document.write("<base href='" + document.location.pathname.replace(/index\\.html$/,'') + "' />");
|
|
139
|
+
</script>
|
|
140
|
+
<link rel="stylesheet" href="public/css/style.css">
|
|
254
141
|
</head>
|
|
255
142
|
<body>
|
|
256
143
|
<div id="app">Loading...</div>
|
|
257
|
-
|
|
144
|
+
|
|
258
145
|
<script type="module">
|
|
259
|
-
|
|
146
|
+
// Import paths WITHOUT leading slash to be relative to index.html
|
|
147
|
+
import { Jalan } from './routes/web.js';
|
|
260
148
|
|
|
261
149
|
async function navigate() {
|
|
262
|
-
|
|
263
|
-
|
|
150
|
+
// Normalize path: Remove base folder logic needed?
|
|
151
|
+
// Simple approach: Match pathname against routes.
|
|
152
|
+
// If app is in /my-app/, and route is '/', browser pathname is '/my-app/'.
|
|
153
|
+
// WE NEED TO STRIP THE BASE PATH.
|
|
154
|
+
|
|
155
|
+
// Hacky detection of Base Path (where index.html sits)
|
|
156
|
+
const basePath = window.location.pathname.replace(/\/index\.html$/, '').replace(/\/$/, '');
|
|
157
|
+
let currentPath = window.location.pathname.replace(/\/index\.html$/, '');
|
|
158
|
+
if (basePath && currentPath.startsWith(basePath)) {
|
|
159
|
+
currentPath = currentPath.substring(basePath.length);
|
|
160
|
+
}
|
|
161
|
+
if (!currentPath) currentPath = '/'; // Root
|
|
162
|
+
|
|
163
|
+
let m = null, args = {};
|
|
264
164
|
for(let r of Jalan.routes) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
if(
|
|
165
|
+
// Route defined as '/home'
|
|
166
|
+
let reg = new RegExp('^'+r.p.replace(/{([a-zA-Z0-9_]+)}/g, '([^/]+)')+'$');
|
|
167
|
+
let res = currentPath.match(reg);
|
|
168
|
+
if(res){ m=r; args=res.slice(1); break; }
|
|
269
169
|
}
|
|
270
170
|
|
|
271
|
-
if(
|
|
272
|
-
const [cName,
|
|
171
|
+
if(m) {
|
|
172
|
+
const [cName, fName] = m.a.split('@');
|
|
273
173
|
try {
|
|
274
|
-
|
|
275
|
-
const
|
|
276
|
-
const
|
|
277
|
-
await
|
|
278
|
-
} catch(e) {
|
|
279
|
-
|
|
174
|
+
// Import Relative to index.html
|
|
175
|
+
const mod = await import('./app/controllers/'+cName+'.js?'+Date.now());
|
|
176
|
+
const C = mod.default; const i = new C(); i.params=args;
|
|
177
|
+
await i[fName](...args);
|
|
178
|
+
} catch(e) {
|
|
179
|
+
console.error(e);
|
|
180
|
+
document.getElementById('app').innerHTML='<h1>Error Loading Controller</h1><pre>'+e.message+'</pre>';
|
|
280
181
|
}
|
|
281
|
-
} else {
|
|
282
|
-
|
|
182
|
+
} else {
|
|
183
|
+
document.getElementById('app').innerHTML='<h1>404 Not Found</h1><p>Path: '+currentPath+'</p>';
|
|
283
184
|
}
|
|
284
185
|
}
|
|
285
186
|
|
|
286
187
|
window.addEventListener('popstate', navigate);
|
|
287
188
|
document.body.addEventListener('click', e => {
|
|
288
|
-
if(e.target.tagName
|
|
289
|
-
e.preventDefault();
|
|
290
|
-
|
|
189
|
+
if(e.target.tagName==='A' && e.target.href.startsWith(window.location.origin)) {
|
|
190
|
+
e.preventDefault();
|
|
191
|
+
// Push relative path?
|
|
192
|
+
history.pushState(null,'',e.target.href);
|
|
291
193
|
navigate();
|
|
292
194
|
}
|
|
293
195
|
});
|
|
@@ -296,14 +198,12 @@ const indexHtmlContent = `<!DOCTYPE html>
|
|
|
296
198
|
</body>
|
|
297
199
|
</html>`;
|
|
298
200
|
|
|
299
|
-
|
|
300
201
|
export function buildProject() {
|
|
301
202
|
const root = process.cwd();
|
|
302
203
|
const dist = path.join(root, 'dist');
|
|
303
204
|
const config = loadConfig(root);
|
|
304
|
-
|
|
305
|
-
console.log('🍳
|
|
306
|
-
|
|
205
|
+
|
|
206
|
+
console.log('🍳 Goreng Project (Mode Bahasa Semarangan + Relative Paths)...');
|
|
307
207
|
if (fs.existsSync(dist)) fs.rmSync(dist, { recursive: true, force: true });
|
|
308
208
|
fs.mkdirSync(dist);
|
|
309
209
|
|
|
@@ -312,69 +212,24 @@ export function buildProject() {
|
|
|
312
212
|
spawnSync(cmd, ['tailwindcss', '-i', './aset/css/style.css', '-o', './public/css/style.css', '--minify'], { cwd: root, stdio: 'ignore', shell: true });
|
|
313
213
|
}
|
|
314
214
|
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
processDirectory(path.join(root, '
|
|
215
|
+
// Process using Relative Path Logic
|
|
216
|
+
// We pass 'rootDepth' to calculate imports to 'core'
|
|
217
|
+
processDirectory(path.join(root, 'app'), path.join(dist, 'app'), 2); // app/controllers/X -> depth 2 from dist
|
|
218
|
+
processDirectory(path.join(root, 'routes'), path.join(dist, 'routes'), 1); // routes/X -> depth 1 from dist
|
|
318
219
|
|
|
319
|
-
fs.mkdirSync(path.join(dist, 'core'), { recursive: true });
|
|
320
|
-
fs.writeFileSync(path.join(dist, 'core', 'index.js'), browserCoreIndex);
|
|
321
|
-
|
|
322
220
|
fs.cpSync(path.join(root, 'views'), path.join(dist, 'views'), { recursive: true });
|
|
323
|
-
|
|
324
|
-
if (fs.existsSync(path.join(root, 'public'))) {
|
|
325
|
-
fs.cpSync(path.join(root, 'public'), path.join(dist, 'public'), { recursive: true });
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
fs.writeFileSync(path.join(dist, 'index.html'), indexHtmlContent);
|
|
329
|
-
|
|
330
|
-
// --- GENERATE ALL ADAPTERS ---
|
|
331
|
-
|
|
332
|
-
// 1. PHP Adapter
|
|
333
|
-
fs.writeFileSync(path.join(dist, 'api.php'), phpBridgeContent);
|
|
334
|
-
|
|
335
|
-
// 2. Node/Vercel Adapter
|
|
336
|
-
fs.writeFileSync(path.join(dist, 'api.js'), nodeBridgeContent);
|
|
337
|
-
fs.writeFileSync(path.join(dist, 'server.js'), serverJsContent);
|
|
338
|
-
fs.writeFileSync(path.join(dist, 'package.json'), JSON.stringify({
|
|
339
|
-
"type": "module",
|
|
340
|
-
"scripts": { "start": "node server.js" },
|
|
341
|
-
"dependencies": { "mysql2": "^3.0.0" }
|
|
342
|
-
}, null, 2));
|
|
221
|
+
if (fs.existsSync(path.join(root, 'public'))) fs.cpSync(path.join(root, 'public'), path.join(dist, 'public'), { recursive: true });
|
|
343
222
|
|
|
344
|
-
|
|
223
|
+
fs.mkdirSync(path.join(dist, 'core'), { recursive: true });
|
|
224
|
+
fs.writeFileSync(path.join(dist, 'core', 'lumpia.js'), browserCore);
|
|
225
|
+
fs.writeFileSync(path.join(dist, 'index.html'), indexHtml);
|
|
345
226
|
|
|
346
|
-
// .htaccess
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
# SPA Routing: Everything else -> index.html
|
|
356
|
-
RewriteCond %{REQUEST_FILENAME} !-f
|
|
357
|
-
RewriteCond %{REQUEST_FILENAME} !-d
|
|
358
|
-
RewriteRule . /index.html [L]
|
|
359
|
-
</IfModule>
|
|
360
|
-
`);
|
|
361
|
-
|
|
362
|
-
// vercel.json (Vercel) -> Redirect /api ke api.js
|
|
363
|
-
fs.writeFileSync(path.join(dist, 'vercel.json'), JSON.stringify({
|
|
364
|
-
"rewrites": [
|
|
365
|
-
{ "source": "/api", "destination": "/api.js" },
|
|
366
|
-
{ "source": "/(.*)", "destination": "/index.html" }
|
|
367
|
-
],
|
|
368
|
-
"functions": {
|
|
369
|
-
"api.js": { "includeFiles": "package.json" }
|
|
370
|
-
}
|
|
371
|
-
}, null, 2));
|
|
372
|
-
|
|
373
|
-
console.log('✅ Mateng! (Universal Build)');
|
|
374
|
-
console.log('----------------------------------------------------');
|
|
375
|
-
console.log('📂 Folder "dist" ini UNIVERSAL:');
|
|
376
|
-
console.log(' - Hosting PHP (XAMPP/cPanel): Otomatis pake api.php (via .htaccess)');
|
|
377
|
-
console.log(' - Vercel: Otomatis pake api.js (via vercel.json)');
|
|
378
|
-
console.log(' - Node VPS: Otomatis pake api.js (via server.js)');
|
|
379
|
-
console.log('----------------------------------------------------');
|
|
227
|
+
// .htaccess for "Subfolder Friendly" SPA?
|
|
228
|
+
// RewriteRule ^index\.html$ - [L]
|
|
229
|
+
// RewriteCond %{REQUEST_FILENAME} !-f
|
|
230
|
+
// RewriteRule . index.html [L]
|
|
231
|
+
// Note: Removed leading slash in redirect destination to be relative
|
|
232
|
+
fs.writeFileSync(path.join(dist, '.htaccess'), `<IfModule mod_rewrite.c>\nRewriteEngine On\nRewriteRule ^index\\.html$ - [L]\nRewriteCond %{REQUEST_FILENAME} !-f\nRewriteCond %{REQUEST_FILENAME} !-d\nRewriteRule . index.html [L]\n</IfModule>`);
|
|
233
|
+
|
|
234
|
+
console.log('✅ Mateng! (Support Subfolder Hosting)');
|
|
380
235
|
}
|
package/lib/commands/create.js
CHANGED
|
@@ -4,155 +4,84 @@ import prompts from 'prompts';
|
|
|
4
4
|
|
|
5
5
|
const routesTemplate = `import { Jalan } from 'lumpiajs';
|
|
6
6
|
|
|
7
|
-
// Pakai gaya Laravel (->) enak to?
|
|
8
7
|
Jalan->get('/', 'HomeController@index');
|
|
9
|
-
Jalan->get('/
|
|
10
|
-
Jalan->get('/profile', 'HomeController@profile');
|
|
11
|
-
Jalan->get('/api/products', 'ProductController@index');
|
|
8
|
+
Jalan->get('/toko', 'ProductController@tampilBarang');
|
|
12
9
|
`;
|
|
13
10
|
|
|
14
|
-
const controllerTemplate = `import { Controller
|
|
11
|
+
const controllerTemplate = `import { Controller } from 'lumpiajs';
|
|
15
12
|
|
|
16
13
|
export default class HomeController extends Controller {
|
|
17
|
-
index() {
|
|
18
|
-
//
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
14
|
+
mengko index() {
|
|
15
|
+
// Pake Bahasa Semarangan, Lur!
|
|
16
|
+
paten pesen = 'Sugeng Rawuh di Website Statis!';
|
|
17
|
+
|
|
18
|
+
// 'aku' menggantikan 'this'
|
|
19
|
+
balek aku->tampil('home', {
|
|
20
|
+
message: pesen,
|
|
21
|
+
info: 'Dibuat dengan LumpiaJS'
|
|
24
22
|
});
|
|
25
23
|
}
|
|
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
|
-
|
|
48
|
-
profile() {
|
|
49
|
-
return this->tampil('profile', { name: 'Loyal User' });
|
|
50
|
-
}
|
|
51
24
|
}
|
|
52
25
|
`;
|
|
53
26
|
|
|
54
|
-
const
|
|
55
|
-
const productData = [
|
|
56
|
-
{ id: 1, name: 'Lumpia Basah', price: 5000 },
|
|
57
|
-
{ id: 2, name: 'Lumpia Goreng', price: 6000 }
|
|
58
|
-
];
|
|
59
|
-
export default productData;
|
|
60
|
-
`;
|
|
61
|
-
|
|
62
|
-
const productControllerTemplate = `import { Controller, Model } from 'lumpiajs';
|
|
63
|
-
import ProductData from '../../app/models/Product.lmp';
|
|
27
|
+
const productControllerTemplate = `import { Controller } from 'lumpiajs';
|
|
64
28
|
|
|
65
29
|
export default class ProductController extends Controller {
|
|
66
|
-
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
30
|
+
mengko tampilBarang() {
|
|
31
|
+
// Contoh API
|
|
32
|
+
ono data = [];
|
|
33
|
+
|
|
34
|
+
jajal {
|
|
35
|
+
paten respon = nteni fetch('https://fakestoreapi.com/products?limit=3');
|
|
36
|
+
data = nteni respon.json();
|
|
37
|
+
kandani('Data sukses!');
|
|
38
|
+
} gagal (e) {
|
|
39
|
+
kandani(e);
|
|
40
|
+
}
|
|
71
41
|
|
|
72
|
-
|
|
42
|
+
balek aku->tampil('product', {
|
|
43
|
+
daftar: data.map(i => '<li>' + i.title + '</li>').join('')
|
|
44
|
+
});
|
|
73
45
|
}
|
|
74
46
|
}
|
|
75
47
|
`;
|
|
76
48
|
|
|
77
|
-
const
|
|
49
|
+
const homeViewTemplate = `<lump>
|
|
50
|
+
<klambi>
|
|
51
|
+
h1 { color: #d35400; text-align: center; }
|
|
52
|
+
</klambi>
|
|
78
53
|
<kulit>
|
|
79
|
-
<div
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
54
|
+
<div style="text-align: center; font-family: sans-serif; margin-top: 50px;">
|
|
55
|
+
<h1>{{ message }}</h1>
|
|
56
|
+
<p>{{ info }}</p>
|
|
57
|
+
<a href="/toko">Cek Toko Sebelah</a>
|
|
83
58
|
</div>
|
|
84
59
|
</kulit>
|
|
85
60
|
</lump>`;
|
|
86
61
|
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
DB_NAME="lumpia_db"
|
|
97
|
-
`;
|
|
98
|
-
|
|
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
|
-
};
|
|
62
|
+
const productViewTemplate = `<lump>
|
|
63
|
+
<kulit>
|
|
64
|
+
<div style="padding: 20px; font-family: sans-serif;">
|
|
65
|
+
<h1>Daftar Barang</h1>
|
|
66
|
+
<ul>{{ daftar }}</ul>
|
|
67
|
+
<a href="/">Balik Omah</a>
|
|
68
|
+
</div>
|
|
69
|
+
</kulit>
|
|
70
|
+
</lump>`;
|
|
139
71
|
|
|
140
72
|
const generatePackageJson = (name, style) => {
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
devDependencies
|
|
146
|
-
|
|
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);
|
|
73
|
+
return JSON.stringify({
|
|
74
|
+
name, version: "1.0.0", type: "module",
|
|
75
|
+
scripts: { "start": "lumpia kukus", "build": "lumpia goreng" },
|
|
76
|
+
dependencies: { "lumpiajs": "latest" },
|
|
77
|
+
devDependencies: style==='tailwindcss'?{"tailwindcss":"^3.4.0"}:{}
|
|
78
|
+
}, null, 2);
|
|
150
79
|
};
|
|
151
80
|
|
|
152
81
|
export async function createProject(parameter) {
|
|
153
82
|
let projectName = parameter;
|
|
154
83
|
if (!projectName) {
|
|
155
|
-
const res = await prompts({ type: 'text', name: 'val', message: 'Jeneng project?', initial: 'my-app' });
|
|
84
|
+
const res = await prompts({ type: 'text', name: 'val', message: 'Jeneng project?', initial: 'my-lumpia-app' });
|
|
156
85
|
projectName = res.val;
|
|
157
86
|
}
|
|
158
87
|
if (!projectName) return;
|
|
@@ -162,42 +91,33 @@ export async function createProject(parameter) {
|
|
|
162
91
|
|
|
163
92
|
const styleRes = await prompts({
|
|
164
93
|
type: 'select', name: 'val', message: 'Styling?',
|
|
165
|
-
choices: [{title:'Vanilla',value:'none'},{title:'Tailwind',value:'tailwindcss'}
|
|
94
|
+
choices: [{title:'Vanilla',value:'none'},{title:'Tailwind',value:'tailwindcss'}],
|
|
166
95
|
initial: 0
|
|
167
96
|
});
|
|
168
|
-
const style = styleRes.val;
|
|
169
|
-
if (!style) return;
|
|
170
97
|
|
|
171
|
-
// Structure
|
|
172
98
|
fs.mkdirSync(root);
|
|
173
99
|
fs.mkdirSync(path.join(root, 'app', 'controllers'), { recursive: true });
|
|
174
|
-
fs.mkdirSync(path.join(root, 'app', 'models'), { recursive: true });
|
|
175
100
|
fs.mkdirSync(path.join(root, 'routes'));
|
|
176
101
|
fs.mkdirSync(path.join(root, 'views'));
|
|
177
102
|
fs.mkdirSync(path.join(root, 'aset', 'css'), { recursive: true });
|
|
178
103
|
fs.mkdirSync(path.join(root, 'public', 'css'), { recursive: true });
|
|
179
104
|
|
|
180
|
-
|
|
181
|
-
fs.writeFileSync(path.join(root, 'package.json'), generatePackageJson(projectName, style));
|
|
105
|
+
fs.writeFileSync(path.join(root, 'package.json'), generatePackageJson(projectName, styleRes.val));
|
|
182
106
|
fs.writeFileSync(path.join(root, '.gitignore'), `.lumpia\nnode_modules\n.env\n`);
|
|
183
|
-
fs.writeFileSync(path.join(root, '.
|
|
184
|
-
fs.writeFileSync(path.join(root, 'config.lmp'), JSON.stringify({ klambi: style }, null, 2));
|
|
107
|
+
fs.writeFileSync(path.join(root, 'config.lmp'), JSON.stringify({ klambi: styleRes.val }, null, 2));
|
|
185
108
|
|
|
186
109
|
fs.writeFileSync(path.join(root, 'routes', 'web.lmp'), routesTemplate);
|
|
187
110
|
fs.writeFileSync(path.join(root, 'app', 'controllers', 'HomeController.lmp'), controllerTemplate);
|
|
188
111
|
fs.writeFileSync(path.join(root, 'app', 'controllers', 'ProductController.lmp'), productControllerTemplate);
|
|
189
|
-
fs.writeFileSync(path.join(root, '
|
|
190
|
-
fs.writeFileSync(path.join(root, 'views', '
|
|
191
|
-
fs.writeFileSync(path.join(root, 'views', 'profile.lmp'), viewProfileTemplate);
|
|
112
|
+
fs.writeFileSync(path.join(root, 'views', 'home.lmp'), homeViewTemplate);
|
|
113
|
+
fs.writeFileSync(path.join(root, 'views', 'product.lmp'), productViewTemplate);
|
|
192
114
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
fs.writeFileSync(path.join(root, '
|
|
196
|
-
fs.writeFileSync(path.join(root, 'aset', 'css', 'style.css'), mainCssTemplate);
|
|
115
|
+
if (styleRes.val === 'tailwindcss') {
|
|
116
|
+
fs.writeFileSync(path.join(root, 'tailwind.config.js'), `module.exports={content:["./views/**/*.lmp"],theme:{extend:{}},plugins:[]}`);
|
|
117
|
+
fs.writeFileSync(path.join(root, 'aset', 'css', 'style.css'), '@tailwind base; @tailwind components; @tailwind utilities;');
|
|
197
118
|
} else {
|
|
198
|
-
fs.writeFileSync(path.join(root, 'aset', 'css', 'style.css'),
|
|
119
|
+
fs.writeFileSync(path.join(root, 'aset', 'css', 'style.css'), '/* CSS */');
|
|
199
120
|
}
|
|
200
121
|
|
|
201
122
|
console.log(`✅ Project "${projectName}" Ready!`);
|
|
202
|
-
console.log(`cd ${projectName} && npm install && lumpia kukus`);
|
|
203
123
|
}
|
package/lib/commands/serve.js
CHANGED
|
@@ -2,58 +2,52 @@ import fs from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import http from 'http';
|
|
4
4
|
import { spawn } from 'child_process';
|
|
5
|
-
import { routes } from '../core/Router.js';
|
|
6
|
-
import { renderLumpia } from '../core/View.js';
|
|
7
5
|
import { loadEnv } from '../core/Env.js';
|
|
8
6
|
import { loadConfig } from '../core/Config.js';
|
|
9
7
|
|
|
10
|
-
const
|
|
8
|
+
const KAMUS = [
|
|
9
|
+
{ from: /paten\s/g, to: 'const ' },
|
|
10
|
+
{ from: /ono\s/g, to: 'let ' },
|
|
11
|
+
{ from: /fungsi\s/g, to: 'function ' },
|
|
12
|
+
{ from: /nteni\s/g, to: 'await ' },
|
|
13
|
+
{ from: /mengko\s/g, to: 'async ' },
|
|
14
|
+
{ from: /balek\s/g, to: 'return ' },
|
|
15
|
+
{ from: /yen\s*\(/g, to: 'if(' },
|
|
16
|
+
{ from: /liyane\s/g, to: 'else ' },
|
|
17
|
+
{ from: /jajal\s*\{/g, to: 'try {' },
|
|
18
|
+
{ from: /gagal\s*\(/g, to: 'catch(' },
|
|
19
|
+
{ from: /kandani\(/g, to: 'console.log(' },
|
|
20
|
+
{ from: /aku->/g, to: 'this.' },
|
|
21
|
+
{ from: /aku\./g, to: 'this.' },
|
|
22
|
+
{ from: /->/g, to: '.' }
|
|
23
|
+
];
|
|
11
24
|
|
|
12
|
-
// Helper to Match Routes
|
|
13
25
|
function matchRoute(definedRoute, method, pathname) {
|
|
14
26
|
if (definedRoute.method !== method) return null;
|
|
15
27
|
if (definedRoute.path === pathname) return { params: {} };
|
|
16
|
-
|
|
17
28
|
const paramNames = [];
|
|
18
29
|
const regexPath = definedRoute.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (match, name) => {
|
|
19
30
|
paramNames.push(name);
|
|
20
31
|
return '([^/]+)';
|
|
21
32
|
});
|
|
22
|
-
|
|
23
33
|
if (regexPath === definedRoute.path) return null;
|
|
24
34
|
const regex = new RegExp(`^${regexPath}$`);
|
|
25
35
|
const match = pathname.match(regex);
|
|
26
36
|
if (match) {
|
|
27
37
|
const params = {};
|
|
28
|
-
paramNames.forEach((name, index) => {
|
|
29
|
-
params[name] = match[index + 1];
|
|
30
|
-
});
|
|
38
|
+
paramNames.forEach((name, index) => { params[name] = match[index + 1]; });
|
|
31
39
|
return { params };
|
|
32
40
|
}
|
|
33
41
|
return null;
|
|
34
42
|
}
|
|
35
43
|
|
|
36
44
|
const backgroundProcess = [];
|
|
37
|
-
|
|
38
45
|
function startTailwindWatcher(root) {
|
|
39
|
-
console.log('🎨 TailwindCSS detected! Starting watcher...');
|
|
40
46
|
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 });
|
|
47
|
+
const tailwind = spawn(cmd, ['tailwindcss', '-i', './aset/css/style.css', '-o', './public/css/style.css', '--watch'], { cwd: root, stdio: 'ignore', shell: true });
|
|
44
48
|
backgroundProcess.push(tailwind);
|
|
45
49
|
}
|
|
46
50
|
|
|
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
51
|
async function loadLumpiaModule(filePath) {
|
|
58
52
|
const originalContent = fs.readFileSync(filePath, 'utf-8');
|
|
59
53
|
const cacheDir = path.join(process.cwd(), '.lumpia', 'cache');
|
|
@@ -63,72 +57,64 @@ async function loadLumpiaModule(filePath) {
|
|
|
63
57
|
const flatName = relativePath.replace(/[\/\\]/g, '_').replace('.lmp', '.js');
|
|
64
58
|
const destPath = path.join(cacheDir, flatName);
|
|
65
59
|
|
|
66
|
-
//
|
|
67
|
-
|
|
60
|
+
// INI DEV SERVER: Path ke Core Library bisa tetap absolute (package) atau relative
|
|
61
|
+
// Karena ini dijalankan di Node.js, 'lumpiajs' masih resolve ke node_modules dengan benar.
|
|
62
|
+
// TAPI content user (seperti `from 'lumpiajs'`) harus dijaga tetap valid buat Node.
|
|
63
|
+
// Jika user nulis `from 'lumpiajs'`, biarkan.
|
|
64
|
+
// Hanya transpile Semarangan -> JS.
|
|
68
65
|
|
|
69
|
-
|
|
66
|
+
let transcoded = originalContent;
|
|
67
|
+
|
|
68
|
+
// DEV MODE: Keep .lmp imports as .js imports
|
|
70
69
|
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)
|
|
70
|
+
// DEV MODE: Do NOT rewrite 'lumpiajs' import because Node handles it via package.json resolution
|
|
78
71
|
|
|
79
|
-
// Replace "->" dengan "."
|
|
80
72
|
transcoded = transcoded.split('\n').map(line => {
|
|
81
|
-
|
|
82
|
-
if (
|
|
83
|
-
|
|
84
|
-
|
|
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, '.');
|
|
73
|
+
let l = line;
|
|
74
|
+
if (l.trim().startsWith('//')) return l;
|
|
75
|
+
KAMUS.forEach(rule => l = l.replace(rule.from, rule.to));
|
|
76
|
+
return l;
|
|
88
77
|
}).join('\n');
|
|
89
78
|
|
|
90
79
|
fs.writeFileSync(destPath, transcoded);
|
|
91
|
-
|
|
92
|
-
// 4. Import file .js
|
|
93
80
|
const module = await import('file://' + destPath + '?t=' + Date.now());
|
|
94
81
|
return module;
|
|
95
82
|
}
|
|
96
83
|
|
|
97
|
-
|
|
98
84
|
export async function serveProject() {
|
|
99
85
|
const root = process.cwd();
|
|
100
|
-
|
|
101
86
|
const routesFile = path.join(root, 'routes', 'web.lmp');
|
|
102
|
-
if (!fs.existsSync(routesFile)) return console.log("❌
|
|
87
|
+
if (!fs.existsSync(routesFile)) return console.log("❌ Missing routes/web.lmp");
|
|
103
88
|
|
|
104
89
|
const env = loadEnv(root);
|
|
105
90
|
const config = loadConfig(root);
|
|
106
|
-
|
|
107
91
|
if (config.klambi === 'tailwindcss') startTailwindWatcher(root);
|
|
108
|
-
else if (config.klambi === 'bootstrap') handleBootstrap(root);
|
|
109
92
|
|
|
110
93
|
try {
|
|
111
94
|
await loadLumpiaModule(routesFile);
|
|
112
|
-
|
|
95
|
+
const activeRoutes = global.LumpiaRouter || [];
|
|
96
|
+
console.log(`🛣️ Routes registered: ${activeRoutes.length}`);
|
|
97
|
+
|
|
98
|
+
// Cek URL, jika '/' tapi masih loading, itu karena logic view belum sempurna?
|
|
99
|
+
// Di Dev Server (Node), kita merender HTML string langsung (SSR).
|
|
100
|
+
// Jadi tidak ada masalah "loading..." dari sisi client-side fetch.
|
|
101
|
+
// Jika dev server loading terus, itu karena res.end() tidak terpanggil.
|
|
102
|
+
|
|
103
|
+
// Periksa controller user: apakah memanggil `balek` (return)?
|
|
104
|
+
// Jika user lupa return, function return undefined.
|
|
105
|
+
// Logic di bawah menangani undefined result -> `res.end(String(result))` -> `undefined`.
|
|
106
|
+
// Browser akan menampilkan "undefined". Bukan loading terus.
|
|
107
|
+
// Loading terus biasanya hang.
|
|
113
108
|
|
|
114
|
-
// Info Syntax
|
|
115
|
-
console.log(`✨ Syntax Mode: PHP/Laravel Style (->) is enabled in .lmp files!`);
|
|
116
|
-
|
|
117
109
|
const server = http.createServer(async (req, res) => {
|
|
118
110
|
const method = req.method;
|
|
119
111
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
120
112
|
const pathname = url.pathname;
|
|
121
113
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const publicMap = {
|
|
125
|
-
'/css/': path.join(root, 'public', 'css'),
|
|
126
|
-
'/vendor/': path.join(root, 'public', 'vendor')
|
|
127
|
-
};
|
|
114
|
+
const publicMap = { '/css/': path.join(root, 'public', 'css'), '/vendor/': path.join(root, 'public', 'vendor') };
|
|
128
115
|
for (const [prefix, localPath] of Object.entries(publicMap)) {
|
|
129
116
|
if (pathname.startsWith(prefix)) {
|
|
130
|
-
const
|
|
131
|
-
const filePath = path.join(localPath, relativePath);
|
|
117
|
+
const filePath = path.join(localPath, pathname.slice(prefix.length));
|
|
132
118
|
if (fs.existsSync(filePath) && fs.lstatSync(filePath).isFile()) {
|
|
133
119
|
const ext = path.extname(filePath);
|
|
134
120
|
const mime = ext === '.css' ? 'text/css' : (ext === '.js' ? 'text/javascript' : 'application/octet-stream');
|
|
@@ -139,71 +125,42 @@ export async function serveProject() {
|
|
|
139
125
|
}
|
|
140
126
|
}
|
|
141
127
|
|
|
142
|
-
let match = null;
|
|
143
|
-
|
|
144
|
-
for (const route of
|
|
128
|
+
let match = null, params = {};
|
|
129
|
+
const currentRoutes = global.LumpiaRouter || [];
|
|
130
|
+
for (const route of currentRoutes) {
|
|
145
131
|
const result = matchRoute(route, method, pathname);
|
|
146
|
-
if (result) {
|
|
147
|
-
match = route;
|
|
148
|
-
params = result.params;
|
|
149
|
-
break;
|
|
150
|
-
}
|
|
132
|
+
if (result) { match = route; params = result.params; break; }
|
|
151
133
|
}
|
|
152
134
|
|
|
153
135
|
if (match) {
|
|
154
136
|
try {
|
|
155
|
-
const [
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
if (!fs.existsSync(controllerPath)) throw new Error(`Controller ${controllerName} not found!`);
|
|
137
|
+
const [cName, mName] = match.action.split('@');
|
|
138
|
+
const cPath = path.join(root, 'app', 'controllers', cName + '.lmp');
|
|
139
|
+
if (!fs.existsSync(cPath)) throw new Error('Controller Not Found');
|
|
159
140
|
|
|
160
|
-
const module = await loadLumpiaModule(
|
|
161
|
-
const
|
|
162
|
-
const instance = new
|
|
141
|
+
const module = await loadLumpiaModule(cPath);
|
|
142
|
+
const Ctrl = module.default;
|
|
143
|
+
const instance = new Ctrl();
|
|
144
|
+
instance.env = env; instance.params = params; instance.config = config;
|
|
163
145
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
if (typeof instance[methodName] !== 'function') throw new Error(`Method ${methodName} missing`);
|
|
169
|
-
|
|
170
|
-
const args = Object.values(params);
|
|
171
|
-
const result = await instance[methodName](...args);
|
|
172
|
-
|
|
173
|
-
if (result.type === 'html') {
|
|
174
|
-
res.writeHead(200, {'Content-Type': 'text/html'});
|
|
175
|
-
res.end(result.content);
|
|
176
|
-
} else if (result.type === 'json') {
|
|
177
|
-
res.writeHead(200, {'Content-Type': 'application/json'});
|
|
146
|
+
const result = await instance[mName](...Object.values(params));
|
|
147
|
+
|
|
148
|
+
if (result && result.type) {
|
|
149
|
+
res.writeHead(200, {'Content-Type': result.type==='json'?'application/json':'text/html'});
|
|
178
150
|
res.end(result.content);
|
|
179
151
|
} else {
|
|
180
|
-
|
|
181
|
-
res.end(
|
|
152
|
+
// FIX: Jika result null/undefined (lupa return balek), kirim response kosong atau error
|
|
153
|
+
if(result === undefined) res.end('');
|
|
154
|
+
else res.end(String(result));
|
|
182
155
|
}
|
|
183
156
|
} catch (e) {
|
|
184
|
-
|
|
185
|
-
const errorMsg = env.APP_DEBUG === 'true' ? `<pre>${e.stack}</pre>` : `<h1>Server Error</h1>`;
|
|
186
|
-
res.writeHead(500, {'Content-Type': 'text/html'});
|
|
187
|
-
res.end(errorMsg);
|
|
157
|
+
res.writeHead(500); res.end(`<pre>${e.stack}</pre>`);
|
|
188
158
|
}
|
|
189
159
|
} else {
|
|
190
|
-
res.writeHead(404
|
|
191
|
-
res.end('<h1>404 Not Found</h1>');
|
|
160
|
+
res.writeHead(404); res.end('404 Not Found');
|
|
192
161
|
}
|
|
193
162
|
});
|
|
194
|
-
|
|
195
163
|
const port = 3000;
|
|
196
|
-
server.listen(port, () => {
|
|
197
|
-
|
|
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();
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
} catch (err) {
|
|
207
|
-
console.error('Fatal Error:', err);
|
|
208
|
-
}
|
|
164
|
+
server.listen(port, () => console.log(`🚀 Server running at http://localhost:${port}`));
|
|
165
|
+
} catch (e) { console.error(e); }
|
|
209
166
|
}
|