afront 1.0.26 → 1.0.27

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.
Files changed (2) hide show
  1. package/install.js +235 -56
  2. package/package.json +1 -1
package/install.js CHANGED
@@ -7,13 +7,46 @@ const os = require("os");
7
7
  const AdmZip = require("adm-zip");
8
8
  const readline = require("readline");
9
9
  const chalk = require("chalk");
10
+ const semver = require("semver");
11
+ const cliProgress = require("cli-progress");
12
+ const packageJson = require("./package.json");
10
13
 
11
14
  const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "afront-"));
12
- const VERSION = "1.0.26";
15
+ const VERSION = packageJson.version;
16
+ const MIN_NODE_VERSION = "20.18.1";
17
+
18
+ if (!semver.gte(process.version, MIN_NODE_VERSION)) {
19
+ console.error(
20
+ chalk.red(
21
+ `\n✖ AFront requires Node.js ${MIN_NODE_VERSION} or higher.\nCurrent: ${process.version}\n`,
22
+ ),
23
+ );
24
+ process.exit(1);
25
+ }
26
+
27
+ process.on("SIGINT", () => {
28
+ console.log(chalk.yellow("\n\nInstallation cancelled."));
29
+ rl.close();
30
+ cleanup();
31
+ process.exit(1);
32
+ });
33
+
34
+ process.on("uncaughtException", (err) => {
35
+ console.error(chalk.red("\nUnexpected error:\n"), err);
36
+ rl.close();
37
+ cleanup();
38
+ process.exit(1);
39
+ });
40
+
41
+ process.on("unhandledRejection", (err) => {
42
+ console.error(chalk.red("\nUnhandled promise rejection:\n"), err);
43
+ rl.close();
44
+ cleanup();
45
+ process.exit(1);
46
+ });
13
47
 
14
48
  // Configuration
15
- const GITHUB_ZIP_URL =
16
- "https://github.com/Asggen/afront/archive/refs/tags/v1.0.26.zip"; // Updated URL
49
+ const GITHUB_ZIP_URL = `https://codeload.github.com/Asggen/afront/zip/refs/tags/v${VERSION}`;
17
50
 
18
51
  // Define files to skip
19
52
  const SKIP_FILES = [
@@ -30,14 +63,30 @@ const rl = readline.createInterface({
30
63
  output: process.stdout,
31
64
  });
32
65
 
66
+ const cleanup = () => {
67
+ try {
68
+ if (fs.existsSync(tmpDir)) {
69
+ fs.rmSync(tmpDir, { recursive: true, force: true });
70
+ }
71
+ } catch (err) {
72
+ console.error("Error cleaning up temporary directory:", err);
73
+ }
74
+ };
75
+
33
76
  // Spinner function
34
77
  const spinner = (text, delay = 100) => {
78
+ if (!process.stdout.isTTY) {
79
+ console.log(text);
80
+ return () => {};
81
+ }
82
+
35
83
  const spinnerChars = ["|", "/", "-", "\\"];
36
84
  let i = 0;
85
+
37
86
  const interval = setInterval(() => {
38
87
  readline.cursorTo(process.stdout, 0);
39
88
  process.stdout.write(`${text} ${spinnerChars[i++]}`);
40
- i = i % spinnerChars.length;
89
+ i %= spinnerChars.length;
41
90
  }, delay);
42
91
 
43
92
  return () => {
@@ -58,43 +107,103 @@ const askQuestion = (question) => {
58
107
  const downloadFile = (url, destination) => {
59
108
  return new Promise((resolve, reject) => {
60
109
  const file = fs.createWriteStream(destination);
61
- const stopSpinner = spinner(chalk.cyan(" Downloading template..."));
62
-
63
- const request = https.get(url, (response) => {
64
- if (
65
- response.statusCode >= 300 &&
66
- response.statusCode < 400 &&
67
- response.headers.location
68
- ) {
69
- return downloadFile(response.headers.location, destination)
70
- .then(resolve)
71
- .catch(reject);
72
- }
110
+ file.on("error", (err) => {
111
+ reject(err);
112
+ });
73
113
 
74
- if (response.statusCode !== 200) {
75
- stopSpinner();
76
- return reject(
77
- new Error(
78
- `Failed to download file. Status code: ${response.statusCode}`,
79
- ),
80
- );
81
- }
114
+ https
115
+ .get(url, (response) => {
116
+ if (
117
+ response.statusCode >= 300 &&
118
+ response.statusCode < 400 &&
119
+ response.headers.location
120
+ ) {
121
+ return downloadFile(response.headers.location, destination)
122
+ .then(resolve)
123
+ .catch(reject);
124
+ }
82
125
 
83
- response.pipe(file);
126
+ if (response.statusCode !== 200) {
127
+ return reject(
128
+ new Error(
129
+ `Failed to download file. Status code: ${response.statusCode}`,
130
+ ),
131
+ );
132
+ }
84
133
 
85
- file.on("finish", () => {
86
- file.close(() => {
87
- stopSpinner();
88
- resolve();
134
+ const totalHeader = response.headers["content-length"];
135
+ const total = totalHeader ? Number(totalHeader) : null;
136
+
137
+ let bar;
138
+ let downloaded = 0;
139
+
140
+ const useTTY = process.stdout.isTTY;
141
+
142
+ const formatBytes = (bytes) => {
143
+ if (bytes < 1024) return bytes + " B";
144
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
145
+ return (bytes / (1024 * 1024)).toFixed(1) + " MB";
146
+ };
147
+
148
+ if (useTTY && total) {
149
+ let downloadedBytes = 0;
150
+ const totalFormatted = formatBytes(total);
151
+
152
+ bar = new cliProgress.SingleBar({
153
+ format: (options, params) => {
154
+ const percentage = Math.floor(
155
+ (params.value / params.total) * 100,
156
+ );
157
+
158
+ const termWidth = process.stdout.columns || 80;
159
+ const barLength = Math.max(10, Math.min(termWidth - 60, 30));
160
+ const filledLength = Math.round(barLength * params.progress);
161
+
162
+ const barVisual =
163
+ "█".repeat(filledLength) + "░".repeat(barLength - filledLength);
164
+
165
+ const downloadedFormatted = formatBytes(params.value);
166
+ const totalFormatted = formatBytes(params.total);
167
+
168
+ return (
169
+ chalk.cyan("⬇ Downloading template ") +
170
+ chalk.green(barVisual) +
171
+ chalk.white(` ${percentage}%`) +
172
+ chalk.gray(` (${downloadedFormatted} / ${totalFormatted})`)
173
+ );
174
+ },
175
+ hideCursor: true,
176
+ clearOnComplete: false,
177
+ });
178
+
179
+ bar.start(total, 0);
180
+
181
+ response.on("data", (chunk) => {
182
+ downloadedBytes += chunk.length;
183
+ bar.update(downloadedBytes);
184
+ });
185
+ } else {
186
+ console.log(chalk.cyan("⬇ Downloading template..."));
187
+ }
188
+
189
+ response.pipe(file);
190
+
191
+ file.on("finish", () => {
192
+ file.close(() => {
193
+ if (bar) {
194
+ bar.stop();
195
+ const totalFormatted = formatBytes(total);
196
+ console.log(
197
+ chalk.green(`✔ Downloaded template (${totalFormatted})`),
198
+ );
199
+ }
200
+ resolve();
201
+ });
89
202
  });
203
+ })
204
+ .on("error", (err) => {
205
+ reject(err);
90
206
  });
91
- });
92
-
93
- request.on("error", (err) => {
94
- stopSpinner();
95
- fs.promises.unlink(destination).catch(() => {});
96
- reject(err);
97
- });
98
207
  });
99
208
  };
100
209
 
@@ -124,42 +233,91 @@ const extractZip = (zipPath, extractTo) => {
124
233
 
125
234
  const runNpmInstall = (directory) => {
126
235
  return new Promise((resolve, reject) => {
127
- const stopSpinner = spinner(chalk.magenta("📦 Installing dependencies (this may take a few seconds)..."));
236
+ const isWindows = process.platform === "win32";
237
+
238
+ // ✅ Non-interactive (CI / Docker / GitHub Actions)
239
+ if (!process.stdout.isTTY) {
240
+ const child = spawn(
241
+ isWindows ? "npm.cmd" : "npm",
242
+ ["install", "--legacy-peer-deps"],
243
+ {
244
+ cwd: directory,
245
+ stdio: "inherit",
246
+ shell: isWindows,
247
+ },
248
+ );
249
+
250
+ child.on("close", (code) =>
251
+ code === 0 ? resolve() : reject(new Error("npm install failed")),
252
+ );
253
+
254
+ child.on("error", reject);
255
+ return;
256
+ }
257
+
258
+ // ✅ Interactive terminal (with progress bar)
259
+ console.log();
260
+
261
+ const bar = new cliProgress.SingleBar(
262
+ {
263
+ format:
264
+ chalk.magenta("📦 Installing dependencies ") +
265
+ chalk.green("{bar}") +
266
+ chalk.white(" {percentage}%"),
267
+ barCompleteChar: "█",
268
+ barIncompleteChar: "░",
269
+ hideCursor: true,
270
+ clearOnComplete: false,
271
+ },
272
+ cliProgress.Presets.shades_classic,
273
+ );
274
+
275
+ bar.start(100, 0);
276
+
277
+ let progress = 0;
278
+
279
+ // Smooth simulated progress
280
+ const interval = setInterval(() => {
281
+ if (progress < 90) {
282
+ progress += Math.random() * 5;
283
+ bar.update(Math.floor(progress));
284
+ }
285
+ }, 200);
128
286
 
129
287
  const child = spawn(
130
- process.platform === "win32" ? "npm.cmd" : "npm",
288
+ isWindows ? "npm.cmd" : "npm",
131
289
  [
132
290
  "install",
133
291
  "--legacy-peer-deps",
134
292
  "--no-audit",
135
293
  "--no-fund",
294
+ "--no-progress",
136
295
  "--loglevel=error",
137
296
  ],
138
297
  {
139
298
  cwd: directory,
140
- stdio: "pipe",
141
- shell: false,
299
+ stdio: "ignore",
300
+ shell: isWindows,
142
301
  },
143
302
  );
144
303
 
145
- let errorOutput = "";
146
-
147
- child.stderr.on("data", (data) => {
148
- errorOutput += data.toString();
149
- });
150
-
151
304
  child.on("close", (code) => {
152
- stopSpinner();
305
+ clearInterval(interval);
306
+
153
307
  if (code === 0) {
308
+ bar.update(100);
309
+ bar.stop();
310
+ console.log();
154
311
  resolve();
155
312
  } else {
156
- console.error(chalk.red(errorOutput));
157
- reject(new Error(`npm install failed with code ${code}`));
313
+ bar.stop();
314
+ reject(new Error(`npm exited with code ${code}`));
158
315
  }
159
316
  });
160
317
 
161
318
  child.on("error", (err) => {
162
- stopSpinner();
319
+ clearInterval(interval);
320
+ bar.stop();
163
321
  reject(err);
164
322
  });
165
323
  });
@@ -394,6 +552,14 @@ const main = async () => {
394
552
 
395
553
  folderName = sanitizeFolderName(folderName);
396
554
 
555
+ if (folderName === path.basename(process.cwd())) {
556
+ console.log(
557
+ chalk.yellow(
558
+ "\nYou are creating a project inside the current directory.\n",
559
+ ),
560
+ );
561
+ }
562
+
397
563
  console.log(
398
564
  chalk.green(`Creating project in ${chalk.bold(`./${folderName}`)}\n`),
399
565
  );
@@ -422,12 +588,14 @@ const main = async () => {
422
588
 
423
589
  const extractedFolderName = extractedItems.find((item) => {
424
590
  const fullPath = path.join(tmpDir, item);
425
- return fs.lstatSync(fullPath).isDirectory();
591
+
592
+ return fs.lstatSync(fullPath).isDirectory() && item.startsWith("afront-");
426
593
  });
427
594
 
428
595
  if (!extractedFolderName) {
429
- throw new Error("Extraction failed: no directory found in archive.");
596
+ throw new Error("Extraction failed: extracted folder not found.");
430
597
  }
598
+
431
599
  const extractedFolderPath = path.join(tmpDir, extractedFolderName);
432
600
 
433
601
  await moveFiles(extractedFolderPath, destDir, tmpDir);
@@ -443,6 +611,10 @@ const main = async () => {
443
611
  // ignore
444
612
  }
445
613
 
614
+ if (!fs.existsSync(path.join(destDir, "package.json"))) {
615
+ throw new Error("package.json not found after extraction.");
616
+ }
617
+
446
618
  await runNpmInstall(destDir);
447
619
 
448
620
  console.log(chalk.green("\n✔ AFront project created successfully!\n"));
@@ -454,8 +626,7 @@ const main = async () => {
454
626
  console.log(chalk.gray("Happy building with AFront 🚀"));
455
627
 
456
628
  rl.close();
457
-
458
- fs.rmSync(tmpDir, { recursive: true, force: true });
629
+ cleanup();
459
630
  process.exit(0);
460
631
  } catch (err) {
461
632
  console.error(chalk.red("✖ Error:"), err.message);
@@ -514,5 +685,13 @@ const showBanner = () => {
514
685
  console.log(chalk.gray("🚀 https://afront.dev\n"));
515
686
  };
516
687
 
517
- showBanner();
518
- main();
688
+ (async () => {
689
+ try {
690
+ showBanner();
691
+ await main();
692
+ } catch (err) {
693
+ console.error(chalk.red("✖ Fatal Error:"), err.message);
694
+ cleanup();
695
+ process.exit(1);
696
+ }
697
+ })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "afront",
3
- "version": "1.0.26",
3
+ "version": "1.0.27",
4
4
  "description": "AFront is a front-end JavaScript library designed to create seamless server-side rendered (SSSR) websites.",
5
5
  "main": "webpack.dev.js",
6
6
  "scripts": {