create-fuzionx 0.1.5
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/index.js +137 -0
- package/package.json +22 -0
- package/templates/.env.example.tpl +14 -0
- package/templates/.gitignore.tpl +4 -0
- package/templates/app.js.tpl +14 -0
- package/templates/controllers/HomeController.js +13 -0
- package/templates/fuzionx.yaml.tpl +30 -0
- package/templates/package.json.tpl +15 -0
- package/templates/routes/api.js.tpl +7 -0
- package/templates/routes/web.js.tpl +5 -0
- package/templates/views/default/errors/404.html +15 -0
- package/templates/views/default/errors/500.html +14 -0
- package/templates/views/default/layouts/main.html +22 -0
- package/templates/views/default/pages/home.html +188 -0
package/index.js
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* create-fuzionx — FuzionX 앱 스캐폴딩 CLI
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* npx create-fuzionx my-app
|
|
7
|
+
* npx create-fuzionx my-app /path/to/dir
|
|
8
|
+
*/
|
|
9
|
+
import { promises as fs } from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { fileURLToPath } from 'node:url';
|
|
12
|
+
|
|
13
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
14
|
+
const TPL_DIR = path.join(__dirname, 'templates');
|
|
15
|
+
|
|
16
|
+
// ── 템플릿 엔진 ({{var}} → value) ──
|
|
17
|
+
|
|
18
|
+
function render(template, vars) {
|
|
19
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_, key) => vars[key] ?? '');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function loadTemplate(relPath, vars) {
|
|
23
|
+
const raw = await fs.readFile(path.join(TPL_DIR, relPath), 'utf-8');
|
|
24
|
+
return render(raw, vars);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// ── 스캐폴딩 파일 목록 ──
|
|
28
|
+
|
|
29
|
+
const APP_FILES = [
|
|
30
|
+
{ tpl: 'package.json.tpl', dest: 'package.json' },
|
|
31
|
+
{ tpl: 'fuzionx.yaml.tpl', dest: 'fuzionx.yaml' },
|
|
32
|
+
{ tpl: 'app.js.tpl', dest: 'app.js' },
|
|
33
|
+
{ tpl: 'routes/web.js.tpl', dest: 'routes/web.js' },
|
|
34
|
+
{ tpl: 'routes/api.js.tpl', dest: 'routes/api.js' },
|
|
35
|
+
{ tpl: '.env.example.tpl', dest: '.env.example' },
|
|
36
|
+
{ tpl: '.env.example.tpl', dest: '.env' },
|
|
37
|
+
{ tpl: '.gitignore.tpl', dest: '.gitignore' },
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
const APP_DIRS = [
|
|
41
|
+
'controllers',
|
|
42
|
+
'models',
|
|
43
|
+
'services',
|
|
44
|
+
'middleware',
|
|
45
|
+
'ws',
|
|
46
|
+
'jobs',
|
|
47
|
+
'events',
|
|
48
|
+
'workers',
|
|
49
|
+
'migrations',
|
|
50
|
+
'seeds',
|
|
51
|
+
'storage/logs',
|
|
52
|
+
'storage/uploads',
|
|
53
|
+
'public/css',
|
|
54
|
+
'public/js',
|
|
55
|
+
'locales',
|
|
56
|
+
'tests',
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
// ── createApp ──
|
|
60
|
+
|
|
61
|
+
async function createApp(name, targetDir) {
|
|
62
|
+
const dir = targetDir || path.resolve(name);
|
|
63
|
+
const vars = {
|
|
64
|
+
name,
|
|
65
|
+
dbName: name.replace(/-/g, '_'),
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// 디렉토리 생성
|
|
69
|
+
for (const d of APP_DIRS) {
|
|
70
|
+
await fs.mkdir(path.join(dir, d), { recursive: true });
|
|
71
|
+
await fs.writeFile(path.join(dir, d, '.gitkeep'), '');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 템플릿 파일 생성
|
|
75
|
+
for (const { tpl, dest } of APP_FILES) {
|
|
76
|
+
const content = await loadTemplate(tpl, vars);
|
|
77
|
+
const fullPath = path.join(dir, dest);
|
|
78
|
+
await fs.mkdir(path.dirname(fullPath), { recursive: true });
|
|
79
|
+
await fs.writeFile(fullPath, content);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// HomeController 복사
|
|
83
|
+
const hcSrc = path.join(TPL_DIR, 'controllers/HomeController.js');
|
|
84
|
+
const hcDst = path.join(dir, 'controllers/HomeController.js');
|
|
85
|
+
await fs.copyFile(hcSrc, hcDst);
|
|
86
|
+
|
|
87
|
+
// Views — views/{theme}/ 구조
|
|
88
|
+
const viewsSrc = path.join(TPL_DIR, 'views/default');
|
|
89
|
+
const viewsDst = path.join(dir, 'views/default');
|
|
90
|
+
|
|
91
|
+
const layoutDir = path.join(viewsDst, 'layouts');
|
|
92
|
+
await fs.mkdir(layoutDir, { recursive: true });
|
|
93
|
+
await fs.copyFile(path.join(viewsSrc, 'layouts/main.html'), path.join(layoutDir, 'main.html'));
|
|
94
|
+
|
|
95
|
+
const pagesDir = path.join(viewsDst, 'pages');
|
|
96
|
+
await fs.mkdir(pagesDir, { recursive: true });
|
|
97
|
+
await fs.copyFile(path.join(viewsSrc, 'pages/home.html'), path.join(pagesDir, 'home.html'));
|
|
98
|
+
|
|
99
|
+
const errDir = path.join(viewsDst, 'errors');
|
|
100
|
+
await fs.mkdir(errDir, { recursive: true });
|
|
101
|
+
await fs.copyFile(path.join(viewsSrc, 'errors/404.html'), path.join(errDir, '404.html'));
|
|
102
|
+
await fs.copyFile(path.join(viewsSrc, 'errors/500.html'), path.join(errDir, '500.html'));
|
|
103
|
+
|
|
104
|
+
return dir;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── CLI 엔트리 ──
|
|
108
|
+
|
|
109
|
+
const name = process.argv[2];
|
|
110
|
+
const targetDir = process.argv[3];
|
|
111
|
+
|
|
112
|
+
if (!name) {
|
|
113
|
+
console.error(`
|
|
114
|
+
Usage: npx create-fuzionx <app-name> [target-dir]
|
|
115
|
+
|
|
116
|
+
Examples:
|
|
117
|
+
npx create-fuzionx my-app
|
|
118
|
+
npx create-fuzionx my-app /path/to/dir
|
|
119
|
+
`);
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
try {
|
|
124
|
+
const dir = await createApp(name, targetDir);
|
|
125
|
+
console.log(`
|
|
126
|
+
✅ Created ${name} at ${dir}
|
|
127
|
+
|
|
128
|
+
Next steps:
|
|
129
|
+
|
|
130
|
+
cd ${name}
|
|
131
|
+
npm install
|
|
132
|
+
npx fx dev
|
|
133
|
+
`);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
console.error(`❌ Failed to create app: ${err.message}`);
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-fuzionx",
|
|
3
|
+
"version": "0.1.5",
|
|
4
|
+
"description": "Create a new FuzionX application — npx create-fuzionx my-app",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-fuzionx": "./index.js"
|
|
8
|
+
},
|
|
9
|
+
"keywords": ["fuzionx", "create", "scaffold", "cli", "framework"],
|
|
10
|
+
"license": "MIT",
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "https://github.com/saytohenry/fuzionx"
|
|
14
|
+
},
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18.0.0"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"index.js",
|
|
20
|
+
"templates/"
|
|
21
|
+
]
|
|
22
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Application } from '@fuzionx/framework';
|
|
2
|
+
import webRoutes from './routes/web.js';
|
|
3
|
+
import apiRoutes from './routes/api.js';
|
|
4
|
+
|
|
5
|
+
const app = new Application({ configPath: './fuzionx.yaml' });
|
|
6
|
+
|
|
7
|
+
app.routes(webRoutes);
|
|
8
|
+
app.routes(apiRoutes);
|
|
9
|
+
|
|
10
|
+
await app.boot();
|
|
11
|
+
|
|
12
|
+
app.listen(49080, () => {
|
|
13
|
+
console.log('🚀 FuzionX running on http://localhost:49080');
|
|
14
|
+
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# FuzionX Configuration
|
|
2
|
+
bridge:
|
|
3
|
+
port: 49080
|
|
4
|
+
workers: 4
|
|
5
|
+
worker_timeout: 30
|
|
6
|
+
|
|
7
|
+
rate_limit:
|
|
8
|
+
enabled: true
|
|
9
|
+
per_ip: 1000
|
|
10
|
+
|
|
11
|
+
database:
|
|
12
|
+
main:
|
|
13
|
+
driver: sqlite
|
|
14
|
+
path: ./storage/database.sqlite
|
|
15
|
+
|
|
16
|
+
app:
|
|
17
|
+
name: '{{name}}'
|
|
18
|
+
environment: development
|
|
19
|
+
auth:
|
|
20
|
+
secret: 'change-me-in-production'
|
|
21
|
+
accessTtl: '15m'
|
|
22
|
+
i18n:
|
|
23
|
+
default_locale: 'ko'
|
|
24
|
+
fallback: 'en'
|
|
25
|
+
docs:
|
|
26
|
+
enabled: true
|
|
27
|
+
path: '/docs'
|
|
28
|
+
|
|
29
|
+
themes:
|
|
30
|
+
default: 'default'
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{% extends "layouts/main.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}404 — 찾을 수 없습니다{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block content %}
|
|
6
|
+
<div style="text-align:center;padding:80px 20px;">
|
|
7
|
+
<h1 style="font-size:72px;color:#e74c3c;">{{ error.code }}</h1>
|
|
8
|
+
<p style="font-size:20px;margin:16px 0;">{{ error.message }}</p>
|
|
9
|
+
<p style="color:#888;">요청: {{ request.url }}</p>
|
|
10
|
+
{% if config.debug %}
|
|
11
|
+
<pre style="text-align:left;max-width:600px;margin:24px auto;background:#f5f5f5;padding:16px;border-radius:8px;overflow:auto;">{{ error.stack }}</pre>
|
|
12
|
+
{% endif %}
|
|
13
|
+
<a href="/" style="display:inline-block;margin-top:24px;padding:12px 24px;background:#e74c3c;color:#fff;text-decoration:none;border-radius:6px;">홈으로 돌아가기</a>
|
|
14
|
+
</div>
|
|
15
|
+
{% endblock %}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{% extends "layouts/main.html" %}
|
|
2
|
+
|
|
3
|
+
{% block title %}500 — 서버 오류{% endblock %}
|
|
4
|
+
|
|
5
|
+
{% block content %}
|
|
6
|
+
<div style="text-align:center;padding:80px 20px;">
|
|
7
|
+
<h1 style="font-size:72px;color:#e74c3c;">500</h1>
|
|
8
|
+
<p style="font-size:20px;margin:16px 0;">{{ error.message | default(value='Internal Server Error') }}</p>
|
|
9
|
+
{% if config.debug and error.stack %}
|
|
10
|
+
<pre style="text-align:left;max-width:600px;margin:24px auto;background:#f5f5f5;padding:16px;border-radius:8px;overflow:auto;">{{ error.stack }}</pre>
|
|
11
|
+
{% endif %}
|
|
12
|
+
<a href="/" style="display:inline-block;margin-top:24px;padding:12px 24px;background:#e74c3c;color:#fff;text-decoration:none;border-radius:6px;">홈으로 돌아가기</a>
|
|
13
|
+
</div>
|
|
14
|
+
{% endblock %}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="{{ locale | default(value='ko') }}">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>{% block title %}{{ config.app.name | default(value='FuzionX') }}{% endblock %}</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
body { font-family: system-ui, -apple-system, sans-serif; line-height: 1.6; color: #333; }
|
|
10
|
+
</style>
|
|
11
|
+
{% block head %}{% endblock %}
|
|
12
|
+
</head>
|
|
13
|
+
<body>
|
|
14
|
+
{% include "partials/header.html" ignore missing %}
|
|
15
|
+
|
|
16
|
+
<main>
|
|
17
|
+
{% block content %}{% endblock %}
|
|
18
|
+
</main>
|
|
19
|
+
|
|
20
|
+
{% include "partials/footer.html" ignore missing %}
|
|
21
|
+
</body>
|
|
22
|
+
</html>
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="ko">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>FuzionX</title>
|
|
7
|
+
<style>
|
|
8
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
9
|
+
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;800&display=swap');
|
|
10
|
+
|
|
11
|
+
body {
|
|
12
|
+
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
|
13
|
+
min-height: 100vh;
|
|
14
|
+
display: flex;
|
|
15
|
+
align-items: center;
|
|
16
|
+
justify-content: center;
|
|
17
|
+
background: linear-gradient(135deg, #0f0c29 0%, #1a1a3e 40%, #24243e 100%);
|
|
18
|
+
color: #e0e0e0;
|
|
19
|
+
overflow: hidden;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.container {
|
|
23
|
+
text-align: center;
|
|
24
|
+
z-index: 1;
|
|
25
|
+
animation: fadeInUp 0.8s ease-out;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
.logo {
|
|
29
|
+
font-size: 4rem;
|
|
30
|
+
font-weight: 800;
|
|
31
|
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 50%, #f093fb 100%);
|
|
32
|
+
-webkit-background-clip: text;
|
|
33
|
+
background-clip: text;
|
|
34
|
+
-webkit-text-fill-color: transparent;
|
|
35
|
+
letter-spacing: -2px;
|
|
36
|
+
margin-bottom: 0.5rem;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.subtitle {
|
|
40
|
+
font-size: 1.1rem;
|
|
41
|
+
font-weight: 300;
|
|
42
|
+
color: rgba(255, 255, 255, 0.5);
|
|
43
|
+
margin-bottom: 2.5rem;
|
|
44
|
+
letter-spacing: 2px;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.card {
|
|
48
|
+
background: rgba(255, 255, 255, 0.05);
|
|
49
|
+
backdrop-filter: blur(20px);
|
|
50
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
51
|
+
border-radius: 16px;
|
|
52
|
+
padding: 2rem 3rem;
|
|
53
|
+
max-width: 480px;
|
|
54
|
+
margin: 0 auto 2rem;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.version {
|
|
58
|
+
display: inline-block;
|
|
59
|
+
background: linear-gradient(135deg, #667eea, #764ba2);
|
|
60
|
+
color: white;
|
|
61
|
+
padding: 4px 14px;
|
|
62
|
+
border-radius: 20px;
|
|
63
|
+
font-size: 0.75rem;
|
|
64
|
+
font-weight: 600;
|
|
65
|
+
letter-spacing: 1px;
|
|
66
|
+
margin-bottom: 1.5rem;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.features {
|
|
70
|
+
display: grid;
|
|
71
|
+
grid-template-columns: 1fr 1fr;
|
|
72
|
+
gap: 1rem;
|
|
73
|
+
text-align: left;
|
|
74
|
+
margin-top: 1.5rem;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.feature {
|
|
78
|
+
display: flex;
|
|
79
|
+
align-items: center;
|
|
80
|
+
gap: 8px;
|
|
81
|
+
font-size: 0.85rem;
|
|
82
|
+
color: rgba(255, 255, 255, 0.7);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.feature span {
|
|
86
|
+
font-size: 1.1rem;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.links {
|
|
90
|
+
display: flex;
|
|
91
|
+
gap: 1rem;
|
|
92
|
+
justify-content: center;
|
|
93
|
+
margin-top: 1rem;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.links a {
|
|
97
|
+
color: rgba(255, 255, 255, 0.6);
|
|
98
|
+
text-decoration: none;
|
|
99
|
+
font-size: 0.85rem;
|
|
100
|
+
padding: 8px 20px;
|
|
101
|
+
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
102
|
+
border-radius: 8px;
|
|
103
|
+
transition: all 0.3s ease;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.links a:hover {
|
|
107
|
+
color: #fff;
|
|
108
|
+
border-color: #667eea;
|
|
109
|
+
background: rgba(102, 126, 234, 0.1);
|
|
110
|
+
transform: translateY(-2px);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.hint {
|
|
114
|
+
margin-top: 2rem;
|
|
115
|
+
font-size: 0.75rem;
|
|
116
|
+
color: rgba(255, 255, 255, 0.3);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.hint code {
|
|
120
|
+
background: rgba(255, 255, 255, 0.08);
|
|
121
|
+
padding: 2px 8px;
|
|
122
|
+
border-radius: 4px;
|
|
123
|
+
font-family: 'JetBrains Mono', monospace;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* Background orbs */
|
|
127
|
+
.orb {
|
|
128
|
+
position: fixed;
|
|
129
|
+
border-radius: 50%;
|
|
130
|
+
filter: blur(80px);
|
|
131
|
+
opacity: 0.3;
|
|
132
|
+
animation: float 8s ease-in-out infinite;
|
|
133
|
+
}
|
|
134
|
+
.orb-1 { width: 400px; height: 400px; background: #667eea; top: -100px; right: -100px; }
|
|
135
|
+
.orb-2 { width: 300px; height: 300px; background: #764ba2; bottom: -80px; left: -80px; animation-delay: -4s; }
|
|
136
|
+
.orb-3 { width: 200px; height: 200px; background: #f093fb; top: 50%; left: 60%; animation-delay: -2s; }
|
|
137
|
+
|
|
138
|
+
@keyframes fadeInUp {
|
|
139
|
+
from { opacity: 0; transform: translateY(30px); }
|
|
140
|
+
to { opacity: 1; transform: translateY(0); }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
@keyframes float {
|
|
144
|
+
0%, 100% { transform: translate(0, 0); }
|
|
145
|
+
50% { transform: translate(30px, -30px); }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
@media (max-width: 600px) {
|
|
149
|
+
.logo { font-size: 2.5rem; }
|
|
150
|
+
.card { padding: 1.5rem; margin: 0 1rem; }
|
|
151
|
+
.features { grid-template-columns: 1fr; }
|
|
152
|
+
}
|
|
153
|
+
</style>
|
|
154
|
+
</head>
|
|
155
|
+
<body>
|
|
156
|
+
<div class="orb orb-1"></div>
|
|
157
|
+
<div class="orb orb-2"></div>
|
|
158
|
+
<div class="orb orb-3"></div>
|
|
159
|
+
|
|
160
|
+
<div class="container">
|
|
161
|
+
<h1 class="logo">FuzionX</h1>
|
|
162
|
+
<p class="subtitle">HIGH-PERFORMANCE NODE.JS FRAMEWORK</p>
|
|
163
|
+
|
|
164
|
+
<div class="card">
|
|
165
|
+
<div class="version">v0.1.0 · POWERED BY RUST</div>
|
|
166
|
+
|
|
167
|
+
<div class="features">
|
|
168
|
+
<div class="feature"><span>⚡</span> 500K+ RPS</div>
|
|
169
|
+
<div class="feature"><span>🦀</span> Rust N-API Bridge</div>
|
|
170
|
+
<div class="feature"><span>🎯</span> MVC Architecture</div>
|
|
171
|
+
<div class="feature"><span>🔌</span> WebSocket</div>
|
|
172
|
+
<div class="feature"><span>🗄️</span> Multi-DB ORM</div>
|
|
173
|
+
<div class="feature"><span>🔐</span> Auth & Session</div>
|
|
174
|
+
<div class="feature"><span>📡</span> Event System</div>
|
|
175
|
+
<div class="feature"><span>⏰</span> Job Scheduler</div>
|
|
176
|
+
</div>
|
|
177
|
+
</div>
|
|
178
|
+
|
|
179
|
+
<div class="links">
|
|
180
|
+
<a href="https://github.com/saytohenry/fuzionx">GitHub</a>
|
|
181
|
+
<a href="/docs">API Docs</a>
|
|
182
|
+
<a href="/api/health">Health Check</a>
|
|
183
|
+
</div>
|
|
184
|
+
|
|
185
|
+
<p class="hint">Edit <code>routes/web.js</code> to get started</p>
|
|
186
|
+
</div>
|
|
187
|
+
</body>
|
|
188
|
+
</html>
|