@untrustnova/nova-cli 1.0.0
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 +50 -0
- package/bin/nova.js +4 -0
- package/package.json +23 -0
- package/src/cli.js +348 -0
- package/templates/base/.env.example +30 -0
- package/templates/base/.env.local +19 -0
- package/templates/base/.eslintrc.cjs +21 -0
- package/templates/base/.prettierrc +6 -0
- package/templates/base/app/controllers/home.controller.js +7 -0
- package/templates/base/app/routes/pages/home.index.js +3 -0
- package/templates/base/app/routes/web.js +12 -0
- package/templates/base/jsconfig.json +17 -0
- package/templates/base/nova.config.js +88 -0
- package/templates/base/package.json +26 -0
- package/templates/base/public/fonts/.gitkeep +0 -0
- package/templates/base/public/images/.gitkeep +0 -0
- package/templates/base/server.js +12 -0
- package/templates/base/storage/app/.gitkeep +0 -0
- package/templates/base/storage/cache/.gitkeep +0 -0
- package/templates/base/storage/logs/.gitkeep +0 -0
- package/templates/base/web/App.jsx +9 -0
- package/templates/base/web/components/Hero.jsx +18 -0
- package/templates/base/web/index.html +12 -0
- package/templates/base/web/lib/api.js +8 -0
- package/templates/base/web/main.jsx +6 -0
- package/templates/base/web/styles/globals.css +77 -0
package/README.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Nova CLI
|
|
2
|
+
|
|
3
|
+
CLI resmi untuk membuat dan menjalankan proyek Nova.js.
|
|
4
|
+
|
|
5
|
+
## Instalasi
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @untrustnova/nova-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Perintah Utama
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
nova new <name> # Scaffold project baru (fetch template)
|
|
15
|
+
nova dev # Jalankan backend + Vite dev server
|
|
16
|
+
nova build # Build produksi (Vite)
|
|
17
|
+
nova db:init # Drizzle generate
|
|
18
|
+
nova db:push # Drizzle push
|
|
19
|
+
nova create:controller <name> # Controller baru
|
|
20
|
+
nova create:middleware <name> # Middleware baru
|
|
21
|
+
nova create:migration <name> # Migration baru
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Alur Cepat
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install -g @untrustnova/nova-cli
|
|
28
|
+
nova new my-app
|
|
29
|
+
cd my-app
|
|
30
|
+
npm run dev
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Struktur Template
|
|
34
|
+
|
|
35
|
+
Template menggunakan:
|
|
36
|
+
- `nova.config.js` sebagai konfigurasi utama (tanpa `vite.config.js`)
|
|
37
|
+
- Routing hybrid (object + file-based)
|
|
38
|
+
- Folder `app/` untuk server-side
|
|
39
|
+
- Folder `web/` untuk frontend React
|
|
40
|
+
|
|
41
|
+
## Catatan
|
|
42
|
+
|
|
43
|
+
`nova new` akan mencoba fetch template dari repo `nova` dan meng-install dependency
|
|
44
|
+
(`@untrustnova/nova-framework` termasuk). Gunakan `--no-install` jika ingin skip instalasi,
|
|
45
|
+
atau set `NOVA_TEMPLATE_REPO` untuk mengganti repo template.
|
|
46
|
+
|
|
47
|
+
`nova dev` akan menjalankan `server.js` dan Vite dev server secara bersamaan.
|
|
48
|
+
|
|
49
|
+
Perintah `db:*` adalah pembungkus untuk `drizzle-kit`.
|
|
50
|
+
Jika belum terpasang, install: `npm install -D drizzle-kit`.
|
package/bin/nova.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@untrustnova/nova-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Nova.js CLI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"nova": "./bin/nova.js"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin",
|
|
14
|
+
"src",
|
|
15
|
+
"templates"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"nova",
|
|
19
|
+
"nova-js",
|
|
20
|
+
"cli"
|
|
21
|
+
],
|
|
22
|
+
"license": "MIT"
|
|
23
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { mkdir, mkdtemp, readdir, readFile, rm, stat, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { createRequire } from 'node:module';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
|
|
8
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const templateRoot = resolve(__dirname, '../templates/base');
|
|
10
|
+
|
|
11
|
+
export async function run(args) {
|
|
12
|
+
const [command, ...rest] = args;
|
|
13
|
+
|
|
14
|
+
if (!command || command === 'help' || command === '--help' || command === '-h') {
|
|
15
|
+
printHelp();
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (command === '--version' || command === '-v') {
|
|
20
|
+
console.log('nova-cli 0.1.0');
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
switch (command) {
|
|
26
|
+
case 'new':
|
|
27
|
+
await scaffoldProject(rest);
|
|
28
|
+
break;
|
|
29
|
+
case 'dev':
|
|
30
|
+
await runDev();
|
|
31
|
+
break;
|
|
32
|
+
case 'build':
|
|
33
|
+
await runBuild();
|
|
34
|
+
break;
|
|
35
|
+
case 'db:init':
|
|
36
|
+
await runDrizzle('generate');
|
|
37
|
+
break;
|
|
38
|
+
case 'db:push':
|
|
39
|
+
await runDrizzle('push');
|
|
40
|
+
break;
|
|
41
|
+
case 'create:controller':
|
|
42
|
+
await createController(rest[0]);
|
|
43
|
+
break;
|
|
44
|
+
case 'create:middleware':
|
|
45
|
+
await createMiddleware(rest[0]);
|
|
46
|
+
break;
|
|
47
|
+
case 'create:migration':
|
|
48
|
+
await createMigration(rest[0]);
|
|
49
|
+
break;
|
|
50
|
+
default:
|
|
51
|
+
console.error(`[nova] unknown command "${command}"`);
|
|
52
|
+
printHelp();
|
|
53
|
+
}
|
|
54
|
+
} catch (error) {
|
|
55
|
+
console.error(`[nova] ${error.message}`);
|
|
56
|
+
process.exitCode = 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function printHelp() {
|
|
61
|
+
console.log(`Nova CLI
|
|
62
|
+
|
|
63
|
+
Usage:
|
|
64
|
+
nova new <name> [--no-install]
|
|
65
|
+
nova dev
|
|
66
|
+
nova build
|
|
67
|
+
nova db:init
|
|
68
|
+
nova db:push
|
|
69
|
+
nova create:controller <name>
|
|
70
|
+
nova create:middleware <name>
|
|
71
|
+
nova create:migration <name>
|
|
72
|
+
`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async function scaffoldProject(args) {
|
|
76
|
+
const [name, ...flags] = args;
|
|
77
|
+
const shouldInstall = !flags.includes('--no-install') && !flags.includes('--skip-install');
|
|
78
|
+
|
|
79
|
+
if (!name) {
|
|
80
|
+
throw new Error('project name is required');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const target = resolve(process.cwd(), name);
|
|
84
|
+
if (await pathExists(target)) {
|
|
85
|
+
const entries = await readdir(target);
|
|
86
|
+
if (entries.length > 0) {
|
|
87
|
+
throw new Error(`directory "${name}" is not empty`);
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
await mkdir(target, { recursive: true });
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const remoteTemplate = await tryFetchTemplate(
|
|
94
|
+
process.env.NOVA_TEMPLATE_REPO || 'https://github.com/nova-js/nova',
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const source = remoteTemplate?.path || templateRoot;
|
|
98
|
+
await copyTemplate(source, target, {
|
|
99
|
+
'__APP_NAME__': name,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
console.log(`[nova] project created at ${target}`);
|
|
103
|
+
|
|
104
|
+
if (remoteTemplate?.cleanup) {
|
|
105
|
+
await remoteTemplate.cleanup();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (shouldInstall) {
|
|
109
|
+
await runCommand('npm', ['install'], { cwd: target });
|
|
110
|
+
console.log('[nova] dependencies installed');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function createController(name) {
|
|
115
|
+
if (!name) {
|
|
116
|
+
throw new Error('controller name is required');
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const fileName = normalizeSuffix(name, '.controller.js');
|
|
120
|
+
const className = toClassName(fileName.replace('.controller.js', ''));
|
|
121
|
+
const target = resolve(process.cwd(), 'app', 'controllers', fileName);
|
|
122
|
+
|
|
123
|
+
await ensureDir(dirname(target));
|
|
124
|
+
await ensureNotExists(target);
|
|
125
|
+
|
|
126
|
+
const body = `import { Controller } from '@untrustnova/nova-framework/controller';
|
|
127
|
+
|
|
128
|
+
export default class ${className}Controller extends Controller {
|
|
129
|
+
async index({ response }) {
|
|
130
|
+
response.json({ message: '${className} ready' });
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
`;
|
|
134
|
+
|
|
135
|
+
await writeFile(target, body, 'utf8');
|
|
136
|
+
console.log(`[nova] controller created: ${relativePath(target)}`);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async function createMiddleware(name) {
|
|
140
|
+
if (!name) {
|
|
141
|
+
throw new Error('middleware name is required');
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const fileName = normalizeSuffix(name, '.middleware.js');
|
|
145
|
+
const className = toClassName(fileName.replace('.middleware.js', ''));
|
|
146
|
+
const target = resolve(process.cwd(), 'app', 'middleware', fileName);
|
|
147
|
+
|
|
148
|
+
await ensureDir(dirname(target));
|
|
149
|
+
await ensureNotExists(target);
|
|
150
|
+
|
|
151
|
+
const body = `export class ${className}Middleware {
|
|
152
|
+
async handle(req, res, next) {
|
|
153
|
+
return next();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
`;
|
|
157
|
+
|
|
158
|
+
await writeFile(target, body, 'utf8');
|
|
159
|
+
console.log(`[nova] middleware created: ${relativePath(target)}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function createMigration(name) {
|
|
163
|
+
if (!name) {
|
|
164
|
+
throw new Error('migration name is required');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const stamp = timestamp();
|
|
168
|
+
const fileName = `${stamp}_${sanitizeFileName(name)}.js`;
|
|
169
|
+
const target = resolve(process.cwd(), 'app', 'migrations', fileName);
|
|
170
|
+
|
|
171
|
+
await ensureDir(dirname(target));
|
|
172
|
+
await ensureNotExists(target);
|
|
173
|
+
|
|
174
|
+
const body = `export async function up(db) {
|
|
175
|
+
// TODO: add migration
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export async function down(db) {
|
|
179
|
+
// TODO: rollback migration
|
|
180
|
+
}
|
|
181
|
+
`;
|
|
182
|
+
|
|
183
|
+
await writeFile(target, body, 'utf8');
|
|
184
|
+
console.log(`[nova] migration created: ${relativePath(target)}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function runDrizzle(command) {
|
|
188
|
+
const args = ['drizzle-kit', command];
|
|
189
|
+
await runCommand('npx', args);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
async function runDev() {
|
|
193
|
+
const config = await loadNovaConfig();
|
|
194
|
+
const { runDev: runViteDev } = await loadFrameworkModule('dev');
|
|
195
|
+
|
|
196
|
+
const server = spawn('node', ['server.js'], {
|
|
197
|
+
stdio: 'inherit',
|
|
198
|
+
shell: process.platform === 'win32',
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
process.on('SIGINT', () => server.kill('SIGINT'));
|
|
202
|
+
process.on('SIGTERM', () => server.kill('SIGTERM'));
|
|
203
|
+
|
|
204
|
+
await runViteDev(config);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function runBuild() {
|
|
208
|
+
const config = await loadNovaConfig();
|
|
209
|
+
const { runBuild: runViteBuild } = await loadFrameworkModule('dev');
|
|
210
|
+
await runViteBuild(config);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function runCommand(command, args, options = {}) {
|
|
214
|
+
await new Promise((resolvePromise, reject) => {
|
|
215
|
+
const child = spawn(command, args, {
|
|
216
|
+
stdio: 'inherit',
|
|
217
|
+
shell: process.platform === 'win32',
|
|
218
|
+
cwd: options.cwd || process.cwd(),
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
child.on('exit', (code) => {
|
|
222
|
+
if (code === 0) resolvePromise();
|
|
223
|
+
else reject(new Error(`command failed: ${command} ${args.join(' ')}`));
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async function tryFetchTemplate(repo) {
|
|
229
|
+
const tempRoot = await mkdtemp(join(tmpdir(), 'nova-template-'));
|
|
230
|
+
const cleanup = async () => {
|
|
231
|
+
await rm(tempRoot, { recursive: true, force: true });
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
await runCommand('git', ['clone', '--depth', '1', repo, tempRoot]);
|
|
236
|
+
const basePath = join(tempRoot, 'templates', 'base');
|
|
237
|
+
if (await pathExists(basePath)) {
|
|
238
|
+
return { path: basePath, cleanup };
|
|
239
|
+
}
|
|
240
|
+
} catch (error) {
|
|
241
|
+
await cleanup();
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
await cleanup();
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function copyTemplate(src, dest, replacements) {
|
|
250
|
+
const entries = await readdir(src, { withFileTypes: true });
|
|
251
|
+
await ensureDir(dest);
|
|
252
|
+
|
|
253
|
+
for (const entry of entries) {
|
|
254
|
+
const srcPath = join(src, entry.name);
|
|
255
|
+
const destPath = join(dest, entry.name);
|
|
256
|
+
|
|
257
|
+
if (entry.isDirectory()) {
|
|
258
|
+
await copyTemplate(srcPath, destPath, replacements);
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const contents = await readFile(srcPath, 'utf8');
|
|
263
|
+
const replaced = applyReplacements(contents, replacements);
|
|
264
|
+
await writeFile(destPath, replaced, 'utf8');
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function applyReplacements(contents, replacements) {
|
|
269
|
+
let result = contents;
|
|
270
|
+
for (const [key, value] of Object.entries(replacements)) {
|
|
271
|
+
result = result.split(key).join(value);
|
|
272
|
+
}
|
|
273
|
+
return result;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
async function ensureDir(path) {
|
|
277
|
+
await mkdir(path, { recursive: true });
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function ensureNotExists(path) {
|
|
281
|
+
if (await pathExists(path)) {
|
|
282
|
+
throw new Error(`${relativePath(path)} already exists`);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
async function pathExists(path) {
|
|
287
|
+
try {
|
|
288
|
+
await stat(path);
|
|
289
|
+
return true;
|
|
290
|
+
} catch (error) {
|
|
291
|
+
if (error.code === 'ENOENT') return false;
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function normalizeSuffix(name, suffix) {
|
|
297
|
+
if (name.endsWith(suffix)) return name;
|
|
298
|
+
const base = name.replace(/\.[^.]+$/, '');
|
|
299
|
+
if (base.endsWith(suffix.replace('.js', ''))) return `${base}.js`;
|
|
300
|
+
return base + suffix;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function toClassName(input) {
|
|
304
|
+
return input
|
|
305
|
+
.split(/[^a-zA-Z0-9]/)
|
|
306
|
+
.filter(Boolean)
|
|
307
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
308
|
+
.join('');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function sanitizeFileName(input) {
|
|
312
|
+
return input
|
|
313
|
+
.trim()
|
|
314
|
+
.toLowerCase()
|
|
315
|
+
.replace(/\s+/g, '_')
|
|
316
|
+
.replace(/[^a-z0-9_]+/g, '');
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function timestamp() {
|
|
320
|
+
const now = new Date();
|
|
321
|
+
const pad = (value) => String(value).padStart(2, '0');
|
|
322
|
+
return [
|
|
323
|
+
now.getFullYear(),
|
|
324
|
+
pad(now.getMonth() + 1),
|
|
325
|
+
pad(now.getDate()),
|
|
326
|
+
pad(now.getHours()),
|
|
327
|
+
pad(now.getMinutes()),
|
|
328
|
+
pad(now.getSeconds()),
|
|
329
|
+
].join('_');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function relativePath(path) {
|
|
333
|
+
return path.replace(process.cwd() + '/', '');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
async function loadNovaConfig() {
|
|
337
|
+
const configPath = resolve(process.cwd(), 'nova.config.js');
|
|
338
|
+
const module = await import(pathToFileURL(configPath));
|
|
339
|
+
return module.default || module;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async function loadFrameworkModule(entry) {
|
|
343
|
+
const require = createRequire(import.meta.url);
|
|
344
|
+
const resolved = require.resolve(`@untrustnova/nova-framework/${entry}`, {
|
|
345
|
+
paths: [process.cwd()],
|
|
346
|
+
});
|
|
347
|
+
return import(pathToFileURL(resolved));
|
|
348
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
NOVA_APP_NAME=Nova App
|
|
2
|
+
NOVA_APP_URL=http://localhost:3000
|
|
3
|
+
NOVA_ENV=development
|
|
4
|
+
NOVA_DEBUG=true
|
|
5
|
+
|
|
6
|
+
NOVA_HOST=0.0.0.0
|
|
7
|
+
NOVA_PORT=3000
|
|
8
|
+
NOVA_KERNEL_ADAPTER=node
|
|
9
|
+
|
|
10
|
+
NOVA_DB_CONNECTION=sqlite
|
|
11
|
+
NOVA_DATABASE_URL=file:./storage/db.sqlite
|
|
12
|
+
# postgresql://user:password@localhost:5432/nova_db
|
|
13
|
+
# mysql://user:password@localhost:3306/nova_db
|
|
14
|
+
# mongodb://user:password@localhost:27017/nova_db
|
|
15
|
+
# supabase://project-ref.supabase.co?key=service-role-key
|
|
16
|
+
|
|
17
|
+
NOVA_STORAGE_DRIVER=local
|
|
18
|
+
# NOVA_AWS_BUCKET=my-bucket
|
|
19
|
+
# NOVA_AWS_REGION=us-east-1
|
|
20
|
+
# NOVA_MINIO_ENDPOINT=minio.example.com
|
|
21
|
+
# NOVA_MINIO_ACCESS_KEY=minioadmin
|
|
22
|
+
# NOVA_MINIO_SECRET_KEY=minioadmin
|
|
23
|
+
|
|
24
|
+
NOVA_CACHE_DRIVER=memory
|
|
25
|
+
# NOVA_REDIS_URL=redis://localhost:6379
|
|
26
|
+
|
|
27
|
+
NOVA_LOG_LEVEL=info
|
|
28
|
+
|
|
29
|
+
VITE_API_URL=http://localhost:3000/api
|
|
30
|
+
VITE_APP_TITLE=Nova.js App
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
NOVA_APP_NAME=Nova
|
|
2
|
+
NOVA_APP_URL=http://localhost:3000
|
|
3
|
+
NOVA_ENV=development
|
|
4
|
+
NOVA_DEBUG=true
|
|
5
|
+
|
|
6
|
+
NOVA_HOST=0.0.0.0
|
|
7
|
+
NOVA_PORT=3000
|
|
8
|
+
NOVA_KERNEL_ADAPTER=node
|
|
9
|
+
|
|
10
|
+
NOVA_DB_CONNECTION=sqlite
|
|
11
|
+
NOVA_DATABASE_URL=file:./storage/db.sqlite
|
|
12
|
+
# postgresql://user:password@localhost:5432/nova_db
|
|
13
|
+
# mysql://user:password@localhost:3306/nova_db
|
|
14
|
+
# mongodb://user:password@localhost:27017/nova_db
|
|
15
|
+
# supabase://project-ref.supabase.co?key=service-role-key
|
|
16
|
+
|
|
17
|
+
NOVA_CACHE_DRIVER=memory
|
|
18
|
+
NOVA_STORAGE_DRIVER=local
|
|
19
|
+
NOVA_LOG_LEVEL=debug
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
env: {
|
|
3
|
+
browser: true,
|
|
4
|
+
es2023: true,
|
|
5
|
+
node: true,
|
|
6
|
+
},
|
|
7
|
+
parserOptions: {
|
|
8
|
+
ecmaVersion: 'latest',
|
|
9
|
+
sourceType: 'module',
|
|
10
|
+
},
|
|
11
|
+
plugins: ['react'],
|
|
12
|
+
extends: ['eslint:recommended', 'plugin:react/recommended', 'prettier'],
|
|
13
|
+
settings: {
|
|
14
|
+
react: {
|
|
15
|
+
version: 'detect',
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
rules: {
|
|
19
|
+
'react/react-in-jsx-scope': 'off',
|
|
20
|
+
},
|
|
21
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { route } from '@untrustnova/nova-framework/routing';
|
|
2
|
+
|
|
3
|
+
export default () => {
|
|
4
|
+
const routes = route();
|
|
5
|
+
|
|
6
|
+
routes.get('/', 'HomeController@index');
|
|
7
|
+
routes.get('/status', async ({ response }) => {
|
|
8
|
+
response.json({ status: 'ready' });
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
return routes.toArray();
|
|
12
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"baseUrl": ".",
|
|
4
|
+
"paths": {
|
|
5
|
+
"@app/*": ["app/*"],
|
|
6
|
+
"@web/*": ["web/*"],
|
|
7
|
+
"@components/*": ["web/components/*"],
|
|
8
|
+
"@lib/*": ["web/lib/*"]
|
|
9
|
+
}
|
|
10
|
+
},
|
|
11
|
+
"include": [
|
|
12
|
+
"app",
|
|
13
|
+
"web",
|
|
14
|
+
"server.js",
|
|
15
|
+
"nova.config.js"
|
|
16
|
+
]
|
|
17
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { defineConfig, react, tailwindcss } from '@untrustnova/nova-framework/config';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
app: {
|
|
5
|
+
name: process.env.NOVA_APP_NAME || '__APP_NAME__',
|
|
6
|
+
url: process.env.NOVA_APP_URL || 'http://localhost:3000',
|
|
7
|
+
env: process.env.NOVA_ENV || 'development',
|
|
8
|
+
debug: process.env.NOVA_DEBUG === 'true',
|
|
9
|
+
},
|
|
10
|
+
server: {
|
|
11
|
+
host: process.env.NOVA_HOST || '0.0.0.0',
|
|
12
|
+
port: Number(process.env.NOVA_PORT || 3000),
|
|
13
|
+
},
|
|
14
|
+
security: {
|
|
15
|
+
bodyLimit: 1024 * 1024,
|
|
16
|
+
},
|
|
17
|
+
kernel: {
|
|
18
|
+
adapter: process.env.NOVA_KERNEL_ADAPTER || 'node',
|
|
19
|
+
},
|
|
20
|
+
database: {
|
|
21
|
+
default: process.env.NOVA_DB_CONNECTION || 'sqlite',
|
|
22
|
+
connections: {
|
|
23
|
+
postgres: {
|
|
24
|
+
driver: 'pg',
|
|
25
|
+
url: process.env.NOVA_DATABASE_URL,
|
|
26
|
+
},
|
|
27
|
+
mysql: {
|
|
28
|
+
driver: 'mysql2',
|
|
29
|
+
url: process.env.NOVA_DATABASE_URL,
|
|
30
|
+
},
|
|
31
|
+
sqlite: {
|
|
32
|
+
driver: 'better-sqlite3',
|
|
33
|
+
url: process.env.NOVA_DATABASE_URL || 'file:./storage/db.sqlite',
|
|
34
|
+
},
|
|
35
|
+
mongodb: {
|
|
36
|
+
driver: 'mongodb',
|
|
37
|
+
url: process.env.NOVA_DATABASE_URL,
|
|
38
|
+
},
|
|
39
|
+
supabase: {
|
|
40
|
+
driver: 'supabase',
|
|
41
|
+
url: process.env.NOVA_DATABASE_URL,
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
},
|
|
45
|
+
modules: {
|
|
46
|
+
storage: {
|
|
47
|
+
driver: process.env.NOVA_STORAGE_DRIVER || 'local',
|
|
48
|
+
disks: {
|
|
49
|
+
local: { root: './storage/app' },
|
|
50
|
+
s3: {
|
|
51
|
+
bucket: process.env.NOVA_AWS_BUCKET,
|
|
52
|
+
region: process.env.NOVA_AWS_REGION,
|
|
53
|
+
},
|
|
54
|
+
minio: {
|
|
55
|
+
endPoint: process.env.NOVA_MINIO_ENDPOINT,
|
|
56
|
+
accessKey: process.env.NOVA_MINIO_ACCESS_KEY,
|
|
57
|
+
secretKey: process.env.NOVA_MINIO_SECRET_KEY,
|
|
58
|
+
},
|
|
59
|
+
},
|
|
60
|
+
},
|
|
61
|
+
cache: {
|
|
62
|
+
driver: process.env.NOVA_CACHE_DRIVER || 'memory',
|
|
63
|
+
stores: {
|
|
64
|
+
memory: { max: 500 },
|
|
65
|
+
redis: { url: process.env.NOVA_REDIS_URL },
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
logs: {
|
|
69
|
+
driver: 'paperlog',
|
|
70
|
+
level: process.env.NOVA_LOG_LEVEL || 'info',
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
frontend: {
|
|
74
|
+
entry: './web/main.jsx',
|
|
75
|
+
globals: './web/styles/globals.css',
|
|
76
|
+
},
|
|
77
|
+
plugins: [
|
|
78
|
+
react(),
|
|
79
|
+
tailwindcss({
|
|
80
|
+
content: ['./web/**/*.{js,jsx}'],
|
|
81
|
+
}),
|
|
82
|
+
],
|
|
83
|
+
alias: {
|
|
84
|
+
'@': './web',
|
|
85
|
+
'@components': './web/components',
|
|
86
|
+
'@lib': './web/lib',
|
|
87
|
+
},
|
|
88
|
+
});
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__APP_NAME__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "nova dev",
|
|
8
|
+
"build": "nova build",
|
|
9
|
+
"start": "node server.js",
|
|
10
|
+
"lint": "eslint ."
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"dotenv": "^16.4.5",
|
|
14
|
+
"drizzle-orm": "^0.40.0",
|
|
15
|
+
"@untrustnova/nova-framework": "^0.1.0",
|
|
16
|
+
"react": "^19.0.0",
|
|
17
|
+
"react-dom": "^19.0.0"
|
|
18
|
+
},
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
21
|
+
"eslint": "^9.18.0",
|
|
22
|
+
"eslint-config-prettier": "^9.1.0",
|
|
23
|
+
"eslint-plugin-react": "^7.37.3",
|
|
24
|
+
"vite": "^6.0.0"
|
|
25
|
+
}
|
|
26
|
+
}
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import 'dotenv/config';
|
|
2
|
+
import config from './nova.config.js';
|
|
3
|
+
import { NovaKernel } from '@untrustnova/nova-framework/kernel';
|
|
4
|
+
import { storageModule, cacheModule, logsModule } from '@untrustnova/nova-framework/modules';
|
|
5
|
+
|
|
6
|
+
const app = new NovaKernel(config);
|
|
7
|
+
|
|
8
|
+
app.registerModule('storage', storageModule);
|
|
9
|
+
app.registerModule('cache', cacheModule);
|
|
10
|
+
app.registerModule('logs', logsModule);
|
|
11
|
+
|
|
12
|
+
app.start();
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export default function Hero() {
|
|
2
|
+
return (
|
|
3
|
+
<main className="hero">
|
|
4
|
+
<div className="badge">Nova</div>
|
|
5
|
+
<h1>DX bersih untuk frontend React & backend modular.</h1>
|
|
6
|
+
<p>
|
|
7
|
+
Mulai dari <code>nova dev</code>, lanjutkan dengan modul Storage, Cache,
|
|
8
|
+
Logs, dan DB yang bisa diganti driver-nya.
|
|
9
|
+
</p>
|
|
10
|
+
<div className="cta">
|
|
11
|
+
<button type="button">Lihat Dokumentasi</button>
|
|
12
|
+
<button type="button" className="ghost">
|
|
13
|
+
Buat Project
|
|
14
|
+
</button>
|
|
15
|
+
</div>
|
|
16
|
+
</main>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Nova.js</title>
|
|
7
|
+
</head>
|
|
8
|
+
<body>
|
|
9
|
+
<div id="root"></div>
|
|
10
|
+
<script type="module" src="/main.jsx"></script>
|
|
11
|
+
</body>
|
|
12
|
+
</html>
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
3
|
+
color: #e6edf3;
|
|
4
|
+
background-color: #0b1220;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
* {
|
|
8
|
+
box-sizing: border-box;
|
|
9
|
+
margin: 0;
|
|
10
|
+
padding: 0;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
body {
|
|
14
|
+
min-height: 100vh;
|
|
15
|
+
background: radial-gradient(circle at top, #1c2b4f 0%, #0b1220 60%);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.app-shell {
|
|
19
|
+
display: flex;
|
|
20
|
+
min-height: 100vh;
|
|
21
|
+
align-items: center;
|
|
22
|
+
justify-content: center;
|
|
23
|
+
padding: 4rem 1.5rem;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.hero {
|
|
27
|
+
max-width: 720px;
|
|
28
|
+
display: grid;
|
|
29
|
+
gap: 1.5rem;
|
|
30
|
+
background: rgba(15, 23, 42, 0.65);
|
|
31
|
+
border: 1px solid rgba(148, 163, 184, 0.2);
|
|
32
|
+
border-radius: 24px;
|
|
33
|
+
padding: 3rem;
|
|
34
|
+
backdrop-filter: blur(20px);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.hero h1 {
|
|
38
|
+
font-size: clamp(2rem, 3vw, 2.75rem);
|
|
39
|
+
line-height: 1.2;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.hero p {
|
|
43
|
+
font-size: 1.05rem;
|
|
44
|
+
color: #cbd5f5;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.badge {
|
|
48
|
+
width: fit-content;
|
|
49
|
+
padding: 0.4rem 0.9rem;
|
|
50
|
+
border-radius: 999px;
|
|
51
|
+
background: rgba(59, 130, 246, 0.2);
|
|
52
|
+
border: 1px solid rgba(59, 130, 246, 0.45);
|
|
53
|
+
color: #93c5fd;
|
|
54
|
+
font-weight: 600;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.cta {
|
|
58
|
+
display: flex;
|
|
59
|
+
gap: 1rem;
|
|
60
|
+
flex-wrap: wrap;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.cta button {
|
|
64
|
+
border: none;
|
|
65
|
+
border-radius: 12px;
|
|
66
|
+
padding: 0.8rem 1.5rem;
|
|
67
|
+
background: #3b82f6;
|
|
68
|
+
color: white;
|
|
69
|
+
font-weight: 600;
|
|
70
|
+
cursor: pointer;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.cta button.ghost {
|
|
74
|
+
background: transparent;
|
|
75
|
+
border: 1px solid rgba(148, 163, 184, 0.5);
|
|
76
|
+
color: #cbd5f5;
|
|
77
|
+
}
|