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 +10 -4
- package/Screenshot/v3.0.1.png +0 -0
- package/Screenshot/v3.1.0.png +0 -0
- package/dist/gui.js +93 -30
- package/dist/makefsdata.js +13 -13
- package/dist/utils.js +73 -0
- package/package.json +1 -1
- package/public/index.html +94 -40
- package/src/gui.ts +101 -34
- package/src/makefsdata.ts +25 -12
- package/src/utils.ts +76 -0
- package/Screenshot/v3.0.0.png +0 -0
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
|
-

|
|
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
|
-
├──
|
|
16
|
-
|
|
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
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
110
|
-
await
|
|
111
|
-
|
|
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
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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}`);
|
package/dist/makefsdata.js
CHANGED
|
@@ -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
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
|
-
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
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
|
103
|
+
app.get('/api/version', (_req, res) => {
|
|
104
|
+
cancelShutdown();
|
|
99
105
|
res.json({ version: getPackageVersion() });
|
|
100
106
|
});
|
|
101
107
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
119
|
-
await
|
|
120
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
+
};
|
package/Screenshot/v3.0.0.png
DELETED
|
Binary file
|