cc4-embedded-system 3.0.2 → 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 +3 -2
- package/Screenshot/v3.1.0.png +0 -0
- package/dist/gui.js +94 -21
- package/dist/makefsdata.js +12 -4
- package/package.json +1 -1
- package/public/index.html +94 -40
- package/src/gui.ts +103 -25
- package/src/makefsdata.ts +24 -2
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
|
-

|
|
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.
|
|
17
|
+
│ └── v3.1.0.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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
100
|
-
await
|
|
101
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
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}`);
|
package/dist/makefsdata.js
CHANGED
|
@@ -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
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:
|
|
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
|
-
|
|
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
|
-
/*
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
|
156
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
92
|
-
|
|
93
|
-
|
|
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
|
-
|
|
108
|
-
await
|
|
109
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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
|
-
|
|
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
|
}
|