cc4-embedded-system 3.0.1 → 3.1.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 CHANGED
@@ -1,19 +1,25 @@
1
1
  # CC4EmbeddedSystem V3
2
- This version based on [html-minifier-next](https://github.com/j9t/html-minifier-next) and rewrite [lwIP makefsdata](https://github.com/m-labs/lwip/tree/master/src/apps/httpd/makefsdata), run on localhost, default port: ```3000```.
2
+ - This version based on [html-minifier-next](https://github.com/j9t/html-minifier-next) and rewrite [lwIP makefsdata](https://github.com/m-labs/lwip/tree/master/src/apps/httpd/makefsdata), run on localhost, default port: ```3000```.
3
+ - This tool is deployed on [npm package](https://www.npmjs.com/package/cc4-embedded-system).
3
4
 
4
- ![](./Screenshot/v3.0.0.png)
5
+ ![](./Screenshot/v3.1.0.png)
5
6
 
6
7
  ## Structure
7
8
  ```text
8
9
  CC4EmbeddedSystem/
9
10
  ├── src/
10
11
  │ ├── gui.ts // Express server & CLI entry point
12
+ │ │ ├── utils.ts // To get latest tool version
11
13
  │ └── makefsdata.ts // Core C code generation & minification logic
12
14
  ├── public/
13
15
  │ └── index.html // Web GUI dashboard
16
+ ├── Sereenshot/
17
+ │ └── v3.1.0.png // Demo image
18
+ ├── node_modules/ // Required submodules during development
14
19
  ├── dist/ // Compiled JavaScript output (Auto-generated)
15
- ├── tsconfig.json // TypeScript configuration
16
- └── package.json // Project configuration & dependencies
20
+ ├── package.json // Project configuration & dependencies
21
+ ├── package-lock.json // Project configuration & dependencies
22
+ └── tsconfig.json // TypeScript configuration
17
23
  ```
18
24
 
19
25
  ## Commands
Binary file
Binary file
package/dist/gui.js CHANGED
@@ -57,17 +57,24 @@
57
57
  * THE SOFTWARE.
58
58
  * ==============================================================================
59
59
  */
60
- // modern ESM
61
60
  import express from 'express';
62
61
  import open from 'open';
63
62
  import path from 'node:path';
64
63
  import { fileURLToPath } from 'node:url';
65
- import fs from 'node:fs';
66
64
  import { runMakeFsData } from './makefsdata.js';
65
+ import { getPackageVersion } from './utils.js';
66
+ import { execSync } from 'node:child_process';
67
+ import os from 'node:os';
68
+ // prevent socket errors
69
+ process.on('uncaughtException', (err) => {
70
+ if (err.code === 'EADDRINUSE')
71
+ return;
72
+ console.error('⚠️ Uncaught Error:', err);
73
+ });
67
74
  const __filename = fileURLToPath(import.meta.url);
68
75
  const __dirname = path.dirname(__filename);
69
76
  const app = express();
70
- // port default: 3000
77
+ // port set by command
71
78
  let PORT = parseInt(process.env.PORT || '3000', 10);
72
79
  const portArgIndex = process.argv.indexOf('--port');
73
80
  if (portArgIndex !== -1 && process.argv[portArgIndex + 1]) {
@@ -76,25 +83,38 @@ if (portArgIndex !== -1 && process.argv[portArgIndex + 1]) {
76
83
  PORT = parsedPort;
77
84
  }
78
85
  let server;
86
+ let shutdownTimer = null;
79
87
  app.use(express.json());
80
88
  app.use(express.static(path.join(__dirname, '../public')));
81
- // get current version
82
- const getPackageVersion = () => {
83
- try {
84
- const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
85
- return pkg.version || "0.0.0";
86
- }
87
- catch {
88
- return "3.0.0";
89
+ const cancelShutdown = () => {
90
+ if (shutdownTimer) {
91
+ clearTimeout(shutdownTimer);
92
+ shutdownTimer = null;
93
+ // console.log('🛡️ Shutdown cancelled (keep alive)');
89
94
  }
90
95
  };
91
96
  app.get('/api/version', (_req, res) => {
97
+ cancelShutdown();
92
98
  res.json({ version: getPackageVersion() });
93
99
  });
94
- // main
100
+ app.post('/api/shutdown', (_req, res) => {
101
+ res.json({ success: true });
102
+ if (!shutdownTimer) {
103
+ shutdownTimer = setTimeout(() => {
104
+ console.log('👋 Window closed, shutting down ...');
105
+ process.exit(0);
106
+ }, 2000);
107
+ }
108
+ });
109
+ app.post('/api/cancel-shutdown', (_req, res) => {
110
+ cancelShutdown();
111
+ res.json({ success: true });
112
+ });
95
113
  app.post('/api/build', async (req, res) => {
96
- // get minifyOpts
114
+ cancelShutdown();
97
115
  const { inputPath, outputPath, minifyOpts } = req.body;
116
+ console.log(`[Build] Input: ${inputPath}`);
117
+ console.log(`[Build] Output: ${outputPath}`);
98
118
  const opts = {
99
119
  inputDir: inputPath,
100
120
  outputFile: outputPath,
@@ -106,40 +126,83 @@ app.post('/api/build', async (req, res) => {
106
126
  minifyOpts: minifyOpts
107
127
  };
108
128
  try {
109
- await runMakeFsData(opts);
110
- await open(path.dirname(outputPath));
111
- res.json({ success: true });
129
+ // get stats
130
+ const stats = await runMakeFsData(opts);
131
+ try {
132
+ await open(path.dirname(path.resolve(outputPath)));
133
+ }
134
+ catch (e) {
135
+ // ignore open folder error
136
+ }
137
+ // return stats
138
+ res.json({
139
+ success: true,
140
+ stats: stats // originalSize, compressedSize, filesCount
141
+ });
112
142
  }
113
143
  catch (error) {
114
144
  res.json({ success: false, message: error.message });
115
145
  }
116
146
  });
117
- app.post('/api/shutdown', (_req, res) => {
118
- console.log('👋 Window closed, shutting down ...');
119
- res.json({ success: true });
120
- setTimeout(() => { process.exit(0); }, 500);
121
- });
122
147
  app.post('/api/change-port', (req, res) => {
148
+ cancelShutdown();
123
149
  const parsedPort = parseInt(req.body.newPort, 10);
124
150
  if (isNaN(parsedPort) || parsedPort < 1 || parsedPort > 65535) {
125
151
  res.json({ success: false, message: 'Invalid port number.' });
126
152
  return;
127
153
  }
128
154
  res.json({ success: true });
129
- // restart
130
155
  setTimeout(() => {
131
156
  if (server) {
132
- server.closeAllConnections();
133
- server.close(() => {
134
- PORT = parsedPort;
135
- server = app.listen(PORT, () => {
157
+ try {
158
+ server.closeAllConnections();
159
+ server.close(() => {
160
+ PORT = parsedPort;
161
+ startServer();
136
162
  console.log(`🔄 Server successfully restarted on port ${PORT}`);
137
163
  });
138
- });
164
+ }
165
+ catch (e) {
166
+ startServer();
167
+ }
139
168
  }
140
169
  }, 100);
141
170
  });
142
- server = app.listen(PORT, async () => {
143
- console.log(`✨ GUI Server started! Opening in browser...`);
144
- await open(`http://localhost:${PORT}`);
171
+ app.get('/api/browse', (req, res) => {
172
+ const isDir = req.query.type === 'dir';
173
+ const platform = os.platform();
174
+ let command = '';
175
+ try {
176
+ if (platform === 'win32') { // Windows: Powershell + OpenFileDialog
177
+ if (isDir)
178
+ command = `powershell -Command "Add-Type -AssemblyName System.Windows.Forms; $f = New-Object System.Windows.Forms.OpenFileDialog; $f.CheckFileExists = $false; $f.CheckPathExists = $true; $f.FileName = 'Select Folder'; $f.ValidateNames = $false; if($f.ShowDialog() -eq 'OK'){ Split-Path $f.FileName }"`;
179
+ else
180
+ command = `powershell -Command "Add-Type -AssemblyName System.Windows.Forms; $f = New-Object System.Windows.Forms.OpenFileDialog; $f.Filter = 'C Files (*.c)|*.c|All Files (*.*)|*.*'; if($f.ShowDialog() -eq 'OK'){ $f.FileName }"`;
181
+ }
182
+ else if (platform === 'darwin') { // macOS: darwin
183
+ if (isDir)
184
+ command = `osascript -e 'POSIX path of (choose folder with prompt "Select Input Directory")'`;
185
+ else
186
+ command = `osascript -e 'POSIX path of (choose file with prompt "Select Output File")'`;
187
+ }
188
+ else { // Linux: Zenity
189
+ if (isDir)
190
+ command = `zenity --file-selection --directory --title="Select Input Directory"`;
191
+ else
192
+ command = `zenity --file-selection --title="Select Output File"`;
193
+ }
194
+ const result = execSync(command).toString().trim();
195
+ res.json({ success: true, path: result || null });
196
+ }
197
+ catch (e) {
198
+ res.json({ success: true, path: null });
199
+ }
145
200
  });
201
+ const startServer = () => {
202
+ server = app.listen(PORT, () => {
203
+ console.log(`✨ GUI Server started on http://localhost:${PORT}`);
204
+ });
205
+ };
206
+ // main run
207
+ startServer();
208
+ open(`http://localhost:${PORT}`);
@@ -60,20 +60,12 @@
60
60
  import fs from 'node:fs';
61
61
  import path from 'node:path';
62
62
  import { minify } from 'html-minifier-next';
63
+ import { getPackageVersion } from './utils.js';
63
64
  // ----------------------------------------------------------------------
64
65
  // 1. Configs & Version Recovery
65
66
  // ----------------------------------------------------------------------
66
67
  const TCP_MSS = 1460;
67
68
  const LWIP_VERSION = "1.3.1"; // this makefsdata.ts is based on lwIP v1.3.1
68
- const getPackageVersion = () => {
69
- try {
70
- const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
71
- return pkg.version || "0.0.0";
72
- }
73
- catch {
74
- return "v3.0.0";
75
- }
76
- };
77
69
  // html-minifier-next options
78
70
  const COMPRESS_OPTS_DEFAULT = {
79
71
  collapseWhitespace: true,
@@ -195,9 +187,6 @@ function bufferToHexCArray(buf) {
195
187
  out += '\n';
196
188
  return out;
197
189
  }
198
- // ----------------------------------------------------------------------
199
- // 4. export API
200
- // ----------------------------------------------------------------------
201
190
  export async function runMakeFsData(opts) {
202
191
  console.log(`🚀 CC4EmbeddedSystem V3: Starting makefsdata compilation...`);
203
192
  // checkers
@@ -214,6 +203,8 @@ export async function runMakeFsData(opts) {
214
203
  fs.mkdirSync(outDir, { recursive: true });
215
204
  const allFiles = getFilesRecursive(opts.inputDir, opts.processSubs);
216
205
  const fileEntries = [];
206
+ let totalOriginalSize = 0;
207
+ let totalCompressedSize = 0;
217
208
  if (allFiles.length === 0)
218
209
  throw new Error(`Input directory is empty! Please put your web files (.html, .css, etc.) into:\n${path.resolve(opts.inputDir)}`);
219
210
  // html-minifier-next options
@@ -223,15 +214,18 @@ export async function runMakeFsData(opts) {
223
214
  const relativePath = '/' + path.relative(opts.inputDir, filePath).replace(/\\/g, '/');
224
215
  const ext = path.extname(filePath).toLowerCase();
225
216
  let content = fs.readFileSync(filePath);
217
+ const originalFileSize = content.length;
218
+ totalOriginalSize += originalFileSize;
226
219
  if (['.html', '.htm', '.css', '.js'].includes(ext)) {
227
220
  const minifiedStr = await minify(content.toString('utf8'), activeCompressOpts);
228
221
  if (typeof minifiedStr === 'string') {
229
222
  content = Buffer.from(minifiedStr, 'utf8');
230
- console.log(`📦 Minified: ${relativePath}`);
223
+ console.log(`📦 Minified: ${relativePath} (${originalFileSize} -> ${content.length} bytes)`);
231
224
  }
232
225
  }
233
226
  else
234
227
  console.log(`📄 Copied: ${relativePath}`);
228
+ totalCompressedSize += content.length;
235
229
  // generate header
236
230
  const headerData = opts.includeHttpHeader ? generateHttpHeaders(filePath, content.length, opts) : { parts: [], totalBuffer: Buffer.alloc(0) };
237
231
  const nameBuffer = Buffer.from(relativePath + '\0', 'utf8');
@@ -318,5 +312,11 @@ export async function runMakeFsData(opts) {
318
312
  cOutput += `#define FS_ROOT file_${rootNode.varName}\n#define FS_NUMFILES ${fileEntries.length + 1}\n`;
319
313
  fs.writeFileSync(opts.outputFile, cOutput);
320
314
  console.log(`\n✨ Success! Output written to: ${opts.outputFile}`);
315
+ return {
316
+ originalSize: totalOriginalSize,
317
+ compressedSize: totalCompressedSize,
318
+ filesCount: fileEntries.length
319
+ };
321
320
  }
321
+ throw new Error("No files processed.");
322
322
  }
package/dist/utils.js ADDED
@@ -0,0 +1,73 @@
1
+ /*
2
+ * ==============================================================================
3
+ * CC4EmbeddedSystem V3 (makefsdata)
4
+ * Copyright (c) 2026 LunaticGhoulPiano / Emile Su
5
+ * Licensed under the MIT License.
6
+ * ==============================================================================
7
+ *
8
+ * This software integrates concepts, logic, and minification processes derived
9
+ * from the following open-source projects. We deeply appreciate their work:
10
+ *
11
+ * ------------------------------------------------------------------------------
12
+ * 1. lwIP (Lightweight TCP/IP stack)
13
+ * Repository: https://github.com/m-labs/lwip
14
+ * Copyright (c) 2001-2004 Swedish Institute of Computer Science.
15
+ * All rights reserved.
16
+ * * Redistribution and use in source and binary forms, with or without modification,
17
+ * are permitted provided that the following conditions are met:
18
+ * * 1. Redistributions of source code must retain the above copyright notice,
19
+ * this list of conditions and the following disclaimer.
20
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
21
+ * this list of conditions and the following disclaimer in the documentation
22
+ * and/or other materials provided with the distribution.
23
+ * 3. The name of the author may not be used to endorse or promote products
24
+ * derived from this software without specific prior written permission.
25
+ * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
26
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
27
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
28
+ * SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
29
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
30
+ * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
31
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
32
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
33
+ * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
34
+ * OF SUCH DAMAGE.
35
+ *
36
+ * ------------------------------------------------------------------------------
37
+ * 2. html-minifier-next
38
+ * Repository: https://github.com/j9t/html-minifier-next
39
+ * Copyright (c) Jens Oliver Meiert (html-minifier-next)
40
+ * Copyright (c) Daniel Ruf (html-minifier-terser)
41
+ * Copyright (c) Juriy "kangax" Zaytsev (html-minifier)
42
+ * * Permission is hereby granted, free of charge, to any person obtaining a copy
43
+ * of this software and associated documentation files (the "Software"), to deal
44
+ * in the Software without restriction, including without limitation the rights
45
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
46
+ * copies of the Software, and to permit persons to whom the Software is
47
+ * furnished to do so, subject to the following conditions:
48
+ * * The above copyright notice and this permission notice shall be included in
49
+ * all copies or substantial portions of the Software.
50
+ * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
51
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
52
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
53
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
54
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
55
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
56
+ * THE SOFTWARE.
57
+ * ==============================================================================
58
+ */
59
+ import fs from 'node:fs';
60
+ import path from 'node:path';
61
+ import { fileURLToPath } from 'node:url';
62
+ const __filename = fileURLToPath(import.meta.url);
63
+ const __dirname = path.dirname(__filename);
64
+ export const getPackageVersion = () => {
65
+ try {
66
+ const pkgPath = path.join(__dirname, '../package.json');
67
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
68
+ return pkg.version || "LTS";
69
+ }
70
+ catch (error) {
71
+ return "LTS";
72
+ }
73
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc4-embedded-system",
3
- "version": "3.0.1",
3
+ "version": "3.1.0",
4
4
  "description": "A modern typescript version of makefsdata based on html-minifier-next and lwIP v1.3.1",
5
5
  "bin": {
6
6
  "cc4es": "./dist/gui.js"
package/public/index.html CHANGED
@@ -5,24 +5,40 @@
5
5
  <title>CC4EmbeddedSystem V3</title>
6
6
  <style>
7
7
  body { font-family: 'Consolas', monospace; background: #1e1e1e; color: #d4d4d4; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; }
8
- .container { background: #252526; padding: 30px 40px; border-radius: 8px; border: 1px solid #333; width: 500px; box-shadow: 0 4px 20px rgba(0,0,0,0.5); }
8
+ .container { background: #252526; padding: 30px 40px; border-radius: 8px; border: 1px solid #333; width: 550px; box-shadow: 0 4px 20px rgba(0,0,0,0.5); }
9
9
  h2 { color: #569cd6; margin-top: 0; display: flex; justify-content: center; align-items: center; gap: 12px; }
10
10
  .github-link { color: #d4d4d4; display: flex; align-items: center; transition: color 0.2s; text-decoration: none; }
11
11
  .github-link:hover { color: #ffffff; }
12
12
  .form-group { margin-bottom: 20px; }
13
13
  label { display: block; font-weight: bold; margin-bottom: 8px; color: #9cdcfe; }
14
- input[type="text"] { width: 100%; padding: 10px; background: #3c3c3c; border: 1px solid #555; border-radius: 4px; color: #d4d4d4; box-sizing: border-box; font-family: 'Consolas', monospace; }
14
+
15
+ /* Input */
16
+ .input-row { display: flex; gap: 8px; }
17
+ input[type="text"] { flex: 1; padding: 10px; background: #3c3c3c; border: 1px solid #555; border-radius: 4px; color: #d4d4d4; box-sizing: border-box; font-family: 'Consolas', monospace; }
15
18
  input[type="text"]:focus { outline: none; border-color: #569cd6; }
16
19
  input::placeholder { color: #888; }
20
+
21
+ /* Browse */
22
+ .btn-browse { width: auto; padding: 0 15px; background: #3a3d41; font-size: 13px; border: 1px solid #555; color: #d4d4d4; cursor: pointer; border-radius: 4px; }
23
+ .btn-browse:hover { background: #45494e; }
24
+
17
25
  button { width: 100%; padding: 12px; background: #0e639c; color: white; border: none; border-radius: 4px; font-size: 16px; font-weight: bold; cursor: pointer; transition: 0.2s; font-family: 'Consolas', monospace; }
18
26
  button:hover { background: #1177bb; }
19
27
 
20
- /* Additional settings' styles */
28
+ /* Advanced Settings */
21
29
  details { background: #1e1e1e; padding: 12px; border-radius: 4px; border: 1px solid #444; margin-bottom: 20px; }
22
30
  summary { cursor: pointer; color: #c586c0; font-weight: bold; outline: none; user-select: none; }
23
31
  .options-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 15px; }
24
32
  .checkbox-label { display: flex; align-items: center; font-weight: normal; color: #cccccc; cursor: pointer; font-size: 14px; margin-bottom: 0; }
25
33
  .checkbox-label input { margin-right: 8px; cursor: pointer; accent-color: #0e639c; }
34
+
35
+ /* Compression Stats Panel */
36
+ .stats-panel { background: #1e1e1e; border: 1px solid #444; border-radius: 4px; padding: 15px; margin-bottom: 20px; display: none; }
37
+ .stats-title { color: #4ec9b0; font-size: 14px; margin-bottom: 10px; border-bottom: 1px solid #333; padding-bottom: 5px; font-weight: bold; }
38
+ .stats-info { display: flex; justify-content: space-between; font-size: 12px; margin-bottom: 5px; }
39
+ .bar-container { background: #444; height: 18px; border-radius: 9px; overflow: hidden; margin: 10px 0; position: relative; border: 1px solid #555; }
40
+ .bar-fill { background: #0e639c; height: 100%; width: 0%; transition: width 0.6s cubic-bezier(0.1, 0.7, 1.0, 0.1); }
41
+ .bar-text { position: absolute; width: 100%; text-align: center; font-size: 10px; line-height: 18px; color: #fff; text-shadow: 1px 1px 2px #000; font-weight: bold; }
26
42
  </style>
27
43
  </head>
28
44
  <body>
@@ -39,22 +55,27 @@
39
55
 
40
56
  <div class="form-group">
41
57
  <label>Input Directory</label>
42
- <input type="text" id="inputPath" placeholder="e.g., ./input_webpages">
58
+ <div class="input-row">
59
+ <input type="text" id="inputPath" placeholder="e.g., C:\Project\web">
60
+ <button type="button" class="btn-browse" onclick="handleBrowse('inputPath', 'dir')">Brows ...</button>
61
+ </div>
43
62
  </div>
44
63
 
45
64
  <div class="form-group">
46
65
  <label>Output File (fsdata.c)</label>
47
- <input type="text" id="outputPath" placeholder="e.g., ./output_fsdata.c">
66
+ <div class="input-row">
67
+ <input type="text" id="outputPath" placeholder="e.g., ./fsdata.c">
68
+ <button type="button" class="btn-browse" onclick="handleBrowse('outputPath', 'file')">Brows ...</button>
69
+ </div>
48
70
  </div>
49
71
 
50
72
  <details>
51
73
  <summary>⚙️ Advanced Settings</summary>
52
-
53
74
  <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; padding-bottom: 12px; border-bottom: 1px solid #444;">
54
75
  <label style="margin-bottom: 0;">GUI Server Port</label>
55
76
  <div style="display: flex; gap: 8px;">
56
77
  <input type="number" id="guiPort" style="width: 80px; padding: 6px; background: #3c3c3c; border: 1px solid #555; border-radius: 4px; color: #d4d4d4; text-align: center; font-family: 'Consolas', monospace;" min="1" max="65535">
57
- <button type="button" onclick="changePort()" style="width: auto; padding: 6px 12px; font-size: 14px; background: #007acc;">Apply</button>
78
+ <button type="button" onclick="changePort()" style="width: auto; padding: 6px 12px; font-size: 14px; background: #007acc; border: none; border-radius: 4px; color: white; cursor: pointer;">Apply</button>
58
79
  </div>
59
80
  </div>
60
81
 
@@ -73,6 +94,19 @@
73
94
  </div>
74
95
  </details>
75
96
 
97
+ <div id="statsPanel" class="stats-panel">
98
+ <div class="stats-title">📊 Compression Analysis</div>
99
+ <div class="stats-info">
100
+ <span>Compressed: <span id="compSize" style="color: #9cdcfe;">0</span> KB</span>
101
+ <span>Original: <span id="origSize" style="color: #ce9178;">0</span> KB</span>
102
+ </div>
103
+ <div class="bar-container">
104
+ <div id="barFill" class="bar-fill"></div>
105
+ <div id="barText" class="bar-text">0% Saved</div>
106
+ </div>
107
+ <div id="savingsInfo" style="font-size: 11px; color: #6a9955; text-align: center;"></div>
108
+ </div>
109
+
76
110
  <button onclick="startBuild()">Generate C Code</button>
77
111
  </div>
78
112
 
@@ -80,53 +114,51 @@
80
114
  let isRedirecting = false;
81
115
  document.getElementById('guiPort').value = window.location.port || '80';
82
116
 
83
- // get package.json version
117
+ async function handleBrowse(targetId, type) {
118
+ try {
119
+ const res = await fetch(`/api/browse?type=${type}`);
120
+ const data = await res.json();
121
+ if (data.success && data.path) {
122
+ document.getElementById(targetId).value = data.path;
123
+ }
124
+ }
125
+ catch (e) {
126
+ console.error('Failed to open system dialog', e);
127
+ }
128
+ }
129
+
84
130
  window.addEventListener('DOMContentLoaded', async () => {
131
+ await fetch('/api/cancel-shutdown', { method: 'POST' });
85
132
  try {
86
133
  const res = await fetch('/api/version');
87
134
  const data = await res.json();
88
135
  if (data.version) document.getElementById('app-title-text').innerText = `CC4EmbeddedSystem V3 (${data.version})`;
89
136
  }
90
- catch (error) {
91
- console.error('Can\'t get package.json version!', error);
92
- }
137
+ catch (error) { console.error('Can\'t get version!', error); }
93
138
  });
94
139
 
95
140
  async function changePort() {
96
141
  const newPort = document.getElementById('guiPort').value.trim();
97
- const currentPort = window.location.port || '80';
98
-
99
- if (!newPort || newPort === currentPort) return;
100
-
142
+ if (! newPort) return;
143
+
101
144
  const res = await fetch('/api/change-port', {
102
145
  method: 'POST',
103
146
  headers: { 'Content-Type': 'application/json' },
104
147
  body: JSON.stringify({ newPort })
105
148
  });
106
-
149
+
107
150
  const result = await res.json();
108
151
  if (result.success) {
109
- isRedirecting = true;
110
-
111
- // 0.5s restart server and redirect
112
- setTimeout(() => {
113
- window.location.href = `${window.location.protocol}//${window.location.hostname}:${newPort}`;
114
- }, 500);
152
+ isRedirecting = true;
153
+ setTimeout(() => { window.location.href = `${window.location.protocol}//${window.location.hostname}:${newPort}`; }, 500);
115
154
  }
116
- else alert('❌ Failed to change port: \n' + result.message);
117
155
  }
118
156
 
119
- // run
120
157
  async function startBuild() {
121
158
  const inPath = document.getElementById('inputPath').value.trim();
122
159
  const outPath = document.getElementById('outputPath').value.trim();
160
+ if (! inPath || ! outPath) { alert('❌ Paths required.'); return; }
123
161
 
124
- if (! inPath || ! outPath) {
125
- alert('❌ Compile Error: \nBoth Input Directory and Output File paths are required.');
126
- return;
127
- }
128
-
129
- // get all checkboxes
130
162
  const minifyOptions = {
131
163
  collapseWhitespace: document.getElementById('opt-collapseWhitespace').checked,
132
164
  removeComments: document.getElementById('opt-removeComments').checked,
@@ -143,24 +175,46 @@
143
175
  const res = await fetch('/api/build', {
144
176
  method: 'POST',
145
177
  headers: { 'Content-Type': 'application/json' },
146
- body: JSON.stringify({
147
- inputPath: inPath,
148
- outputPath: outPath,
149
- minifyOpts: minifyOptions
150
- })
178
+ body: JSON.stringify({ inputPath: inPath, outputPath: outPath, minifyOpts: minifyOptions })
151
179
  });
152
-
153
180
  const result = await res.json();
154
181
 
155
- if (result.success) alert('✨ Success: \nC Array successfully generated at ' + outPath);
156
- else alert('❌ Compile Error: \n' + result.message);
182
+ if (result.success) {
183
+ // visualize
184
+ if (result.stats) {
185
+ const panel = document.getElementById('statsPanel');
186
+ panel.style.display = 'block';
187
+
188
+ const { originalSize, compressedSize } = result.stats;
189
+ const origKB = (originalSize / 1024).toFixed(2);
190
+ const compKB = (compressedSize / 1024).toFixed(2);
191
+
192
+ // ratio: the saved percentage
193
+ const ratio = originalSize > 0 ? (((originalSize - compressedSize) / originalSize) * 100).toFixed(1) : 0;
194
+ // compressedRatio: the remainings percentage
195
+ const compressedRatio = (100 - ratio).toFixed(1);
196
+
197
+ document.getElementById('origSize').innerText = origKB;
198
+ document.getElementById('compSize').innerText = compKB;
199
+ document.getElementById('barText').innerText = `${ratio}% Size Reduced`;
200
+
201
+ // actual used percentage
202
+ setTimeout(() => {
203
+ document.getElementById('barFill').style.width = compressedRatio + '%';
204
+ }, 50);
205
+
206
+ document.getElementById('savingsInfo').innerText = `✨ Total saved: ${(origKB - compKB).toFixed(2)} KB`;
207
+ }
208
+
209
+ alert('✨ Success! C Code generated at ' + outPath);
210
+ } else {
211
+ alert('❌ Error: ' + result.message);
212
+ }
157
213
  }
158
214
 
159
- // listen close window
160
215
  window.addEventListener('beforeunload', () => {
161
216
  if (! isRedirecting) navigator.sendBeacon('/api/shutdown');
162
217
  });
163
218
  </script>
164
-
165
219
  </body>
166
220
  </html>
package/src/gui.ts CHANGED
@@ -59,19 +59,26 @@
59
59
  * ==============================================================================
60
60
  */
61
61
 
62
- // modern ESM
63
62
  import express from 'express';
64
63
  import open from 'open';
65
64
  import path from 'node:path';
66
65
  import { fileURLToPath } from 'node:url';
67
- import fs from 'node:fs';
68
66
  import { runMakeFsData, MakeFsDataOptions } from './makefsdata.js';
67
+ import { getPackageVersion } from './utils.js';
68
+ import { execSync } from 'node:child_process';
69
+ import os from 'node:os';
70
+
71
+ // prevent socket errors
72
+ process.on('uncaughtException', (err: any) => {
73
+ if (err.code === 'EADDRINUSE') return;
74
+ console.error('⚠️ Uncaught Error:', err);
75
+ });
69
76
 
70
77
  const __filename: string = fileURLToPath(import.meta.url);
71
78
  const __dirname: string = path.dirname(__filename);
72
79
  const app = express();
73
80
 
74
- // port default: 3000
81
+ // port set by command
75
82
  let PORT: number = parseInt(process.env.PORT || '3000', 10);
76
83
  const portArgIndex = process.argv.indexOf('--port');
77
84
  if (portArgIndex !== -1 && process.argv[portArgIndex + 1]) {
@@ -80,29 +87,47 @@ if (portArgIndex !== -1 && process.argv[portArgIndex + 1]) {
80
87
  }
81
88
 
82
89
  let server: any;
90
+ let shutdownTimer: NodeJS.Timeout | null = null;
83
91
 
84
92
  app.use(express.json());
85
93
  app.use(express.static(path.join(__dirname, '../public')));
86
94
 
87
- // get current version
88
- const getPackageVersion = (): string => {
89
- try {
90
- const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
91
- return pkg.version || "0.0.0";
92
- }
93
- catch {
94
- return "3.0.0";
95
+ const cancelShutdown = () => {
96
+ if (shutdownTimer) {
97
+ clearTimeout(shutdownTimer);
98
+ shutdownTimer = null;
99
+ // console.log('🛡️ Shutdown cancelled (keep alive)');
95
100
  }
96
101
  };
97
102
 
98
- app.get('/api/version', (_req: express.Request, res: express.Response) => {
103
+ app.get('/api/version', (_req, res) => {
104
+ cancelShutdown();
99
105
  res.json({ version: getPackageVersion() });
100
106
  });
101
107
 
102
- // main
103
- app.post('/api/build', async (req: express.Request, res: express.Response) => {
104
- // get minifyOpts
108
+ app.post('/api/shutdown', (_req, res) => {
109
+ res.json({ success: true });
110
+
111
+ if (! shutdownTimer) {
112
+ shutdownTimer = setTimeout(() => {
113
+ console.log('👋 Window closed, shutting down ...');
114
+ process.exit(0);
115
+ }, 2000);
116
+ }
117
+ });
118
+
119
+ app.post('/api/cancel-shutdown', (_req, res) => {
120
+ cancelShutdown();
121
+ res.json({ success: true });
122
+ });
123
+
124
+ app.post('/api/build', async (req: express.Request, res: express.Response): Promise<void> => {
125
+ cancelShutdown();
105
126
  const { inputPath, outputPath, minifyOpts } = req.body;
127
+
128
+ console.log(`[Build] Input: ${inputPath}`);
129
+ console.log(`[Build] Output: ${outputPath}`);
130
+
106
131
  const opts: MakeFsDataOptions = {
107
132
  inputDir: inputPath,
108
133
  outputFile: outputPath,
@@ -115,22 +140,29 @@ app.post('/api/build', async (req: express.Request, res: express.Response) => {
115
140
  };
116
141
 
117
142
  try {
118
- await runMakeFsData(opts);
119
- await open(path.dirname(outputPath));
120
- res.json({ success: true });
143
+ // get stats
144
+ const stats = await runMakeFsData(opts);
145
+
146
+ try {
147
+ await open(path.dirname(path.resolve(outputPath)));
148
+ }
149
+ catch (e) {
150
+ // ignore open folder error
151
+ }
152
+
153
+ // return stats
154
+ res.json({
155
+ success: true,
156
+ stats: stats // originalSize, compressedSize, filesCount
157
+ });
121
158
  }
122
159
  catch (error: any) {
123
160
  res.json({ success: false, message: error.message });
124
161
  }
125
162
  });
126
163
 
127
- app.post('/api/shutdown', (_req: express.Request, res: express.Response) => {
128
- console.log('👋 Window closed, shutting down ...');
129
- res.json({ success: true });
130
- setTimeout(() => { process.exit(0); }, 500);
131
- });
132
-
133
164
  app.post('/api/change-port', (req: express.Request, res: express.Response): void => {
165
+ cancelShutdown();
134
166
  const parsedPort = parseInt(req.body.newPort, 10);
135
167
 
136
168
  if (isNaN(parsedPort) || parsedPort < 1 || parsedPort > 65535) {
@@ -140,21 +172,56 @@ app.post('/api/change-port', (req: express.Request, res: express.Response): void
140
172
 
141
173
  res.json({ success: true });
142
174
 
143
- // restart
144
175
  setTimeout(() => {
145
176
  if (server) {
146
- server.closeAllConnections();
147
- server.close(() => {
148
- PORT = parsedPort;
149
- server = app.listen(PORT, () => {
177
+ try {
178
+ server.closeAllConnections();
179
+ server.close(() => {
180
+ PORT = parsedPort;
181
+ startServer();
150
182
  console.log(`🔄 Server successfully restarted on port ${PORT}`);
151
183
  });
152
- });
184
+ }
185
+ catch (e) {
186
+ startServer();
187
+ }
153
188
  }
154
189
  }, 100);
155
190
  });
156
191
 
157
- server = app.listen(PORT, async () => {
158
- console.log(`✨ GUI Server started! Opening in browser...`);
159
- await open(`http://localhost:${PORT}`);
160
- });
192
+ app.get('/api/browse', (req, res) => {
193
+ const isDir = req.query.type === 'dir';
194
+ const platform = os.platform();
195
+ let command = '';
196
+
197
+ try {
198
+ if (platform === 'win32') { // Windows: Powershell + OpenFileDialog
199
+ if (isDir) command = `powershell -Command "Add-Type -AssemblyName System.Windows.Forms; $f = New-Object System.Windows.Forms.OpenFileDialog; $f.CheckFileExists = $false; $f.CheckPathExists = $true; $f.FileName = 'Select Folder'; $f.ValidateNames = $false; if($f.ShowDialog() -eq 'OK'){ Split-Path $f.FileName }"`;
200
+ else command = `powershell -Command "Add-Type -AssemblyName System.Windows.Forms; $f = New-Object System.Windows.Forms.OpenFileDialog; $f.Filter = 'C Files (*.c)|*.c|All Files (*.*)|*.*'; if($f.ShowDialog() -eq 'OK'){ $f.FileName }"`;
201
+ }
202
+ else if (platform === 'darwin') { // macOS: darwin
203
+ if (isDir) command = `osascript -e 'POSIX path of (choose folder with prompt "Select Input Directory")'`;
204
+ else command = `osascript -e 'POSIX path of (choose file with prompt "Select Output File")'`;
205
+ }
206
+ else { // Linux: Zenity
207
+ if (isDir) command = `zenity --file-selection --directory --title="Select Input Directory"`;
208
+ else command = `zenity --file-selection --title="Select Output File"`;
209
+ }
210
+
211
+ const result = execSync(command).toString().trim();
212
+ res.json({ success: true, path: result || null });
213
+ }
214
+ catch (e) {
215
+ res.json({ success: true, path: null });
216
+ }
217
+ });
218
+
219
+ const startServer = () => {
220
+ server = app.listen(PORT, () => {
221
+ console.log(`✨ GUI Server started on http://localhost:${PORT}`);
222
+ });
223
+ };
224
+
225
+ // main run
226
+ startServer();
227
+ open(`http://localhost:${PORT}`);
package/src/makefsdata.ts CHANGED
@@ -61,6 +61,7 @@
61
61
  import fs from 'node:fs';
62
62
  import path from 'node:path';
63
63
  import { minify } from 'html-minifier-next';
64
+ import { getPackageVersion } from './utils.js';
64
65
 
65
66
  // ----------------------------------------------------------------------
66
67
  // 1. Configs & Version Recovery
@@ -68,16 +69,6 @@ import { minify } from 'html-minifier-next';
68
69
  const TCP_MSS: number = 1460;
69
70
  const LWIP_VERSION: string = "1.3.1"; // this makefsdata.ts is based on lwIP v1.3.1
70
71
 
71
- const getPackageVersion = (): string => {
72
- try {
73
- const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
74
- return pkg.version || "0.0.0";
75
- }
76
- catch {
77
- return "v3.0.0";
78
- }
79
- };
80
-
81
72
  // html-minifier-next options
82
73
  const COMPRESS_OPTS_DEFAULT = {
83
74
  collapseWhitespace: true,
@@ -232,7 +223,14 @@ function bufferToHexCArray(buf: Buffer): string {
232
223
  // ----------------------------------------------------------------------
233
224
  // 4. export API
234
225
  // ----------------------------------------------------------------------
235
- export async function runMakeFsData(opts: MakeFsDataOptions): Promise<void> {
226
+
227
+ export interface BuildStats {
228
+ originalSize: number;
229
+ compressedSize: number;
230
+ filesCount: number;
231
+ }
232
+
233
+ export async function runMakeFsData(opts: MakeFsDataOptions): Promise<BuildStats> {
236
234
  console.log(`🚀 CC4EmbeddedSystem V3: Starting makefsdata compilation...`);
237
235
 
238
236
  // checkers
@@ -251,6 +249,8 @@ export async function runMakeFsData(opts: MakeFsDataOptions): Promise<void> {
251
249
 
252
250
  const allFiles = getFilesRecursive(opts.inputDir, opts.processSubs);
253
251
  const fileEntries: FileEntry[] = [];
252
+ let totalOriginalSize = 0;
253
+ let totalCompressedSize = 0;
254
254
 
255
255
  if (allFiles.length === 0) throw new Error(`Input directory is empty! Please put your web files (.html, .css, etc.) into:\n${path.resolve(opts.inputDir)}`);
256
256
 
@@ -263,16 +263,21 @@ export async function runMakeFsData(opts: MakeFsDataOptions): Promise<void> {
263
263
  const ext = path.extname(filePath).toLowerCase();
264
264
  let content = fs.readFileSync(filePath);
265
265
 
266
+ const originalFileSize = content.length;
267
+ totalOriginalSize += originalFileSize;
268
+
266
269
  if (['.html', '.htm', '.css', '.js'].includes(ext)) {
267
270
  const minifiedStr = await minify(content.toString('utf8'), activeCompressOpts);
268
271
 
269
272
  if (typeof minifiedStr === 'string') {
270
273
  content = Buffer.from(minifiedStr, 'utf8');
271
- console.log(`📦 Minified: ${relativePath}`);
274
+ console.log(`📦 Minified: ${relativePath} (${originalFileSize} -> ${content.length} bytes)`);
272
275
  }
273
276
  }
274
277
  else console.log(`📄 Copied: ${relativePath}`);
275
278
 
279
+ totalCompressedSize += content.length;
280
+
276
281
  // generate header
277
282
  const headerData = opts.includeHttpHeader ? generateHttpHeaders(filePath, content.length, opts) : { parts: [], totalBuffer: Buffer.alloc(0) };
278
283
  const nameBuffer = Buffer.from(relativePath + '\0', 'utf8');
@@ -373,5 +378,13 @@ export async function runMakeFsData(opts: MakeFsDataOptions): Promise<void> {
373
378
  cOutput += `#define FS_ROOT file_${rootNode.varName}\n#define FS_NUMFILES ${fileEntries.length + 1}\n`;
374
379
  fs.writeFileSync(opts.outputFile, cOutput);
375
380
  console.log(`\n✨ Success! Output written to: ${opts.outputFile}`);
381
+
382
+ return {
383
+ originalSize: totalOriginalSize,
384
+ compressedSize: totalCompressedSize,
385
+ filesCount: fileEntries.length
386
+ };
376
387
  }
388
+
389
+ throw new Error("No files processed.");
377
390
  }
package/src/utils.ts ADDED
@@ -0,0 +1,76 @@
1
+ /*
2
+ * ==============================================================================
3
+ * CC4EmbeddedSystem V3 (makefsdata)
4
+ * Copyright (c) 2026 LunaticGhoulPiano / Emile Su
5
+ * Licensed under the MIT License.
6
+ * ==============================================================================
7
+ *
8
+ * This software integrates concepts, logic, and minification processes derived
9
+ * from the following open-source projects. We deeply appreciate their work:
10
+ *
11
+ * ------------------------------------------------------------------------------
12
+ * 1. lwIP (Lightweight TCP/IP stack)
13
+ * Repository: https://github.com/m-labs/lwip
14
+ * Copyright (c) 2001-2004 Swedish Institute of Computer Science.
15
+ * All rights reserved.
16
+ * * Redistribution and use in source and binary forms, with or without modification,
17
+ * are permitted provided that the following conditions are met:
18
+ * * 1. Redistributions of source code must retain the above copyright notice,
19
+ * this list of conditions and the following disclaimer.
20
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
21
+ * this list of conditions and the following disclaimer in the documentation
22
+ * and/or other materials provided with the distribution.
23
+ * 3. The name of the author may not be used to endorse or promote products
24
+ * derived from this software without specific prior written permission.
25
+ * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
26
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
27
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
28
+ * SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
29
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
30
+ * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
31
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
32
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
33
+ * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
34
+ * OF SUCH DAMAGE.
35
+ *
36
+ * ------------------------------------------------------------------------------
37
+ * 2. html-minifier-next
38
+ * Repository: https://github.com/j9t/html-minifier-next
39
+ * Copyright (c) Jens Oliver Meiert (html-minifier-next)
40
+ * Copyright (c) Daniel Ruf (html-minifier-terser)
41
+ * Copyright (c) Juriy "kangax" Zaytsev (html-minifier)
42
+ * * Permission is hereby granted, free of charge, to any person obtaining a copy
43
+ * of this software and associated documentation files (the "Software"), to deal
44
+ * in the Software without restriction, including without limitation the rights
45
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
46
+ * copies of the Software, and to permit persons to whom the Software is
47
+ * furnished to do so, subject to the following conditions:
48
+ * * The above copyright notice and this permission notice shall be included in
49
+ * all copies or substantial portions of the Software.
50
+ * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
51
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
52
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
53
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
54
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
55
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
56
+ * THE SOFTWARE.
57
+ * ==============================================================================
58
+ */
59
+
60
+ import fs from 'node:fs';
61
+ import path from 'node:path';
62
+ import { fileURLToPath } from 'node:url';
63
+
64
+ const __filename = fileURLToPath(import.meta.url);
65
+ const __dirname = path.dirname(__filename);
66
+
67
+ export const getPackageVersion = (): string => {
68
+ try {
69
+ const pkgPath = path.join(__dirname, '../package.json');
70
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf8'));
71
+ return pkg.version || "LTS";
72
+ }
73
+ catch (error) {
74
+ return "LTS";
75
+ }
76
+ };
Binary file