cc4-embedded-system 3.0.2 → 3.1.1

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
@@ -2,18 +2,19 @@
2
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
3
  - This tool is deployed on [npm package](https://www.npmjs.com/package/cc4-embedded-system).
4
4
 
5
- ![](./Screenshot/v3.0.1.png)
5
+ ![](./Screenshot/v3.1.1.png)
6
6
 
7
7
  ## Structure
8
8
  ```text
9
9
  CC4EmbeddedSystem/
10
10
  ├── src/
11
11
  │ ├── gui.ts // Express server & CLI entry point
12
+ │ │ ├── utils.ts // To get latest tool version
12
13
  │ └── makefsdata.ts // Core C code generation & minification logic
13
14
  ├── public/
14
15
  │ └── index.html // Web GUI dashboard
15
16
  ├── Sereenshot/
16
- │ └── v3.0.1.png // Demo image
17
+ │ └── v3.1.1.png // Demo image
17
18
  ├── node_modules/ // Required submodules during development
18
19
  ├── dist/ // Compiled JavaScript output (Auto-generated)
19
20
  ├── package.json // Project configuration & dependencies
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
64
  import { runMakeFsData } from './makefsdata.js';
66
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,15 +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')));
89
+ const cancelShutdown = () => {
90
+ if (shutdownTimer) {
91
+ clearTimeout(shutdownTimer);
92
+ shutdownTimer = null;
93
+ // console.log('🛡️ Shutdown cancelled (keep alive)');
94
+ }
95
+ };
81
96
  app.get('/api/version', (_req, res) => {
97
+ cancelShutdown();
82
98
  res.json({ version: getPackageVersion() });
83
99
  });
84
- // 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
+ });
85
113
  app.post('/api/build', async (req, res) => {
86
- // get minifyOpts
114
+ cancelShutdown();
87
115
  const { inputPath, outputPath, minifyOpts } = req.body;
116
+ console.log(`[Build] Input: ${inputPath}`);
117
+ console.log(`[Build] Output: ${outputPath}`);
88
118
  const opts = {
89
119
  inputDir: inputPath,
90
120
  outputFile: outputPath,
@@ -96,40 +126,83 @@ app.post('/api/build', async (req, res) => {
96
126
  minifyOpts: minifyOpts
97
127
  };
98
128
  try {
99
- await runMakeFsData(opts);
100
- await open(path.dirname(outputPath));
101
- 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
+ });
102
142
  }
103
143
  catch (error) {
104
144
  res.json({ success: false, message: error.message });
105
145
  }
106
146
  });
107
- app.post('/api/shutdown', (_req, res) => {
108
- console.log('👋 Window closed, shutting down ...');
109
- res.json({ success: true });
110
- setTimeout(() => { process.exit(0); }, 500);
111
- });
112
147
  app.post('/api/change-port', (req, res) => {
148
+ cancelShutdown();
113
149
  const parsedPort = parseInt(req.body.newPort, 10);
114
150
  if (isNaN(parsedPort) || parsedPort < 1 || parsedPort > 65535) {
115
151
  res.json({ success: false, message: 'Invalid port number.' });
116
152
  return;
117
153
  }
118
154
  res.json({ success: true });
119
- // restart
120
155
  setTimeout(() => {
121
156
  if (server) {
122
- server.closeAllConnections();
123
- server.close(() => {
124
- PORT = parsedPort;
125
- server = app.listen(PORT, () => {
157
+ try {
158
+ server.closeAllConnections();
159
+ server.close(() => {
160
+ PORT = parsedPort;
161
+ startServer();
126
162
  console.log(`🔄 Server successfully restarted on port ${PORT}`);
127
163
  });
128
- });
164
+ }
165
+ catch (e) {
166
+ startServer();
167
+ }
129
168
  }
130
169
  }, 100);
131
170
  });
132
- server = app.listen(PORT, async () => {
133
- console.log(`✨ GUI Server started! Opening in browser...`);
134
- 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
+ }
135
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}`);
@@ -187,9 +187,6 @@ function bufferToHexCArray(buf) {
187
187
  out += '\n';
188
188
  return out;
189
189
  }
190
- // ----------------------------------------------------------------------
191
- // 4. export API
192
- // ----------------------------------------------------------------------
193
190
  export async function runMakeFsData(opts) {
194
191
  console.log(`🚀 CC4EmbeddedSystem V3: Starting makefsdata compilation...`);
195
192
  // checkers
@@ -206,6 +203,8 @@ export async function runMakeFsData(opts) {
206
203
  fs.mkdirSync(outDir, { recursive: true });
207
204
  const allFiles = getFilesRecursive(opts.inputDir, opts.processSubs);
208
205
  const fileEntries = [];
206
+ let totalOriginalSize = 0;
207
+ let totalCompressedSize = 0;
209
208
  if (allFiles.length === 0)
210
209
  throw new Error(`Input directory is empty! Please put your web files (.html, .css, etc.) into:\n${path.resolve(opts.inputDir)}`);
211
210
  // html-minifier-next options
@@ -215,15 +214,18 @@ export async function runMakeFsData(opts) {
215
214
  const relativePath = '/' + path.relative(opts.inputDir, filePath).replace(/\\/g, '/');
216
215
  const ext = path.extname(filePath).toLowerCase();
217
216
  let content = fs.readFileSync(filePath);
217
+ const originalFileSize = content.length;
218
+ totalOriginalSize += originalFileSize;
218
219
  if (['.html', '.htm', '.css', '.js'].includes(ext)) {
219
220
  const minifiedStr = await minify(content.toString('utf8'), activeCompressOpts);
220
221
  if (typeof minifiedStr === 'string') {
221
222
  content = Buffer.from(minifiedStr, 'utf8');
222
- console.log(`📦 Minified: ${relativePath}`);
223
+ console.log(`📦 Minified: ${relativePath} (${originalFileSize} -> ${content.length} bytes)`);
223
224
  }
224
225
  }
225
226
  else
226
227
  console.log(`📄 Copied: ${relativePath}`);
228
+ totalCompressedSize += content.length;
227
229
  // generate header
228
230
  const headerData = opts.includeHttpHeader ? generateHttpHeaders(filePath, content.length, opts) : { parts: [], totalBuffer: Buffer.alloc(0) };
229
231
  const nameBuffer = Buffer.from(relativePath + '\0', 'utf8');
@@ -310,5 +312,11 @@ export async function runMakeFsData(opts) {
310
312
  cOutput += `#define FS_ROOT file_${rootNode.varName}\n#define FS_NUMFILES ${fileEntries.length + 1}\n`;
311
313
  fs.writeFileSync(opts.outputFile, cOutput);
312
314
  console.log(`\n✨ Success! Output written to: ${opts.outputFile}`);
315
+ return {
316
+ originalSize: totalOriginalSize,
317
+ compressedSize: totalCompressedSize,
318
+ filesCount: fileEntries.length
319
+ };
313
320
  }
321
+ throw new Error("No files processed.");
314
322
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc4-embedded-system",
3
- "version": "3.0.2",
3
+ "version": "3.1.1",
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
66
  import { runMakeFsData, MakeFsDataOptions } from './makefsdata.js';
68
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,18 +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
- app.get('/api/version', (_req: express.Request, res: express.Response) => {
95
+ const cancelShutdown = () => {
96
+ if (shutdownTimer) {
97
+ clearTimeout(shutdownTimer);
98
+ shutdownTimer = null;
99
+ // console.log('🛡️ Shutdown cancelled (keep alive)');
100
+ }
101
+ };
102
+
103
+ app.get('/api/version', (_req, res) => {
104
+ cancelShutdown();
88
105
  res.json({ version: getPackageVersion() });
89
106
  });
90
107
 
91
- // main
92
- app.post('/api/build', async (req: express.Request, res: express.Response) => {
93
- // 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();
94
126
  const { inputPath, outputPath, minifyOpts } = req.body;
127
+
128
+ console.log(`[Build] Input: ${inputPath}`);
129
+ console.log(`[Build] Output: ${outputPath}`);
130
+
95
131
  const opts: MakeFsDataOptions = {
96
132
  inputDir: inputPath,
97
133
  outputFile: outputPath,
@@ -104,22 +140,29 @@ app.post('/api/build', async (req: express.Request, res: express.Response) => {
104
140
  };
105
141
 
106
142
  try {
107
- await runMakeFsData(opts);
108
- await open(path.dirname(outputPath));
109
- 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
+ });
110
158
  }
111
159
  catch (error: any) {
112
160
  res.json({ success: false, message: error.message });
113
161
  }
114
162
  });
115
163
 
116
- app.post('/api/shutdown', (_req: express.Request, res: express.Response) => {
117
- console.log('👋 Window closed, shutting down ...');
118
- res.json({ success: true });
119
- setTimeout(() => { process.exit(0); }, 500);
120
- });
121
-
122
164
  app.post('/api/change-port', (req: express.Request, res: express.Response): void => {
165
+ cancelShutdown();
123
166
  const parsedPort = parseInt(req.body.newPort, 10);
124
167
 
125
168
  if (isNaN(parsedPort) || parsedPort < 1 || parsedPort > 65535) {
@@ -129,21 +172,56 @@ app.post('/api/change-port', (req: express.Request, res: express.Response): void
129
172
 
130
173
  res.json({ success: true });
131
174
 
132
- // restart
133
175
  setTimeout(() => {
134
176
  if (server) {
135
- server.closeAllConnections();
136
- server.close(() => {
137
- PORT = parsedPort;
138
- server = app.listen(PORT, () => {
177
+ try {
178
+ server.closeAllConnections();
179
+ server.close(() => {
180
+ PORT = parsedPort;
181
+ startServer();
139
182
  console.log(`🔄 Server successfully restarted on port ${PORT}`);
140
183
  });
141
- });
184
+ }
185
+ catch (e) {
186
+ startServer();
187
+ }
142
188
  }
143
189
  }, 100);
144
190
  });
145
191
 
146
- server = app.listen(PORT, async () => {
147
- console.log(`✨ GUI Server started! Opening in browser...`);
148
- await open(`http://localhost:${PORT}`);
149
- });
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
@@ -223,7 +223,14 @@ function bufferToHexCArray(buf: Buffer): string {
223
223
  // ----------------------------------------------------------------------
224
224
  // 4. export API
225
225
  // ----------------------------------------------------------------------
226
- 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> {
227
234
  console.log(`🚀 CC4EmbeddedSystem V3: Starting makefsdata compilation...`);
228
235
 
229
236
  // checkers
@@ -242,6 +249,8 @@ export async function runMakeFsData(opts: MakeFsDataOptions): Promise<void> {
242
249
 
243
250
  const allFiles = getFilesRecursive(opts.inputDir, opts.processSubs);
244
251
  const fileEntries: FileEntry[] = [];
252
+ let totalOriginalSize = 0;
253
+ let totalCompressedSize = 0;
245
254
 
246
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)}`);
247
256
 
@@ -254,16 +263,21 @@ export async function runMakeFsData(opts: MakeFsDataOptions): Promise<void> {
254
263
  const ext = path.extname(filePath).toLowerCase();
255
264
  let content = fs.readFileSync(filePath);
256
265
 
266
+ const originalFileSize = content.length;
267
+ totalOriginalSize += originalFileSize;
268
+
257
269
  if (['.html', '.htm', '.css', '.js'].includes(ext)) {
258
270
  const minifiedStr = await minify(content.toString('utf8'), activeCompressOpts);
259
271
 
260
272
  if (typeof minifiedStr === 'string') {
261
273
  content = Buffer.from(minifiedStr, 'utf8');
262
- console.log(`📦 Minified: ${relativePath}`);
274
+ console.log(`📦 Minified: ${relativePath} (${originalFileSize} -> ${content.length} bytes)`);
263
275
  }
264
276
  }
265
277
  else console.log(`📄 Copied: ${relativePath}`);
266
278
 
279
+ totalCompressedSize += content.length;
280
+
267
281
  // generate header
268
282
  const headerData = opts.includeHttpHeader ? generateHttpHeaders(filePath, content.length, opts) : { parts: [], totalBuffer: Buffer.alloc(0) };
269
283
  const nameBuffer = Buffer.from(relativePath + '\0', 'utf8');
@@ -364,5 +378,13 @@ export async function runMakeFsData(opts: MakeFsDataOptions): Promise<void> {
364
378
  cOutput += `#define FS_ROOT file_${rootNode.varName}\n#define FS_NUMFILES ${fileEntries.length + 1}\n`;
365
379
  fs.writeFileSync(opts.outputFile, cOutput);
366
380
  console.log(`\n✨ Success! Output written to: ${opts.outputFile}`);
381
+
382
+ return {
383
+ originalSize: totalOriginalSize,
384
+ compressedSize: totalCompressedSize,
385
+ filesCount: fileEntries.length
386
+ };
367
387
  }
388
+
389
+ throw new Error("No files processed.");
368
390
  }
Binary file