@velox0/cerver 0.4.1 → 0.4.3

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.
@@ -0,0 +1,35 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ pull_request:
6
+
7
+ jobs:
8
+ tests:
9
+ name: Tests
10
+ runs-on: ubuntu-latest
11
+
12
+ steps:
13
+ - name: Checkout
14
+ uses: actions/checkout@v6
15
+
16
+ - name: Setup pnpm
17
+ uses: pnpm/action-setup@v6
18
+ with:
19
+ version: 10
20
+ run_install: false
21
+
22
+ - name: Setup Node.js
23
+ uses: actions/setup-node@v6
24
+ with:
25
+ node-version: 24
26
+ cache: pnpm
27
+
28
+ - name: Install dependencies
29
+ run: pnpm install --frozen-lockfile
30
+
31
+ - name: Run runtime tests
32
+ run: make test-runtime
33
+
34
+ - name: Run JS tests
35
+ run: pnpm test
@@ -35,6 +35,9 @@ jobs:
35
35
  - name: Install dependencies
36
36
  run: pnpm install --frozen-lockfile
37
37
 
38
+ - name: Run runtime tests
39
+ run: make test-runtime
40
+
38
41
  - name: Run tests
39
42
  run: pnpm test
40
43
 
package/Makefile ADDED
@@ -0,0 +1,26 @@
1
+ CC ?= cc
2
+ CFLAGS ?= -std=c11 -Wall -Wextra -O2
3
+
4
+ RUNTIME_SRCS = \
5
+ runtime/http_parser.c \
6
+ runtime/http_writer.c \
7
+ runtime/router.c \
8
+ runtime/static.c \
9
+ runtime/mime.c \
10
+ runtime/server.c
11
+
12
+ TEST_SRCS = runtime/tests/runtime_tests.c \
13
+ runtime/tests/minunit.c
14
+ TEST_BIN = build/runtime_tests
15
+
16
+ .PHONY: test-runtime clean
17
+
18
+ test-runtime: $(TEST_BIN)
19
+ ./$(TEST_BIN)
20
+
21
+ $(TEST_BIN): $(RUNTIME_SRCS) $(TEST_SRCS) runtime/cerver.h
22
+ mkdir -p build
23
+ $(CC) $(CFLAGS) -Iruntime -o $(TEST_BIN) $(RUNTIME_SRCS) $(TEST_SRCS) -pthread
24
+
25
+ clean:
26
+ rm -rf build
package/README.md CHANGED
@@ -1,9 +1,11 @@
1
- <div align="center">
2
- <img src="templates/cerver.png" alt="Cerver Logo" width="120" />
3
- </div>
4
-
5
1
  # Cerver
6
2
 
3
+ [![Publish](https://github.com/velox0/cerver/actions/workflows/publish.yml/badge.svg)](https://github.com/velox0/cerver/actions/workflows/publish.yml)
4
+ [![CI](https://github.com/velox0/cerver/actions/workflows/ci.yml/badge.svg)](https://github.com/velox0/cerver/actions/workflows/ci.yml)
5
+ [![npm](https://img.shields.io/npm/v/@velox0/cerver)](https://www.npmjs.com/package/@velox0/cerver)
6
+
7
+ <img src="templates/cerver.png" alt="Cerver Logo" width="200px" align="right" />
8
+
7
9
  A lightweight, compile-time web framework that transpiles restricted JavaScript server logic into highly optimized native C HTTP server binaries.
8
10
 
9
11
  Cerver takes a Next.js-style file-based routing structure (written in a strict subset of JavaScript), parses it, generates equivalent C code, embeds your static assets, and compiles it all into a single, standalone executable that runs with zero Node.js dependency.
@@ -11,11 +13,26 @@ Cerver takes a Next.js-style file-based routing structure (written in a strict s
11
13
  ## Features
12
14
 
13
15
  - **Compile-Time Framework**: Your JavaScript is parsed and compiled to native C. There is no JavaScript engine (like V8) or interpreter included in the final binary.
14
- - **Microscopic Footprint**: Generated executables are typically ~50KB and start in milliseconds.
16
+ - **Microscopic Footprint**: Generated executables are tiny and start in milliseconds.
15
17
  - **Single-Binary Deployment**: Static assets (HTML, CSS, JS, images) are automatically minified and embedded directly into the executable as C byte arrays.
16
18
  - **Native Performance**: Uses `kqueue` (macOS) or `epoll` (Linux) event loops for high-performance non-blocking I/O.
17
19
  - **File-Based Routing**: Intuitive `app/routes/` directory structure, supporting dynamic segments (e.g., `/item/[id].js`).
18
20
 
21
+ ## Benchmarks (Autocannon)
22
+
23
+ Local loopback runs against `localhost`, 20s per run.
24
+
25
+ Note: timeouts only appear at the highest concurrency (240 connections). On a single machine, autocannon and the server compete for CPU and kernel resources; the timeouts are likely client/loopback saturation rather than server errors.
26
+
27
+ | Connections | Pipelining | Avg req/s | Avg latency | p99 latency | Total read | Errors (timeouts) |
28
+ | ----------- | ---------- | --------- | ----------- | ----------- | ---------- | ----------------- |
29
+ | 60 | 1 | 123,005 | 0.01 ms | 0 ms | 21.0 GB | 0 |
30
+ | 60 | 10 | 125,973 | 4.26 ms | 10 ms | 21.5 GB | 0 |
31
+ | 120 | 1 | 124,214 | 0.15 ms | 1 ms | 21.2 GB | 0 |
32
+ | 120 | 10 | 131,245 | 8.64 ms | 15 ms | 22.4 GB | 0 |
33
+ | 240 | 1 | 124,890 | 0.54 ms | 1 ms | 21.3 GB | 118 (timeout) |
34
+ | 240 | 10 | 123,677 | 9.87 ms | 14 ms | 21.1 GB | 1540 (timeout) |
35
+
19
36
  ## Getting Started
20
37
 
21
38
  1. Install globally (requires `gcc` or `clang` on your system):
package/bin/cerver.js CHANGED
@@ -7,6 +7,17 @@ const pkg = require("../package.json");
7
7
 
8
8
  const program = new Command();
9
9
 
10
+ function restoreTty() {
11
+ const stdin = process.stdin;
12
+ if (!stdin || !stdin.isTTY || typeof stdin.setRawMode !== "function") return;
13
+ if (!stdin.isRaw) return;
14
+ try {
15
+ stdin.setRawMode(false);
16
+ } catch (_) {}
17
+ }
18
+
19
+ process.on("exit", restoreTty);
20
+
10
21
  program
11
22
  .name("cerver")
12
23
  .description("Compile restricted JavaScript into native C server binaries")
@@ -162,6 +162,9 @@ function generateDispatch(routes) {
162
162
  lines.push(
163
163
  ` req->params[req->params_count].value = seg${i}_start;`
164
164
  );
165
+ lines.push(
166
+ ` ((char*)seg${i}_start)[seg${i}_len] = '\\0';`
167
+ );
165
168
  lines.push(` req->params_count++;`);
166
169
  paramIdx++;
167
170
  }
@@ -16,6 +16,7 @@ async function dev(opts) {
16
16
  let serverProcess = null;
17
17
  let building = false;
18
18
  let pendingRebuild = false;
19
+ let shuttingDown = false;
19
20
 
20
21
  const port = opts.port || null;
21
22
 
@@ -40,7 +41,7 @@ async function dev(opts) {
40
41
  try {
41
42
  await build({
42
43
  embed: opts.embed !== undefined ? opts.embed : true,
43
- minify: false, /* Skip minification in dev for speed */
44
+ minify: false /* Skip minification in dev for speed */,
44
45
  static: false,
45
46
  });
46
47
 
@@ -84,6 +85,16 @@ async function dev(opts) {
84
85
  });
85
86
  }
86
87
 
88
+ function waitForExit(proc) {
89
+ if (!proc) return Promise.resolve();
90
+ return new Promise((resolve) => {
91
+ const done = () => resolve();
92
+ proc.once("exit", done);
93
+ proc.once("close", done);
94
+ proc.once("error", done);
95
+ });
96
+ }
97
+
87
98
  /* ---- Watch for changes ---- */
88
99
  const watchPaths = [
89
100
  path.join(projectDir, "app"),
@@ -92,11 +103,7 @@ async function dev(opts) {
92
103
  ].filter((p) => fs.existsSync(p));
93
104
 
94
105
  const watcher = chokidar.watch(watchPaths, {
95
- ignored: [
96
- /(^|[\/\\])\./, /* dotfiles */
97
- /node_modules/,
98
- /dist/,
99
- ],
106
+ ignored: [/(^|[\/\\])\./ /* dotfiles */, /node_modules/, /dist/],
100
107
  persistent: true,
101
108
  ignoreInitial: true,
102
109
  awaitWriteFinish: {
@@ -125,18 +132,25 @@ async function dev(opts) {
125
132
  .on("unlink", scheduleRebuild);
126
133
 
127
134
  /* ---- Graceful shutdown ---- */
128
- function shutdown() {
135
+ async function shutdown() {
136
+ if (shuttingDown) return;
137
+ shuttingDown = true;
129
138
  console.log("\n cerver dev: shutting down...");
130
139
  watcher.close();
131
140
  if (debounceTimer) clearTimeout(debounceTimer);
132
141
  if (serverProcess) {
133
142
  serverProcess.kill("SIGTERM");
143
+ await waitForExit(serverProcess);
134
144
  }
135
145
  process.exit(0);
136
146
  }
137
147
 
138
- process.on("SIGINT", shutdown);
139
- process.on("SIGTERM", shutdown);
148
+ process.on("SIGINT", () => {
149
+ void shutdown();
150
+ });
151
+ process.on("SIGTERM", () => {
152
+ void shutdown();
153
+ });
140
154
 
141
155
  /* ---- Start ---- */
142
156
  console.log("\n cerver dev — watching for changes\n");
@@ -17,13 +17,7 @@ function newProject(name) {
17
17
  console.log(`\n Creating cerver project: ${name}\n`);
18
18
 
19
19
  // Create directory structure
20
- const dirs = [
21
- "",
22
- "app",
23
- "app/routes",
24
- "public",
25
- "dist",
26
- ];
20
+ const dirs = ["", "app", "app/routes", "public", "dist"];
27
21
 
28
22
  for (const dir of dirs) {
29
23
  fs.mkdirSync(path.join(projectDir, dir), { recursive: true });
@@ -35,13 +29,13 @@ function newProject(name) {
35
29
  // cerver.config.js
36
30
  fs.copyFileSync(
37
31
  path.join(templatesDir, "cerver.config.js"),
38
- path.join(projectDir, "cerver.config.js")
32
+ path.join(projectDir, "cerver.config.js"),
39
33
  );
40
34
 
41
35
  // Default route
42
36
  fs.copyFileSync(
43
37
  path.join(templatesDir, "index.route.js"),
44
- path.join(projectDir, "app", "routes", "index.js")
38
+ path.join(projectDir, "app", "routes", "index.js"),
45
39
  );
46
40
 
47
41
  // Default public/index.html
@@ -54,100 +48,300 @@ function newProject(name) {
54
48
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
55
49
  <title>${name}</title>
56
50
  <link rel="icon" href="/favicon.ico" type="image/x-icon">
51
+ <link rel="preconnect" href="https://fonts.googleapis.com">
52
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
53
+ <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=Unbounded:wght@500;700&display=swap" rel="stylesheet">
57
54
  <style>
58
55
  :root {
59
- --bg-color: #0f172a;
60
- --text-color: #f8fafc;
61
- --accent-color: #38bdf8;
62
- --card-bg: rgba(30, 41, 59, 0.7);
56
+ --ink: #121316;
57
+ --muted: #4a4f57;
58
+ --paper: #f7f4f1;
59
+ --glass: rgba(255, 255, 255, 0.7);
60
+ --edge: rgba(255, 255, 255, 0.6);
61
+ --pink: #ff7abf;
62
+ --peach: #ffb380;
63
+ --mint: #7de3c9;
64
+ --blue: #6aa9ff;
65
+ --shadow: rgba(18, 19, 22, 0.18);
66
+ }
67
+ * {
68
+ box-sizing: border-box;
63
69
  }
64
70
  body {
65
71
  margin: 0;
66
- padding: 0;
67
- font-family: 'Inter', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
68
- background-color: var(--bg-color);
69
- color: var(--text-color);
70
- display: flex;
71
- flex-direction: column;
72
- align-items: center;
73
- justify-content: center;
72
+ font-family: "Space Grotesk", "Segoe UI", sans-serif;
73
+ color: var(--ink);
74
74
  min-height: 100vh;
75
- background: radial-gradient(circle at top right, #1e293b, #0f172a);
76
- }
77
- .container {
78
- background: var(--card-bg);
79
- backdrop-filter: blur(12px);
80
- -webkit-backdrop-filter: blur(12px);
81
- border: 1px solid rgba(255, 255, 255, 0.1);
82
- padding: 3rem;
83
- border-radius: 16px;
84
- text-align: center;
85
- box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.5);
86
- max-width: 500px;
87
- width: 90%;
88
- animation: fadeUp 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
89
- opacity: 0;
90
- transform: translateY(20px);
91
- }
92
- @keyframes fadeUp {
93
- to { opacity: 1; transform: translateY(0); }
94
- }
95
- .logo {
96
- width: 120px;
97
- height: 120px;
98
- margin-bottom: 1.5rem;
99
- border-radius: 24%;
100
- box-shadow: 0 10px 25px rgba(0, 0, 0, 0.4);
101
- transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
102
- }
103
- .logo:hover {
104
- transform: scale(1.08) rotate(-3deg);
75
+ display: grid;
76
+ place-items: center;
77
+ background-color: var(--paper);
78
+ background-image:
79
+ radial-gradient(circle at 15% 10%, rgba(255, 122, 191, 0.35), transparent 45%),
80
+ radial-gradient(circle at 85% 12%, rgba(255, 179, 128, 0.4), transparent 50%),
81
+ radial-gradient(circle at 82% 82%, rgba(122, 214, 255, 0.35), transparent 55%),
82
+ radial-gradient(circle at 20% 80%, rgba(125, 227, 201, 0.35), transparent 55%),
83
+ linear-gradient(120deg, #f7f4f1 0%, #f2f7ff 100%);
84
+ }
85
+ body::before,
86
+ body::after {
87
+ content: "";
88
+ position: fixed;
89
+ inset: -20% -10%;
90
+ pointer-events: none;
91
+ }
92
+ body::before {
93
+ background:
94
+ conic-gradient(from 200deg at 50% 50%, rgba(255, 122, 191, 0.08), rgba(122, 214, 255, 0.08), rgba(125, 227, 201, 0.08), rgba(255, 179, 128, 0.08));
95
+ filter: blur(60px);
96
+ opacity: 0.6;
97
+ }
98
+ body::after {
99
+ background-image: radial-gradient(circle, rgba(18, 19, 22, 0.04) 1px, transparent 1px);
100
+ background-size: 24px 24px;
101
+ opacity: 0.6;
102
+ }
103
+ .stage {
104
+ width: min(1100px, 92vw);
105
+ padding: 4rem 0;
106
+ }
107
+ .frame {
108
+ position: relative;
109
+ display: grid;
110
+ grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
111
+ gap: 2.5rem;
112
+ align-items: center;
113
+ padding: clamp(2.5rem, 4vw, 4rem);
114
+ border-radius: 32px;
115
+ background: var(--glass);
116
+ border: 1px solid var(--edge);
117
+ box-shadow:
118
+ 0 40px 90px -40px var(--shadow),
119
+ inset 0 1px 0 rgba(255, 255, 255, 0.7);
120
+ backdrop-filter: blur(18px);
121
+ -webkit-backdrop-filter: blur(18px);
122
+ animation: rise 0.8s cubic-bezier(0.16, 1, 0.3, 1) both;
105
123
  }
106
124
  h1 {
107
- margin: 0 0 1rem 0;
108
- font-size: 2.5rem;
109
- font-weight: 700;
110
- letter-spacing: -0.025em;
125
+ margin: 0.6rem 0 1rem;
126
+ font-family: "Unbounded", "Space Grotesk", sans-serif;
127
+ font-size: clamp(2.2rem, 4vw, 4.4rem);
128
+ letter-spacing: 0.08em;
129
+ text-transform: uppercase;
111
130
  }
112
131
  p {
132
+ margin: 0 0 1.5rem;
133
+ font-size: 1.1rem;
134
+ line-height: 1.7;
135
+ color: var(--muted);
136
+ max-width: 32rem;
137
+ }
138
+ .steps {
139
+ margin: 0 0 1.8rem;
140
+ }
141
+ .steps-box {
142
+ padding: 1rem 1.2rem;
143
+ border-radius: 18px;
144
+ background: rgba(255, 255, 255, 0.35);
145
+ border: 1px solid rgba(18, 19, 22, 0.08);
146
+ backdrop-filter: blur(8px);
147
+ -webkit-backdrop-filter: blur(8px);
148
+ }
149
+ .steps-title {
150
+ margin: 0 0 0.7rem;
151
+ font-size: 0.75rem;
152
+ text-transform: uppercase;
153
+ letter-spacing: 0.18em;
154
+ color: var(--muted);
155
+ }
156
+ .steps-list {
113
157
  margin: 0;
114
- color: #94a3b8;
115
- font-size: 1.125rem;
116
- line-height: 1.6;
117
- }
118
- .badge {
119
- display: inline-block;
120
- margin-top: 2rem;
121
- padding: 0.5rem 1rem;
122
- background: rgba(56, 189, 248, 0.1);
123
- color: var(--accent-color);
124
- border-radius: 9999px;
125
- font-size: 0.875rem;
158
+ padding-left: 1.1rem;
159
+ display: grid;
160
+ gap: 0.55rem;
161
+ color: var(--muted);
162
+ }
163
+ .steps-list li::marker {
164
+ color: var(--muted);
165
+ }
166
+ .steps-list strong {
167
+ color: #2c3138;
168
+ font-weight: 600;
169
+ }
170
+ .meta {
171
+ display: inline-flex;
172
+ align-items: center;
173
+ gap: 0.6rem;
174
+ font-weight: 600;
175
+ color: #2c3138;
176
+ }
177
+ .dot {
178
+ width: 10px;
179
+ height: 10px;
180
+ border-radius: 50%;
181
+ background: linear-gradient(135deg, var(--pink), var(--blue));
182
+ box-shadow: 0 0 12px rgba(122, 214, 255, 0.8);
183
+ }
184
+ .footer {
185
+ margin-top: 1.8rem;
186
+ text-align: center;
187
+ font-size: 0.9rem;
188
+ color: var(--muted);
189
+ }
190
+ .footer a {
191
+ color: inherit;
192
+ text-decoration: none;
126
193
  font-weight: 600;
127
- border: 1px solid rgba(56, 189, 248, 0.2);
194
+ }
195
+ .footer a:hover {
196
+ text-decoration: underline;
197
+ }
198
+ .art {
199
+ position: relative;
200
+ display: grid;
201
+ place-items: center;
202
+ min-height: 280px;
203
+ }
204
+ .art::before {
205
+ content: "";
206
+ position: absolute;
207
+ width: min(320px, 70vw);
208
+ aspect-ratio: 1;
209
+ border-radius: 28%;
210
+ background: linear-gradient(140deg, rgba(255, 122, 191, 0.25), rgba(122, 214, 255, 0.2), rgba(125, 227, 201, 0.25));
211
+ filter: blur(10px);
212
+ transform: rotate(18deg);
213
+ }
214
+ .art img {
215
+ width: min(300px, 65vw);
216
+ height: auto;
217
+ border-radius: 18%;
218
+ filter: drop-shadow(0 25px 40px rgba(18, 19, 22, 0.25));
219
+ animation: float 6s ease-in-out infinite;
220
+ }
221
+ @keyframes float {
222
+ 0%, 100% { transform: translateY(0px) rotate(-2deg); }
223
+ 50% { transform: translateY(-12px) rotate(2deg); }
224
+ }
225
+ @keyframes rise {
226
+ from { opacity: 0; transform: translateY(18px) scale(0.98); }
227
+ to { opacity: 1; transform: translateY(0) scale(1); }
228
+ }
229
+ @media (max-width: 760px) {
230
+ .stage {
231
+ padding: 2.5rem 0;
232
+ }
233
+ .frame {
234
+ padding: 2.2rem;
235
+ }
236
+ h1 {
237
+ letter-spacing: 0.05em;
238
+ }
239
+ .art {
240
+ order: -1;
241
+ }
242
+ }
243
+ @media (prefers-color-scheme: dark) {
244
+ :root {
245
+ --ink: #f7f8fb;
246
+ --muted: #c2c8d0;
247
+ --paper: #0b0e13;
248
+ --glass: rgba(15, 19, 28, 0.78);
249
+ --edge: rgba(255, 255, 255, 0.12);
250
+ --shadow: rgba(0, 0, 0, 0.65);
251
+ }
252
+ body {
253
+ background-image:
254
+ radial-gradient(circle at 15% 10%, rgba(255, 122, 191, 0.45), transparent 45%),
255
+ radial-gradient(circle at 85% 12%, rgba(255, 179, 128, 0.45), transparent 50%),
256
+ radial-gradient(circle at 82% 82%, rgba(106, 169, 255, 0.45), transparent 55%),
257
+ radial-gradient(circle at 20% 80%, rgba(125, 227, 201, 0.4), transparent 55%),
258
+ linear-gradient(120deg, #0b0e13 0%, #121826 100%);
259
+ }
260
+ body::before {
261
+ opacity: 0.95;
262
+ filter: blur(90px);
263
+ }
264
+ body::after {
265
+ opacity: 0.2;
266
+ }
267
+ .frame {
268
+ box-shadow:
269
+ 0 50px 120px -40px rgba(0, 0, 0, 0.75),
270
+ 0 0 120px rgba(106, 169, 255, 0.25),
271
+ inset 0 1px 0 rgba(255, 255, 255, 0.08);
272
+ }
273
+ .steps-box {
274
+ background: rgba(6, 8, 14, 0.45);
275
+ border: 1px solid rgba(255, 255, 255, 0.12);
276
+ }
277
+ .steps-list strong {
278
+ color: #eef1f6;
279
+ }
280
+ .meta {
281
+ color: #e6e9ef;
282
+ }
283
+ .dot {
284
+ box-shadow: 0 0 18px rgba(255, 122, 191, 0.9);
285
+ }
286
+ .art::before {
287
+ filter: blur(18px);
288
+ }
289
+ .art img {
290
+ filter: drop-shadow(0 30px 60px rgba(106, 169, 255, 0.45));
291
+ }
292
+ .footer {
293
+ color: #c2c8d0;
294
+ }
295
+ }
296
+ @media (prefers-reduced-motion: reduce) {
297
+ .frame,
298
+ .art img {
299
+ animation: none;
300
+ }
128
301
  }
129
302
  </style>
130
303
  </head>
131
304
  <body>
132
- <div class="container">
133
- <img src="/cerver.png" alt="cerver logo" class="logo">
134
- <h1>${name}</h1>
135
- <p>Your ultra-fast, native web application is running.</p>
136
- <div class="badge">Powered by cerver</div>
137
- </div>
305
+ <main class="stage">
306
+ <section class="frame">
307
+ <div class="copy">
308
+ <h1>${name}</h1>
309
+ <p>Native-speed web apps with a glassy sheen. Your new cerver project is wired, built, and ready to ship.</p>
310
+ <div class="steps">
311
+ <div class="steps-box">
312
+ <div class="steps-title">Next steps</div>
313
+ <ul class="steps-list">
314
+ <li>Edit <strong>public/index.html</strong> to change this page.</li>
315
+ <li>Config lives in <strong>cerver.config.js</strong> at the project root.</li>
316
+ </ul>
317
+ </div>
318
+ </div>
319
+ <div class="meta">
320
+ <span class="dot"></span>
321
+ <span>Powered by cerver</span>
322
+ </div>
323
+ </div>
324
+ <div class="art">
325
+ <img src="/cerver.png" alt="cerver logo">
326
+ </div>
327
+ </section>
328
+ <footer class="footer">
329
+ <a href="https://github.com/velox0/cerver" target="_blank" rel="noreferrer">GitHub: github.com/velox0/cerver</a>
330
+ </footer>
331
+ </main>
138
332
  </body>
139
333
  </html>
140
- `
334
+ `,
141
335
  );
142
336
 
143
337
  // Copy standard static assets
144
338
  fs.copyFileSync(
145
339
  path.join(templatesDir, "cerver.png"),
146
- path.join(projectDir, "public", "cerver.png")
340
+ path.join(projectDir, "public", "cerver.png"),
147
341
  );
148
342
  fs.copyFileSync(
149
343
  path.join(templatesDir, "favicon.ico"),
150
- path.join(projectDir, "public", "favicon.ico")
344
+ path.join(projectDir, "public", "favicon.ico"),
151
345
  );
152
346
 
153
347
  // package.json
@@ -164,8 +358,8 @@ function newProject(name) {
164
358
  },
165
359
  },
166
360
  null,
167
- 2
168
- ) + "\n"
361
+ 2,
362
+ ) + "\n",
169
363
  );
170
364
 
171
365
  console.log(" Created:");