lumpiajs 1.0.6 → 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 -159
- package/bin/lumpia.js +8 -7
- package/index.js +4 -1
- package/lib/commands/build.js +246 -0
- package/lib/commands/create.js +151 -97
- package/lib/commands/serve.js +154 -27
- package/lib/core/Config.js +20 -0
- package/lib/core/DB.js +139 -0
- package/lib/core/Env.js +23 -0
- package/lib/core/Router.js +10 -3
- package/lib/core/View.js +21 -8
- package/package.json +4 -2
- package/templates/.env.example +6 -0
- package/templates/gitignore.txt +3 -0
package/lib/commands/create.js
CHANGED
|
@@ -1,149 +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
|
-
|
|
18
|
+
// "this.tampil" juga bisa ditulis "this->tampil"
|
|
19
|
+
// Transpiler LumpiaJS sing ngatur, Bos!
|
|
20
|
+
return this->tampil('home', {
|
|
19
21
|
message: 'Welcome to LumpiaJS MVC!',
|
|
20
|
-
author: 'Pakdhe Koding'
|
|
22
|
+
author: 'Pakdhe Koding',
|
|
23
|
+
env: this.env.APP_ENV
|
|
21
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
|
+
|
|
24
48
|
profile() {
|
|
25
|
-
return this
|
|
49
|
+
return this->tampil('profile', { name: 'Loyal User' });
|
|
26
50
|
}
|
|
27
51
|
}
|
|
28
52
|
`;
|
|
29
53
|
|
|
30
|
-
const modelTemplate = `// Example dummy data
|
|
54
|
+
const modelTemplate = `// Example dummy data (Static)
|
|
31
55
|
const productData = [
|
|
32
56
|
{ id: 1, name: 'Lumpia Basah', price: 5000 },
|
|
33
|
-
{ id: 2, name: 'Lumpia Goreng', price: 6000 }
|
|
34
|
-
{ id: 3, name: 'Tahu Gimbal', price: 12000 }
|
|
57
|
+
{ id: 2, name: 'Lumpia Goreng', price: 6000 }
|
|
35
58
|
];
|
|
36
|
-
|
|
37
59
|
export default productData;
|
|
38
60
|
`;
|
|
39
61
|
|
|
40
62
|
const productControllerTemplate = `import { Controller, Model } from 'lumpiajs';
|
|
41
|
-
import ProductData from '../../app/models/Product.
|
|
63
|
+
import ProductData from '../../app/models/Product.lmp';
|
|
42
64
|
|
|
43
65
|
export default class ProductController extends Controller {
|
|
44
66
|
index() {
|
|
45
|
-
//
|
|
46
|
-
const result = Model
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return this
|
|
51
|
-
status: 'success',
|
|
52
|
-
data: result
|
|
53
|
-
});
|
|
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 });
|
|
54
73
|
}
|
|
55
74
|
}
|
|
56
75
|
`;
|
|
57
76
|
|
|
58
|
-
const
|
|
59
|
-
<klambi>
|
|
60
|
-
h1 { color: #d35400; text-align: center; }
|
|
61
|
-
.box { border: 1px solid #ddd; padding: 20px; text-align: center; margin-top: 20px; }
|
|
62
|
-
</klambi>
|
|
63
|
-
|
|
77
|
+
const viewProfileTemplate = `<lump>
|
|
64
78
|
<kulit>
|
|
65
|
-
<
|
|
66
|
-
|
|
67
|
-
<p>
|
|
68
|
-
<
|
|
69
|
-
<br><br>
|
|
70
|
-
<a href="/profile">Go to Profile</a>
|
|
79
|
+
<div class="container mt-5">
|
|
80
|
+
<h1>User Profile</h1>
|
|
81
|
+
<p>Hello, <strong>{{ name }}</strong>!</p>
|
|
82
|
+
<a href="/" class="btn btn-secondary">Back Home</a>
|
|
71
83
|
</div>
|
|
72
84
|
</kulit>
|
|
73
|
-
|
|
74
|
-
<isi>
|
|
75
|
-
gawe checkPrice() {
|
|
76
|
-
fetch('/api/products')
|
|
77
|
-
.then(res => res.json())
|
|
78
|
-
.then(data => {
|
|
79
|
-
alert('Data from API: ' + JSON.stringify(data));
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
</isi>
|
|
83
85
|
</lump>`;
|
|
84
86
|
|
|
85
|
-
const
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
<a href="/">Back Home</a>
|
|
90
|
-
</kulit>
|
|
91
|
-
</lump>`;
|
|
87
|
+
const envTemplate = `
|
|
88
|
+
BASE_URL="http://localhost:3000"
|
|
89
|
+
APP_ENV="local"
|
|
90
|
+
APP_DEBUG="true"
|
|
92
91
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
"scripts": {
|
|
99
|
-
"start": "lumpia dodolan",
|
|
100
|
-
"serve": "lumpia dodolan"
|
|
101
|
-
},
|
|
102
|
-
"dependencies": {
|
|
103
|
-
"lumpiajs": "latest"
|
|
104
|
-
}
|
|
105
|
-
}
|
|
92
|
+
# Database Config
|
|
93
|
+
DB_HOST="localhost"
|
|
94
|
+
DB_USER="root"
|
|
95
|
+
DB_PASSWORD=""
|
|
96
|
+
DB_NAME="lumpia_db"
|
|
106
97
|
`;
|
|
107
98
|
|
|
108
|
-
|
|
109
|
-
|
|
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;
|
|
110
154
|
if (!projectName) {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return;
|
|
155
|
+
const res = await prompts({ type: 'text', name: 'val', message: 'Jeneng project?', initial: 'my-app' });
|
|
156
|
+
projectName = res.val;
|
|
114
157
|
}
|
|
115
|
-
|
|
158
|
+
if (!projectName) return;
|
|
159
|
+
|
|
116
160
|
const root = path.join(process.cwd(), projectName);
|
|
117
|
-
if (fs.existsSync(root))
|
|
161
|
+
if (fs.existsSync(root)) { console.log('❌ Folder Exists'); return; }
|
|
118
162
|
|
|
119
|
-
|
|
120
|
-
|
|
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
|
|
121
172
|
fs.mkdirSync(root);
|
|
122
|
-
|
|
123
|
-
// STANDARD MVC STRUCTURE:
|
|
124
|
-
// app/controllers
|
|
125
|
-
// app/models
|
|
126
|
-
// views
|
|
127
|
-
// routes
|
|
128
|
-
|
|
129
173
|
fs.mkdirSync(path.join(root, 'app', 'controllers'), { recursive: true });
|
|
130
174
|
fs.mkdirSync(path.join(root, 'app', 'models'), { recursive: true });
|
|
131
175
|
fs.mkdirSync(path.join(root, 'routes'));
|
|
132
|
-
fs.mkdirSync(path.join(root, 'views'));
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
fs.writeFileSync(path.join(root, '
|
|
138
|
-
fs.writeFileSync(path.join(root, '
|
|
139
|
-
fs.writeFileSync(path.join(root, '
|
|
140
|
-
fs.writeFileSync(path.join(root, '
|
|
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));
|
|
141
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
|
+
}
|
|
142
200
|
|
|
143
|
-
console.log(
|
|
144
|
-
console.log(
|
|
145
|
-
console.log(`cd ${projectName}`);
|
|
146
|
-
console.log('npm install');
|
|
147
|
-
console.log('npm start');
|
|
148
|
-
console.log('----------------------------------------------------');
|
|
201
|
+
console.log(`✅ Project "${projectName}" Ready!`);
|
|
202
|
+
console.log(`cd ${projectName} && npm install && lumpia kukus`);
|
|
149
203
|
}
|
package/lib/commands/serve.js
CHANGED
|
@@ -1,53 +1,174 @@
|
|
|
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';
|
|
7
|
+
import { loadEnv } from '../core/Env.js';
|
|
8
|
+
import { loadConfig } from '../core/Config.js';
|
|
9
|
+
|
|
10
|
+
const cliPackageJson = JSON.parse(fs.readFileSync(new URL('../../package.json', import.meta.url)));
|
|
11
|
+
|
|
12
|
+
// Helper to Match Routes
|
|
13
|
+
function matchRoute(definedRoute, method, pathname) {
|
|
14
|
+
if (definedRoute.method !== method) return null;
|
|
15
|
+
if (definedRoute.path === pathname) return { params: {} };
|
|
16
|
+
|
|
17
|
+
const paramNames = [];
|
|
18
|
+
const regexPath = definedRoute.path.replace(/\{([a-zA-Z0-9_]+)\}/g, (match, name) => {
|
|
19
|
+
paramNames.push(name);
|
|
20
|
+
return '([^/]+)';
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
if (regexPath === definedRoute.path) return null;
|
|
24
|
+
const regex = new RegExp(`^${regexPath}$`);
|
|
25
|
+
const match = pathname.match(regex);
|
|
26
|
+
if (match) {
|
|
27
|
+
const params = {};
|
|
28
|
+
paramNames.forEach((name, index) => {
|
|
29
|
+
params[name] = match[index + 1];
|
|
30
|
+
});
|
|
31
|
+
return { params };
|
|
32
|
+
}
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
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 });
|
|
61
|
+
|
|
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;
|
|
83
|
+
|
|
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
|
+
|
|
6
97
|
|
|
7
98
|
export async function serveProject() {
|
|
8
99
|
const root = process.cwd();
|
|
9
|
-
|
|
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)");
|
|
10
103
|
|
|
11
|
-
|
|
104
|
+
const env = loadEnv(root);
|
|
105
|
+
const config = loadConfig(root);
|
|
12
106
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
try {
|
|
16
|
-
// Load User Routes
|
|
17
|
-
const userRouteUrl = path.join(root, 'routes', 'web.js');
|
|
18
|
-
await import('file://' + userRouteUrl);
|
|
107
|
+
if (config.klambi === 'tailwindcss') startTailwindWatcher(root);
|
|
108
|
+
else if (config.klambi === 'bootstrap') handleBootstrap(root);
|
|
19
109
|
|
|
20
|
-
|
|
110
|
+
try {
|
|
111
|
+
await loadLumpiaModule(routesFile);
|
|
112
|
+
console.log(`🛣️ Routes registered: ${routes.length}`);
|
|
113
|
+
|
|
114
|
+
// Info Syntax
|
|
115
|
+
console.log(`✨ Syntax Mode: PHP/Laravel Style (->) is enabled in .lmp files!`);
|
|
21
116
|
|
|
22
|
-
// Start Server
|
|
23
117
|
const server = http.createServer(async (req, res) => {
|
|
24
118
|
const method = req.method;
|
|
25
119
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
|
26
120
|
const pathname = url.pathname;
|
|
27
121
|
|
|
28
|
-
console.log(`📥 ${method} ${pathname}`);
|
|
122
|
+
if (env.APP_DEBUG === 'true') console.log(`📥 ${method} ${pathname}`);
|
|
29
123
|
|
|
30
|
-
const
|
|
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
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
let match = null;
|
|
143
|
+
let params = {};
|
|
144
|
+
for (const route of routes) {
|
|
145
|
+
const result = matchRoute(route, method, pathname);
|
|
146
|
+
if (result) {
|
|
147
|
+
match = route;
|
|
148
|
+
params = result.params;
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
31
152
|
|
|
32
153
|
if (match) {
|
|
33
154
|
try {
|
|
34
|
-
// Action format: 'ControllerName@method'
|
|
35
155
|
const [controllerName, methodName] = match.action.split('@');
|
|
156
|
+
const controllerPath = path.join(root, 'app', 'controllers', controllerName + '.lmp');
|
|
36
157
|
|
|
37
|
-
|
|
38
|
-
const controllerPath = path.join(root, 'app', 'controllers', controllerName + '.js');
|
|
39
|
-
|
|
40
|
-
if (!fs.existsSync(controllerPath)) throw new Error(`Controller ${controllerName} not found in app/controllers!`);
|
|
158
|
+
if (!fs.existsSync(controllerPath)) throw new Error(`Controller ${controllerName} not found!`);
|
|
41
159
|
|
|
42
|
-
const module = await
|
|
160
|
+
const module = await loadLumpiaModule(controllerPath);
|
|
43
161
|
const ControllerClass = module.default;
|
|
44
162
|
const instance = new ControllerClass();
|
|
45
163
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
164
|
+
instance.env = env;
|
|
165
|
+
instance.params = params;
|
|
166
|
+
instance.config = config;
|
|
167
|
+
|
|
168
|
+
if (typeof instance[methodName] !== 'function') throw new Error(`Method ${methodName} missing`);
|
|
49
169
|
|
|
50
|
-
const
|
|
170
|
+
const args = Object.values(params);
|
|
171
|
+
const result = await instance[methodName](...args);
|
|
51
172
|
|
|
52
173
|
if (result.type === 'html') {
|
|
53
174
|
res.writeHead(200, {'Content-Type': 'text/html'});
|
|
@@ -61,22 +182,28 @@ export async function serveProject() {
|
|
|
61
182
|
}
|
|
62
183
|
} catch (e) {
|
|
63
184
|
console.error(e);
|
|
185
|
+
const errorMsg = env.APP_DEBUG === 'true' ? `<pre>${e.stack}</pre>` : `<h1>Server Error</h1>`;
|
|
64
186
|
res.writeHead(500, {'Content-Type': 'text/html'});
|
|
65
|
-
res.end(
|
|
187
|
+
res.end(errorMsg);
|
|
66
188
|
}
|
|
67
189
|
} else {
|
|
68
190
|
res.writeHead(404, {'Content-Type': 'text/html'});
|
|
69
|
-
res.end('<h1>404
|
|
191
|
+
res.end('<h1>404 Not Found</h1>');
|
|
70
192
|
}
|
|
71
193
|
});
|
|
72
194
|
|
|
73
195
|
const port = 3000;
|
|
74
196
|
server.listen(port, () => {
|
|
75
|
-
console.log(`🚀 Server running at http://localhost
|
|
76
|
-
|
|
197
|
+
console.log(`🚀 Server running at ${env.BASE_URL || 'http://localhost:3000'}`);
|
|
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();
|
|
77
204
|
});
|
|
78
205
|
|
|
79
206
|
} catch (err) {
|
|
80
|
-
console.error('Fatal Error
|
|
207
|
+
console.error('Fatal Error:', err);
|
|
81
208
|
}
|
|
82
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
|
+
}
|