cc4-embedded-system 3.0.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 ADDED
@@ -0,0 +1,44 @@
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```.
3
+
4
+ ![](./Screenshot/v3.0.0.png)
5
+
6
+ ## Structure
7
+ ```text
8
+ CC4EmbeddedSystem/
9
+ ├── src/
10
+ │ ├── gui.ts // Express server & CLI entry point
11
+ │ └── makefsdata.ts // Core C code generation & minification logic
12
+ ├── public/
13
+ │ └── index.html // Web GUI dashboard
14
+ ├── dist/ // Compiled JavaScript output (Auto-generated)
15
+ ├── tsconfig.json // TypeScript configuration
16
+ └── package.json // Project configuration & dependencies
17
+ ```
18
+
19
+ ## Commands
20
+ ### Normal Use
21
+ - Global installation
22
+ ```bash
23
+ npm install -g cc4-embedded-system
24
+ cc4es # run
25
+ ```
26
+ - Run once (no installation)
27
+ ```bash
28
+ npx cc4-embedded-system
29
+ ```
30
+ ### Development
31
+ ```bash
32
+ # build
33
+ npm run build
34
+
35
+ # mimic global installation
36
+ npm link
37
+ cc4es
38
+
39
+ # publish
40
+ npm login
41
+ # npm version patch # if increase version
42
+ npm run build
43
+ npm publish --access public
44
+ ```
Binary file
package/dist/gui.js ADDED
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env node
2
+ /*
3
+ * ==============================================================================
4
+ * CC4EmbeddedSystem V3 (makefsdata)
5
+ * Copyright (c) 2026 LunaticGhoulPiano / Emile Su
6
+ * Licensed under the MIT License.
7
+ * ==============================================================================
8
+ *
9
+ * This software integrates concepts, logic, and minification processes derived
10
+ * from the following open-source projects. We deeply appreciate their work:
11
+ *
12
+ * ------------------------------------------------------------------------------
13
+ * 1. lwIP (Lightweight TCP/IP stack)
14
+ * Repository: https://github.com/m-labs/lwip
15
+ * Copyright (c) 2001-2004 Swedish Institute of Computer Science.
16
+ * All rights reserved.
17
+ * * Redistribution and use in source and binary forms, with or without modification,
18
+ * are permitted provided that the following conditions are met:
19
+ * * 1. Redistributions of source code must retain the above copyright notice,
20
+ * this list of conditions and the following disclaimer.
21
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
22
+ * this list of conditions and the following disclaimer in the documentation
23
+ * and/or other materials provided with the distribution.
24
+ * 3. The name of the author may not be used to endorse or promote products
25
+ * derived from this software without specific prior written permission.
26
+ * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
27
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
28
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
29
+ * SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
30
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
31
+ * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
32
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
33
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
34
+ * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
35
+ * OF SUCH DAMAGE.
36
+ *
37
+ * ------------------------------------------------------------------------------
38
+ * 2. html-minifier-next
39
+ * Repository: https://github.com/j9t/html-minifier-next
40
+ * Copyright (c) Jens Oliver Meiert (html-minifier-next)
41
+ * Copyright (c) Daniel Ruf (html-minifier-terser)
42
+ * Copyright (c) Juriy "kangax" Zaytsev (html-minifier)
43
+ * * Permission is hereby granted, free of charge, to any person obtaining a copy
44
+ * of this software and associated documentation files (the "Software"), to deal
45
+ * in the Software without restriction, including without limitation the rights
46
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
47
+ * copies of the Software, and to permit persons to whom the Software is
48
+ * furnished to do so, subject to the following conditions:
49
+ * * The above copyright notice and this permission notice shall be included in
50
+ * all copies or substantial portions of the Software.
51
+ * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
52
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
53
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
54
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
55
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
56
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
57
+ * THE SOFTWARE.
58
+ * ==============================================================================
59
+ */
60
+ // modern ESM
61
+ import express from 'express';
62
+ import open from 'open';
63
+ import path from 'node:path';
64
+ import { fileURLToPath } from 'node:url';
65
+ import fs from 'node:fs';
66
+ import { runMakeFsData } from './makefsdata.js';
67
+ const __filename = fileURLToPath(import.meta.url);
68
+ const __dirname = path.dirname(__filename);
69
+ const app = express();
70
+ // port default: 3000
71
+ let PORT = parseInt(process.env.PORT || '3000', 10);
72
+ const portArgIndex = process.argv.indexOf('--port');
73
+ if (portArgIndex !== -1 && process.argv[portArgIndex + 1]) {
74
+ const parsedPort = parseInt(process.argv[portArgIndex + 1], 10);
75
+ if (!isNaN(parsedPort))
76
+ PORT = parsedPort;
77
+ }
78
+ let server;
79
+ app.use(express.json());
80
+ app.use(express.static(path.join(__dirname, '../public')));
81
+ // get current version
82
+ const getPackageVersion = () => {
83
+ try {
84
+ const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
85
+ return pkg.version || "0.0.0";
86
+ }
87
+ catch {
88
+ return "3.0.0";
89
+ }
90
+ };
91
+ app.get('/api/version', (_req, res) => {
92
+ res.json({ version: getPackageVersion() });
93
+ });
94
+ // main
95
+ app.post('/api/build', async (req, res) => {
96
+ // get minifyOpts
97
+ const { inputPath, outputPath, minifyOpts } = req.body;
98
+ const opts = {
99
+ inputDir: inputPath,
100
+ outputFile: outputPath,
101
+ processSubs: true,
102
+ includeHttpHeader: true,
103
+ useHttp11: false,
104
+ supportSsi: true,
105
+ precalcChksum: false,
106
+ minifyOpts: minifyOpts
107
+ };
108
+ try {
109
+ await runMakeFsData(opts);
110
+ await open(path.dirname(outputPath));
111
+ res.json({ success: true });
112
+ }
113
+ catch (error) {
114
+ res.json({ success: false, message: error.message });
115
+ }
116
+ });
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
+ app.post('/api/change-port', (req, res) => {
123
+ const parsedPort = parseInt(req.body.newPort, 10);
124
+ if (isNaN(parsedPort) || parsedPort < 1 || parsedPort > 65535) {
125
+ res.json({ success: false, message: 'Invalid port number.' });
126
+ return;
127
+ }
128
+ res.json({ success: true });
129
+ // restart
130
+ setTimeout(() => {
131
+ if (server) {
132
+ server.closeAllConnections();
133
+ server.close(() => {
134
+ PORT = parsedPort;
135
+ server = app.listen(PORT, () => {
136
+ console.log(`🔄 Server successfully restarted on port ${PORT}`);
137
+ });
138
+ });
139
+ }
140
+ }, 100);
141
+ });
142
+ server = app.listen(PORT, async () => {
143
+ console.log(`✨ GUI Server started! Opening in browser...`);
144
+ await open(`http://localhost:${PORT}`);
145
+ });
@@ -0,0 +1,322 @@
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
+ // modern ESM
60
+ import fs from 'node:fs';
61
+ import path from 'node:path';
62
+ import { minify } from 'html-minifier-next';
63
+ // ----------------------------------------------------------------------
64
+ // 1. Configs & Version Recovery
65
+ // ----------------------------------------------------------------------
66
+ const TCP_MSS = 1460;
67
+ 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
+ // html-minifier-next options
78
+ const COMPRESS_OPTS_DEFAULT = {
79
+ collapseWhitespace: true,
80
+ removeComments: true,
81
+ minifyJS: true,
82
+ minifyCSS: true,
83
+ processConditionalComments: true,
84
+ decodeEntities: true
85
+ };
86
+ // ----------------------------------------------------------------------
87
+ // 3. lwIP Helpers
88
+ // ----------------------------------------------------------------------
89
+ function getMimeType(fileName) {
90
+ const ext = path.extname(fileName).toLowerCase();
91
+ switch (ext) {
92
+ case '.html':
93
+ case '.htm':
94
+ case '.shtml':
95
+ case '.shtm':
96
+ case '.ssi': {
97
+ return 'text/html';
98
+ }
99
+ case '.css':
100
+ return 'text/css';
101
+ case '.js':
102
+ return 'application/javascript';
103
+ case '.png':
104
+ return 'image/png';
105
+ case '.gif':
106
+ return 'image/gif';
107
+ case '.jpg':
108
+ case '.jpeg': {
109
+ return 'image/jpeg';
110
+ }
111
+ case '.ico':
112
+ return 'image/x-icon';
113
+ case '.xml':
114
+ return 'text/xml';
115
+ case '.json':
116
+ return 'application/json';
117
+ default:
118
+ return 'text/plain';
119
+ }
120
+ }
121
+ function inetChksum(buf) {
122
+ let sum = 0;
123
+ for (let i = 0; i < buf.length; i += 2) {
124
+ const b1 = buf[i];
125
+ const b2 = (i + 1 < buf.length) ? buf[i + 1] : 0;
126
+ if (b1 === undefined)
127
+ continue;
128
+ sum += (b1 << 8) | (b2 ?? 0);
129
+ }
130
+ while (sum >> 16)
131
+ sum = (sum & 0xFFFF) + (sum >> 16);
132
+ return (~sum) & 0xFFFF;
133
+ }
134
+ function generateHttpHeaders(fileName, dataLength, opts) {
135
+ const parts = [];
136
+ const ccVersion = getPackageVersion();
137
+ const addPart = (str) => { parts.push({ str, buf: Buffer.from(str, 'ascii') }); };
138
+ const baseName = path.basename(fileName);
139
+ const ext = path.extname(fileName).toLowerCase();
140
+ const protocol = opts.useHttp11 ? "HTTP/1.1" : "HTTP/1.0";
141
+ if (baseName.startsWith("404"))
142
+ addPart(`${protocol} 404 File not found\r\n`);
143
+ else if (baseName.startsWith("400"))
144
+ addPart(`${protocol} 400 Bad Request\r\n`);
145
+ else if (baseName.startsWith("501"))
146
+ addPart(`${protocol} 501 Not Implemented\r\n`);
147
+ else
148
+ addPart(`${protocol} 200 OK\r\n`);
149
+ const serverInfo = `Server: lwIP & CC4EmbeddedSystem V3\r\n` +
150
+ `lwIP (${LWIP_VERSION}): (http://savannah.nongnu.org/projects/lwip)\r\n` +
151
+ `CC4EmbeddedSystem V3 (${ccVersion}): https://github.com/LunaticGhoulPiano/CC4EmbeddedSystem\r\n`;
152
+ parts.push({ str: serverInfo, buf: Buffer.alloc(0) }); // addPart(serverInfo);
153
+ let isSsi = false;
154
+ if (opts.supportSsi && ['.shtml', '.shtm', '.ssi', '.xml'].includes(ext))
155
+ isSsi = true;
156
+ if (opts.useHttp11) {
157
+ if (!isSsi) {
158
+ addPart(`Content-Length: ${dataLength}\r\n`);
159
+ addPart(`Connection: keep-alive\r\n`);
160
+ }
161
+ else
162
+ addPart(`Connection: close\r\n`);
163
+ }
164
+ addPart(`Content-type: ${getMimeType(fileName)}\r\n\r\n`);
165
+ return { parts, totalBuffer: Buffer.concat(parts.map(p => p.buf)) };
166
+ }
167
+ function getFilesRecursive(dir, processSubs) {
168
+ const results = [];
169
+ if (!fs.existsSync(dir))
170
+ return results;
171
+ const list = fs.readdirSync(dir);
172
+ for (const file of list) {
173
+ const fullPath = path.join(dir, file);
174
+ const stat = fs.statSync(fullPath);
175
+ if (!stat.isDirectory())
176
+ results.push(fullPath);
177
+ else if (processSubs)
178
+ results.push(...getFilesRecursive(fullPath, processSubs));
179
+ }
180
+ return results;
181
+ }
182
+ function bufferToHexCArray(buf) {
183
+ let out = '';
184
+ for (let i = 0; i < buf.length; i++) {
185
+ if (i % 16 === 0)
186
+ out += '\t';
187
+ const b = buf[i];
188
+ if (b === undefined)
189
+ continue;
190
+ out += `0x${b.toString(16).padStart(2, '0')},`;
191
+ if ((i + 1) % 16 === 0)
192
+ out += '\n';
193
+ }
194
+ if (!out.endsWith('\n'))
195
+ out += '\n';
196
+ return out;
197
+ }
198
+ // ----------------------------------------------------------------------
199
+ // 4. export API
200
+ // ----------------------------------------------------------------------
201
+ export async function runMakeFsData(opts) {
202
+ console.log(`🚀 CC4EmbeddedSystem V3: Starting makefsdata compilation...`);
203
+ // checkers
204
+ if (!fs.existsSync(opts.inputDir)) {
205
+ console.log(`⚠️ Input directory not found. Auto-creating directory: \n${opts.inputDir}`);
206
+ fs.mkdirSync(opts.inputDir, { recursive: true });
207
+ }
208
+ if (!opts.outputFile.toLowerCase().endsWith('.c')) {
209
+ opts.outputFile += '.c';
210
+ console.log(`🔧 Auto-appended '.c' to output file -> ${opts.outputFile}`);
211
+ }
212
+ const outDir = path.dirname(opts.outputFile);
213
+ if (!fs.existsSync(outDir))
214
+ fs.mkdirSync(outDir, { recursive: true });
215
+ const allFiles = getFilesRecursive(opts.inputDir, opts.processSubs);
216
+ const fileEntries = [];
217
+ if (allFiles.length === 0)
218
+ throw new Error(`Input directory is empty! Please put your web files (.html, .css, etc.) into:\n${path.resolve(opts.inputDir)}`);
219
+ // html-minifier-next options
220
+ const activeCompressOpts = opts.minifyOpts || COMPRESS_OPTS_DEFAULT;
221
+ for (let i = 0; i < allFiles.length; i++) {
222
+ const filePath = allFiles[i];
223
+ const relativePath = '/' + path.relative(opts.inputDir, filePath).replace(/\\/g, '/');
224
+ const ext = path.extname(filePath).toLowerCase();
225
+ let content = fs.readFileSync(filePath);
226
+ if (['.html', '.htm', '.css', '.js'].includes(ext)) {
227
+ const minifiedStr = await minify(content.toString('utf8'), activeCompressOpts);
228
+ if (typeof minifiedStr === 'string') {
229
+ content = Buffer.from(minifiedStr, 'utf8');
230
+ console.log(`📦 Minified: ${relativePath}`);
231
+ }
232
+ }
233
+ else
234
+ console.log(`📄 Copied: ${relativePath}`);
235
+ // generate header
236
+ const headerData = opts.includeHttpHeader ? generateHttpHeaders(filePath, content.length, opts) : { parts: [], totalBuffer: Buffer.alloc(0) };
237
+ const nameBuffer = Buffer.from(relativePath + '\0', 'utf8');
238
+ const varName = relativePath.replace(/[^A-Za-z0-9]/g, '_');
239
+ const chksums = [];
240
+ if (opts.precalcChksum) {
241
+ let offset = 0;
242
+ if (headerData.totalBuffer.length > 0) {
243
+ chksums.push({ offset: 0, chksum: inetChksum(headerData.totalBuffer), len: headerData.totalBuffer.length });
244
+ offset += headerData.totalBuffer.length;
245
+ }
246
+ for (let chunkOffset = 0; chunkOffset < content.length; chunkOffset += TCP_MSS) {
247
+ const chunkLen = Math.min(TCP_MSS, content.length - chunkOffset);
248
+ chksums.push({ offset: offset, chksum: inetChksum(content.subarray(chunkOffset, chunkOffset + chunkLen)), len: chunkLen });
249
+ offset += chunkLen;
250
+ }
251
+ }
252
+ fileEntries.push({ varName, pathName: relativePath, nameBuffer, headerParts: headerData.parts, headerTotalBuffer: headerData.totalBuffer, contentBuffer: content, chksums });
253
+ }
254
+ // ====================================================================
255
+ // C Code Generation
256
+ // ====================================================================
257
+ let cOutput = `/* Generated by CC4EmbeddedSystem V3 (makefsdata) */\n\n`;
258
+ cOutput += `#include "fs.h"\n#include "lwip/def.h"\n#include "fsdata.h"\n\n\n`;
259
+ cOutput += `#define file_NULL (struct fsdata_file *) NULL\n\n\n`;
260
+ if (opts.precalcChksum) {
261
+ cOutput += `#if HTTPD_PRECALCULATED_CHECKSUM\n`;
262
+ for (const file of fileEntries) {
263
+ cOutput += `const struct fsdata_chksum chksums_${file.varName}[] = {\n`;
264
+ for (const chk of file.chksums)
265
+ cOutput += `\t{${chk.offset}, 0x${chk.chksum.toString(16).padStart(4, '0')}, ${chk.len}},\n`;
266
+ cOutput += `};\n`;
267
+ }
268
+ cOutput += `#endif /* HTTPD_PRECALCULATED_CHECKSUM */\n\n\n`;
269
+ }
270
+ // generate data arrays
271
+ for (const file of fileEntries) {
272
+ cOutput += `static const unsigned int dummy_align_${file.varName} = 0;\n`;
273
+ cOutput += `static const unsigned char data_${file.varName}[] = {\n`;
274
+ cOutput += `\t/* ${file.pathName} (${file.nameBuffer.length} chars) */\n`;
275
+ cOutput += bufferToHexCArray(file.nameBuffer);
276
+ if (file.headerParts.length > 0) {
277
+ cOutput += `\n\t/* HTTP header */\n`;
278
+ for (const part of file.headerParts) {
279
+ let finalStr = part.str.replace(/\n/g, '\n\t');
280
+ if (finalStr.endsWith('\t'))
281
+ finalStr = finalStr.slice(0, -1);
282
+ cOutput += `\t/* "${finalStr}\t" (${part.buf.length} bytes) */\n`;
283
+ cOutput += bufferToHexCArray(part.buf);
284
+ }
285
+ }
286
+ cOutput += `\n\t/* raw file data (${file.contentBuffer.length} bytes) */\n`;
287
+ cOutput += bufferToHexCArray(file.contentBuffer);
288
+ cOutput += `};\n\n\n\n`;
289
+ }
290
+ // redirhome.html
291
+ cOutput += `/* --- Empty arrays --- */\n`;
292
+ cOutput += `static const unsigned char data__redirhome_html[] = {};\n`;
293
+ cOutput += `const struct fsdata_file file__redirhome_html[] = { {\n`;
294
+ cOutput += `\tfile_NULL,\n`;
295
+ cOutput += `\tdata__redirhome_html,\n`;
296
+ cOutput += `\tdata__redirhome_html + 16,\n`;
297
+ cOutput += `\tsizeof(data__redirhome_html) - 16,\n`;
298
+ cOutput += `\t1,\n`;
299
+ // checksum
300
+ if (opts.precalcChksum)
301
+ cOutput += `#if HTTPD_PRECALCULATED_CHECKSUM\n\t0, NULL,\n#endif /* HTTPD_PRECALCULATED_CHECKSUM */\n`;
302
+ cOutput += `}};\n\n\n`;
303
+ // generate linked-list
304
+ for (let i = fileEntries.length - 1; i >= 0; i--) {
305
+ const current = fileEntries[i];
306
+ // last element didn't points to file_NULL but file__redirhome_html
307
+ const nextVar = fileEntries[i + 1] ? `file_${fileEntries[i + 1].varName}` : 'file__redirhome_html';
308
+ const nameLen = current.nameBuffer.length;
309
+ // address: e.g. "/index.html\0" = '/' + 'i' + 'n' + 'd' + 'e' + 'x' + '.' + 'h' + 't' + 'm' + 'l' + '\0' = 12
310
+ cOutput += `const struct fsdata_file file_${current.varName}[] = { {\n`;
311
+ cOutput += `\t${nextVar},\n\tdata_${current.varName},\n\tdata_${current.varName} + ${nameLen},\n\tsizeof(data_${current.varName}) - ${nameLen},\n\t${opts.includeHttpHeader ? 1 : 0},\n`;
312
+ if (opts.precalcChksum)
313
+ cOutput += `#if HTTPD_PRECALCULATED_CHECKSUM\n\t${current.chksums.length}, chksums_${current.varName},\n#endif /* HTTPD_PRECALCULATED_CHECKSUM */\n`;
314
+ cOutput += `}};\n\n`;
315
+ }
316
+ const rootNode = fileEntries[0];
317
+ if (rootNode) {
318
+ cOutput += `#define FS_ROOT file_${rootNode.varName}\n#define FS_NUMFILES ${fileEntries.length + 1}\n`;
319
+ fs.writeFileSync(opts.outputFile, cOutput);
320
+ console.log(`\n✨ Success! Output written to: ${opts.outputFile}`);
321
+ }
322
+ }
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "cc4-embedded-system",
3
+ "version": "3.0.0",
4
+ "description": "A modern typescript version of makefsdata based on html-minifier-next and lwIP v1.3.1",
5
+ "bin": {
6
+ "cc4es": "./dist/gui.js"
7
+ },
8
+ "type": "module",
9
+ "scripts": {
10
+ "dev": "tsx src/gui.ts",
11
+ "build": "tsc",
12
+ "prepublishOnly": "npm run build"
13
+ },
14
+ "keywords": [
15
+ "lwip",
16
+ "makefsdata",
17
+ "stm32",
18
+ "embedded-system",
19
+ "html-minifier",
20
+ "html-minifier-next",
21
+ "html-compressor"
22
+ ],
23
+ "author": "LunaticGhoulPiano",
24
+ "license": "MIT",
25
+ "dependencies": {
26
+ "express": "^5.2.1",
27
+ "html-minifier-next": "^6.1.2",
28
+ "open": "^11.0.0"
29
+ },
30
+ "devDependencies": {
31
+ "@types/express": "^5.0.6",
32
+ "@types/node": "^25.6.0",
33
+ "@types/open": "^6.1.0",
34
+ "tsx": "^4.21.0",
35
+ "typescript": "^6.0.3"
36
+ }
37
+ }
@@ -0,0 +1,166 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>CC4EmbeddedSystem V3</title>
6
+ <style>
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); }
9
+ h2 { color: #569cd6; margin-top: 0; display: flex; justify-content: center; align-items: center; gap: 12px; }
10
+ .github-link { color: #d4d4d4; display: flex; align-items: center; transition: color 0.2s; text-decoration: none; }
11
+ .github-link:hover { color: #ffffff; }
12
+ .form-group { margin-bottom: 20px; }
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; }
15
+ input[type="text"]:focus { outline: none; border-color: #569cd6; }
16
+ input::placeholder { color: #888; }
17
+ 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
+ button:hover { background: #1177bb; }
19
+
20
+ /* Additional settings' styles */
21
+ details { background: #1e1e1e; padding: 12px; border-radius: 4px; border: 1px solid #444; margin-bottom: 20px; }
22
+ summary { cursor: pointer; color: #c586c0; font-weight: bold; outline: none; user-select: none; }
23
+ .options-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 15px; }
24
+ .checkbox-label { display: flex; align-items: center; font-weight: normal; color: #cccccc; cursor: pointer; font-size: 14px; margin-bottom: 0; }
25
+ .checkbox-label input { margin-right: 8px; cursor: pointer; accent-color: #0e639c; }
26
+ </style>
27
+ </head>
28
+ <body>
29
+
30
+ <div class="container">
31
+ <h2>
32
+ <a href="https://github.com/LunaticGhoulPiano/CC4EmbeddedSystem" target="_blank" rel="noopener noreferrer" class="github-link" title="View Source on GitHub">
33
+ <svg height="28" viewBox="0 0 16 16" width="28" aria-hidden="true">
34
+ <path fill="currentColor" d="M8 0c4.42 0 8 3.58 8 8a8.013 8.013 0 0 1-5.45 7.59c-.4.08-.55-.17-.55-.38 0-.27.01-1.13.01-2.2 0-.75-.25-1.23-.54-1.48 1.78-.2 3.65-.88 3.65-3.95 0-.88-.31-1.59-.82-2.15.08-.2.36-1.02-.08-2.12 0 0-.67-.22-2.2.82-.64-.18-1.32-.27-2-.27-.68 0-1.36.09-2 .27-1.53-1.03-2.2-.82-2.2-.82-.44 1.1-.16 1.92-.08 2.12-.51.56-.82 1.28-.82 2.15 0 3.06 1.86 3.75 3.64 3.95-.23.2-.44.55-.51 1.07-.46.21-1.61.55-2.33-.66-.15-.24-.6-.83-1.23-.82-.67.01-.27.38.01.53.34.19.73.9.82 1.13.16.45.68 1.31 2.69.94 0 .67.01 1.3.01 1.49 0 .21-.15.45-.55.38A7.995 7.995 0 0 1 0 8c0-4.42 3.58-8 8-8Z"></path>
35
+ </svg>
36
+ </a>
37
+ <span id="app-title-text">CC4EmbeddedSystem V3</span>
38
+ </h2>
39
+
40
+ <div class="form-group">
41
+ <label>Input Directory</label>
42
+ <input type="text" id="inputPath" placeholder="e.g., ./input_webpages">
43
+ </div>
44
+
45
+ <div class="form-group">
46
+ <label>Output File (fsdata.c)</label>
47
+ <input type="text" id="outputPath" placeholder="e.g., ./output_fsdata.c">
48
+ </div>
49
+
50
+ <details>
51
+ <summary>⚙️ Advanced Settings</summary>
52
+
53
+ <div style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 12px; padding-bottom: 12px; border-bottom: 1px solid #444;">
54
+ <label style="margin-bottom: 0;">GUI Server Port</label>
55
+ <div style="display: flex; gap: 8px;">
56
+ <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>
58
+ </div>
59
+ </div>
60
+
61
+ <label style="margin-bottom: 10px; font-size: 14px;">Compression Options:</label>
62
+ <div class="options-grid">
63
+ <label class="checkbox-label"><input type="checkbox" id="opt-collapseWhitespace" checked> collapseWhitespace</label>
64
+ <label class="checkbox-label"><input type="checkbox" id="opt-removeComments" checked> removeComments</label>
65
+ <label class="checkbox-label"><input type="checkbox" id="opt-minifyJS" checked> minifyJS</label>
66
+ <label class="checkbox-label"><input type="checkbox" id="opt-minifyCSS" checked> minifyCSS</label>
67
+ <label class="checkbox-label"><input type="checkbox" id="opt-processConditionalComments" checked> conditionalComments</label>
68
+ <label class="checkbox-label"><input type="checkbox" id="opt-decodeEntities" checked> decodeEntities</label>
69
+ <label class="checkbox-label"><input type="checkbox" id="opt-removeAttributeQuotes"> removeAttributeQuotes</label>
70
+ <label class="checkbox-label"><input type="checkbox" id="opt-removeEmptyAttributes"> removeEmptyAttributes</label>
71
+ <label class="checkbox-label"><input type="checkbox" id="opt-removeRedundantAttributes"> redundantAttributes</label>
72
+ <label class="checkbox-label"><input type="checkbox" id="opt-useShortDoctype"> useShortDoctype</label>
73
+ </div>
74
+ </details>
75
+
76
+ <button onclick="startBuild()">Generate C Code</button>
77
+ </div>
78
+
79
+ <script>
80
+ let isRedirecting = false;
81
+ document.getElementById('guiPort').value = window.location.port || '80';
82
+
83
+ // get package.json version
84
+ window.addEventListener('DOMContentLoaded', async () => {
85
+ try {
86
+ const res = await fetch('/api/version');
87
+ const data = await res.json();
88
+ if (data.version) document.getElementById('app-title-text').innerText = `CC4EmbeddedSystem V3 (${data.version})`;
89
+ }
90
+ catch (error) {
91
+ console.error('Can\'t get package.json version!', error);
92
+ }
93
+ });
94
+
95
+ async function changePort() {
96
+ const newPort = document.getElementById('guiPort').value.trim();
97
+ const currentPort = window.location.port || '80';
98
+
99
+ if (!newPort || newPort === currentPort) return;
100
+
101
+ const res = await fetch('/api/change-port', {
102
+ method: 'POST',
103
+ headers: { 'Content-Type': 'application/json' },
104
+ body: JSON.stringify({ newPort })
105
+ });
106
+
107
+ const result = await res.json();
108
+ 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);
115
+ }
116
+ else alert('❌ Failed to change port: \n' + result.message);
117
+ }
118
+
119
+ // run
120
+ async function startBuild() {
121
+ const inPath = document.getElementById('inputPath').value.trim();
122
+ const outPath = document.getElementById('outputPath').value.trim();
123
+
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
+ const minifyOptions = {
131
+ collapseWhitespace: document.getElementById('opt-collapseWhitespace').checked,
132
+ removeComments: document.getElementById('opt-removeComments').checked,
133
+ minifyJS: document.getElementById('opt-minifyJS').checked,
134
+ minifyCSS: document.getElementById('opt-minifyCSS').checked,
135
+ processConditionalComments: document.getElementById('opt-processConditionalComments').checked,
136
+ decodeEntities: document.getElementById('opt-decodeEntities').checked,
137
+ removeAttributeQuotes: document.getElementById('opt-removeAttributeQuotes').checked,
138
+ removeEmptyAttributes: document.getElementById('opt-removeEmptyAttributes').checked,
139
+ removeRedundantAttributes: document.getElementById('opt-removeRedundantAttributes').checked,
140
+ useShortDoctype: document.getElementById('opt-useShortDoctype').checked
141
+ };
142
+
143
+ const res = await fetch('/api/build', {
144
+ method: 'POST',
145
+ headers: { 'Content-Type': 'application/json' },
146
+ body: JSON.stringify({
147
+ inputPath: inPath,
148
+ outputPath: outPath,
149
+ minifyOpts: minifyOptions
150
+ })
151
+ });
152
+
153
+ const result = await res.json();
154
+
155
+ if (result.success) alert('✨ Success: \nC Array successfully generated at ' + outPath);
156
+ else alert('❌ Compile Error: \n' + result.message);
157
+ }
158
+
159
+ // listen close window
160
+ window.addEventListener('beforeunload', () => {
161
+ if (! isRedirecting) navigator.sendBeacon('/api/shutdown');
162
+ });
163
+ </script>
164
+
165
+ </body>
166
+ </html>
package/src/gui.ts ADDED
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env node
2
+
3
+ /*
4
+ * ==============================================================================
5
+ * CC4EmbeddedSystem V3 (makefsdata)
6
+ * Copyright (c) 2026 LunaticGhoulPiano / Emile Su
7
+ * Licensed under the MIT License.
8
+ * ==============================================================================
9
+ *
10
+ * This software integrates concepts, logic, and minification processes derived
11
+ * from the following open-source projects. We deeply appreciate their work:
12
+ *
13
+ * ------------------------------------------------------------------------------
14
+ * 1. lwIP (Lightweight TCP/IP stack)
15
+ * Repository: https://github.com/m-labs/lwip
16
+ * Copyright (c) 2001-2004 Swedish Institute of Computer Science.
17
+ * All rights reserved.
18
+ * * Redistribution and use in source and binary forms, with or without modification,
19
+ * are permitted provided that the following conditions are met:
20
+ * * 1. Redistributions of source code must retain the above copyright notice,
21
+ * this list of conditions and the following disclaimer.
22
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
23
+ * this list of conditions and the following disclaimer in the documentation
24
+ * and/or other materials provided with the distribution.
25
+ * 3. The name of the author may not be used to endorse or promote products
26
+ * derived from this software without specific prior written permission.
27
+ * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
28
+ * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
29
+ * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
30
+ * SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
31
+ * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
32
+ * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
33
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
34
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
35
+ * IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
36
+ * OF SUCH DAMAGE.
37
+ *
38
+ * ------------------------------------------------------------------------------
39
+ * 2. html-minifier-next
40
+ * Repository: https://github.com/j9t/html-minifier-next
41
+ * Copyright (c) Jens Oliver Meiert (html-minifier-next)
42
+ * Copyright (c) Daniel Ruf (html-minifier-terser)
43
+ * Copyright (c) Juriy "kangax" Zaytsev (html-minifier)
44
+ * * Permission is hereby granted, free of charge, to any person obtaining a copy
45
+ * of this software and associated documentation files (the "Software"), to deal
46
+ * in the Software without restriction, including without limitation the rights
47
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
48
+ * copies of the Software, and to permit persons to whom the Software is
49
+ * furnished to do so, subject to the following conditions:
50
+ * * The above copyright notice and this permission notice shall be included in
51
+ * all copies or substantial portions of the Software.
52
+ * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
53
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
54
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
55
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
56
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
57
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
58
+ * THE SOFTWARE.
59
+ * ==============================================================================
60
+ */
61
+
62
+ // modern ESM
63
+ import express from 'express';
64
+ import open from 'open';
65
+ import path from 'node:path';
66
+ import { fileURLToPath } from 'node:url';
67
+ import fs from 'node:fs';
68
+ import { runMakeFsData, MakeFsDataOptions } from './makefsdata.js';
69
+
70
+ const __filename: string = fileURLToPath(import.meta.url);
71
+ const __dirname: string = path.dirname(__filename);
72
+ const app = express();
73
+
74
+ // port default: 3000
75
+ let PORT: number = parseInt(process.env.PORT || '3000', 10);
76
+ const portArgIndex = process.argv.indexOf('--port');
77
+ if (portArgIndex !== -1 && process.argv[portArgIndex + 1]) {
78
+ const parsedPort = parseInt(process.argv[portArgIndex + 1]!, 10);
79
+ if (!isNaN(parsedPort)) PORT = parsedPort;
80
+ }
81
+
82
+ let server: any;
83
+
84
+ app.use(express.json());
85
+ app.use(express.static(path.join(__dirname, '../public')));
86
+
87
+ // get current version
88
+ const getPackageVersion = (): string => {
89
+ try {
90
+ const pkg = JSON.parse(fs.readFileSync('./package.json', 'utf8'));
91
+ return pkg.version || "0.0.0";
92
+ }
93
+ catch {
94
+ return "3.0.0";
95
+ }
96
+ };
97
+
98
+ app.get('/api/version', (_req: express.Request, res: express.Response) => {
99
+ res.json({ version: getPackageVersion() });
100
+ });
101
+
102
+ // main
103
+ app.post('/api/build', async (req: express.Request, res: express.Response) => {
104
+ // get minifyOpts
105
+ const { inputPath, outputPath, minifyOpts } = req.body;
106
+ const opts: MakeFsDataOptions = {
107
+ inputDir: inputPath,
108
+ outputFile: outputPath,
109
+ processSubs: true,
110
+ includeHttpHeader: true,
111
+ useHttp11: false,
112
+ supportSsi: true,
113
+ precalcChksum: false,
114
+ minifyOpts: minifyOpts
115
+ };
116
+
117
+ try {
118
+ await runMakeFsData(opts);
119
+ await open(path.dirname(outputPath));
120
+ res.json({ success: true });
121
+ }
122
+ catch (error: any) {
123
+ res.json({ success: false, message: error.message });
124
+ }
125
+ });
126
+
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
+ app.post('/api/change-port', (req: express.Request, res: express.Response): void => {
134
+ const parsedPort = parseInt(req.body.newPort, 10);
135
+
136
+ if (isNaN(parsedPort) || parsedPort < 1 || parsedPort > 65535) {
137
+ res.json({ success: false, message: 'Invalid port number.' });
138
+ return;
139
+ }
140
+
141
+ res.json({ success: true });
142
+
143
+ // restart
144
+ setTimeout(() => {
145
+ if (server) {
146
+ server.closeAllConnections();
147
+ server.close(() => {
148
+ PORT = parsedPort;
149
+ server = app.listen(PORT, () => {
150
+ console.log(`🔄 Server successfully restarted on port ${PORT}`);
151
+ });
152
+ });
153
+ }
154
+ }, 100);
155
+ });
156
+
157
+ server = app.listen(PORT, async () => {
158
+ console.log(`✨ GUI Server started! Opening in browser...`);
159
+ await open(`http://localhost:${PORT}`);
160
+ });
@@ -0,0 +1,377 @@
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
+ // modern ESM
61
+ import fs from 'node:fs';
62
+ import path from 'node:path';
63
+ import { minify } from 'html-minifier-next';
64
+
65
+ // ----------------------------------------------------------------------
66
+ // 1. Configs & Version Recovery
67
+ // ----------------------------------------------------------------------
68
+ const TCP_MSS: number = 1460;
69
+ const LWIP_VERSION: string = "1.3.1"; // this makefsdata.ts is based on lwIP v1.3.1
70
+
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
+ // html-minifier-next options
82
+ const COMPRESS_OPTS_DEFAULT = {
83
+ collapseWhitespace: true,
84
+ removeComments: true,
85
+ minifyJS: true,
86
+ minifyCSS: true,
87
+ processConditionalComments: true,
88
+ decodeEntities: true
89
+ };
90
+
91
+ export interface MakeFsDataOptions {
92
+ inputDir: string;
93
+ outputFile: string;
94
+ processSubs: boolean;
95
+ includeHttpHeader: boolean;
96
+ useHttp11: boolean;
97
+ supportSsi: boolean;
98
+ precalcChksum: boolean;
99
+ minifyOpts?: any; // customize
100
+ }
101
+
102
+ // ----------------------------------------------------------------------
103
+ // 2. Structures
104
+ // ----------------------------------------------------------------------
105
+ interface ChksumBlock { offset: number; chksum: number; len: number; }
106
+ interface HeaderPart { str: string; buf: Buffer; }
107
+ interface FileEntry {
108
+ varName: string; pathName: string; nameBuffer: Buffer;
109
+ headerParts: HeaderPart[]; headerTotalBuffer: Buffer;
110
+ contentBuffer: Buffer; chksums: ChksumBlock[];
111
+ }
112
+
113
+ // ----------------------------------------------------------------------
114
+ // 3. lwIP Helpers
115
+ // ----------------------------------------------------------------------
116
+ function getMimeType(fileName: string): string {
117
+ const ext: string = path.extname(fileName).toLowerCase();
118
+ switch (ext) {
119
+ case '.html':
120
+ case '.htm':
121
+ case '.shtml':
122
+ case '.shtm':
123
+ case '.ssi': {
124
+ return 'text/html';
125
+ }
126
+
127
+ case '.css':
128
+ return 'text/css';
129
+ case '.js':
130
+ return 'application/javascript';
131
+ case '.png':
132
+ return 'image/png';
133
+ case '.gif':
134
+ return 'image/gif';
135
+
136
+ case '.jpg':
137
+ case '.jpeg': {
138
+ return 'image/jpeg';
139
+ }
140
+
141
+ case '.ico':
142
+ return 'image/x-icon';
143
+ case '.xml':
144
+ return 'text/xml';
145
+ case '.json':
146
+ return 'application/json';
147
+ default:
148
+ return 'text/plain';
149
+ }
150
+ }
151
+
152
+ function inetChksum(buf: Buffer): number {
153
+ let sum: number = 0;
154
+ for (let i = 0; i < buf.length; i += 2) {
155
+ const b1: number | undefined = buf[i];
156
+ const b2: number | undefined = (i + 1 < buf.length) ? buf[i + 1] : 0;
157
+
158
+ if (b1 === undefined) continue;
159
+ sum += (b1 << 8) | (b2 ?? 0);
160
+ }
161
+
162
+ while (sum >> 16) sum = (sum & 0xFFFF) + (sum >> 16);
163
+
164
+ return (~sum) & 0xFFFF;
165
+ }
166
+
167
+ function generateHttpHeaders(fileName: string, dataLength: number, opts: MakeFsDataOptions): { parts: HeaderPart[], totalBuffer: Buffer } {
168
+ const parts: HeaderPart[] = [];
169
+ const ccVersion = getPackageVersion();
170
+ const addPart = (str: string) => { parts.push({ str, buf: Buffer.from(str, 'ascii') }); };
171
+
172
+ const baseName: string = path.basename(fileName);
173
+ const ext: string = path.extname(fileName).toLowerCase();
174
+
175
+ const protocol: string = opts.useHttp11 ? "HTTP/1.1" : "HTTP/1.0";
176
+ if (baseName.startsWith("404")) addPart(`${protocol} 404 File not found\r\n`);
177
+ else if (baseName.startsWith("400")) addPart(`${protocol} 400 Bad Request\r\n`);
178
+ else if (baseName.startsWith("501")) addPart(`${protocol} 501 Not Implemented\r\n`);
179
+ else addPart(`${protocol} 200 OK\r\n`);
180
+
181
+ const serverInfo = `Server: lwIP & CC4EmbeddedSystem V3\r\n` +
182
+ `lwIP (${LWIP_VERSION}): (http://savannah.nongnu.org/projects/lwip)\r\n` +
183
+ `CC4EmbeddedSystem V3 (${ccVersion}): https://github.com/LunaticGhoulPiano/CC4EmbeddedSystem\r\n`;
184
+ parts.push({ str: serverInfo, buf: Buffer.alloc(0) }); // addPart(serverInfo);
185
+
186
+ let isSsi: boolean = false;
187
+ if (opts.supportSsi && ['.shtml', '.shtm', '.ssi', '.xml'].includes(ext)) isSsi = true;
188
+
189
+ if (opts.useHttp11) {
190
+ if (! isSsi) {
191
+ addPart(`Content-Length: ${dataLength}\r\n`);
192
+ addPart(`Connection: keep-alive\r\n`);
193
+ }
194
+ else addPart(`Connection: close\r\n`);
195
+ }
196
+
197
+ addPart(`Content-type: ${getMimeType(fileName)}\r\n\r\n`);
198
+
199
+ return { parts, totalBuffer: Buffer.concat(parts.map(p => p.buf)) };
200
+ }
201
+
202
+ function getFilesRecursive(dir: string, processSubs: boolean): string[] {
203
+ const results: string[] = [];
204
+ if (! fs.existsSync(dir)) return results;
205
+ const list: string[] = fs.readdirSync(dir);
206
+
207
+ for (const file of list) {
208
+ const fullPath: string = path.join(dir, file);
209
+ const stat: fs.Stats = fs.statSync(fullPath);
210
+ if (! stat.isDirectory()) results.push(fullPath);
211
+ else if (processSubs) results.push(...getFilesRecursive(fullPath, processSubs));
212
+ }
213
+
214
+ return results;
215
+ }
216
+
217
+ function bufferToHexCArray(buf: Buffer): string {
218
+ let out = '';
219
+ for (let i = 0; i < buf.length; i++) {
220
+ if (i % 16 === 0) out += '\t';
221
+ const b: number | undefined = buf[i];
222
+ if (b === undefined) continue;
223
+ out += `0x${b.toString(16).padStart(2, '0')},`;
224
+ if ((i + 1) % 16 === 0) out += '\n';
225
+ }
226
+
227
+ if (!out.endsWith('\n')) out += '\n';
228
+
229
+ return out;
230
+ }
231
+
232
+ // ----------------------------------------------------------------------
233
+ // 4. export API
234
+ // ----------------------------------------------------------------------
235
+ export async function runMakeFsData(opts: MakeFsDataOptions): Promise<void> {
236
+ console.log(`🚀 CC4EmbeddedSystem V3: Starting makefsdata compilation...`);
237
+
238
+ // checkers
239
+ if (! fs.existsSync(opts.inputDir)) {
240
+ console.log(`⚠️ Input directory not found. Auto-creating directory: \n${opts.inputDir}`);
241
+ fs.mkdirSync(opts.inputDir, { recursive: true });
242
+ }
243
+
244
+ if (!opts.outputFile.toLowerCase().endsWith('.c')) {
245
+ opts.outputFile += '.c';
246
+ console.log(`🔧 Auto-appended '.c' to output file -> ${opts.outputFile}`);
247
+ }
248
+
249
+ const outDir = path.dirname(opts.outputFile);
250
+ if (! fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
251
+
252
+ const allFiles = getFilesRecursive(opts.inputDir, opts.processSubs);
253
+ const fileEntries: FileEntry[] = [];
254
+
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
+
257
+ // html-minifier-next options
258
+ const activeCompressOpts = opts.minifyOpts || COMPRESS_OPTS_DEFAULT;
259
+
260
+ for (let i = 0; i < allFiles.length; i++) {
261
+ const filePath = allFiles[i]!;
262
+ const relativePath = '/' + path.relative(opts.inputDir, filePath).replace(/\\/g, '/');
263
+ const ext = path.extname(filePath).toLowerCase();
264
+ let content = fs.readFileSync(filePath);
265
+
266
+ if (['.html', '.htm', '.css', '.js'].includes(ext)) {
267
+ const minifiedStr = await minify(content.toString('utf8'), activeCompressOpts);
268
+
269
+ if (typeof minifiedStr === 'string') {
270
+ content = Buffer.from(minifiedStr, 'utf8');
271
+ console.log(`📦 Minified: ${relativePath}`);
272
+ }
273
+ }
274
+ else console.log(`📄 Copied: ${relativePath}`);
275
+
276
+ // generate header
277
+ const headerData = opts.includeHttpHeader ? generateHttpHeaders(filePath, content.length, opts) : { parts: [], totalBuffer: Buffer.alloc(0) };
278
+ const nameBuffer = Buffer.from(relativePath + '\0', 'utf8');
279
+ const varName = relativePath.replace(/[^A-Za-z0-9]/g, '_');
280
+
281
+ const chksums: ChksumBlock[] = [];
282
+ if (opts.precalcChksum) {
283
+ let offset = 0;
284
+
285
+ if (headerData.totalBuffer.length > 0) {
286
+ chksums.push({ offset: 0, chksum: inetChksum(headerData.totalBuffer), len: headerData.totalBuffer.length });
287
+ offset += headerData.totalBuffer.length;
288
+ }
289
+
290
+ for (let chunkOffset = 0; chunkOffset < content.length; chunkOffset += TCP_MSS) {
291
+ const chunkLen = Math.min(TCP_MSS, content.length - chunkOffset);
292
+ chksums.push({ offset: offset, chksum: inetChksum(content.subarray(chunkOffset, chunkOffset + chunkLen)), len: chunkLen });
293
+ offset += chunkLen;
294
+ }
295
+ }
296
+
297
+ fileEntries.push({ varName, pathName: relativePath, nameBuffer, headerParts: headerData.parts, headerTotalBuffer: headerData.totalBuffer, contentBuffer: content, chksums });
298
+ }
299
+
300
+ // ====================================================================
301
+ // C Code Generation
302
+ // ====================================================================
303
+ let cOutput: string = `/* Generated by CC4EmbeddedSystem V3 (makefsdata) */\n\n`;
304
+ cOutput += `#include "fs.h"\n#include "lwip/def.h"\n#include "fsdata.h"\n\n\n`;
305
+ cOutput += `#define file_NULL (struct fsdata_file *) NULL\n\n\n`;
306
+
307
+ if (opts.precalcChksum) {
308
+ cOutput += `#if HTTPD_PRECALCULATED_CHECKSUM\n`;
309
+ for (const file of fileEntries) {
310
+ cOutput += `const struct fsdata_chksum chksums_${file.varName}[] = {\n`;
311
+ for (const chk of file.chksums) cOutput += `\t{${chk.offset}, 0x${chk.chksum.toString(16).padStart(4, '0')}, ${chk.len}},\n`;
312
+ cOutput += `};\n`;
313
+ }
314
+
315
+ cOutput += `#endif /* HTTPD_PRECALCULATED_CHECKSUM */\n\n\n`;
316
+ }
317
+
318
+ // generate data arrays
319
+ for (const file of fileEntries) {
320
+ cOutput += `static const unsigned int dummy_align_${file.varName} = 0;\n`;
321
+ cOutput += `static const unsigned char data_${file.varName}[] = {\n`;
322
+ cOutput += `\t/* ${file.pathName} (${file.nameBuffer.length} chars) */\n`;
323
+ cOutput += bufferToHexCArray(file.nameBuffer);
324
+
325
+ if (file.headerParts.length > 0) {
326
+ cOutput += `\n\t/* HTTP header */\n`;
327
+ for (const part of file.headerParts) {
328
+ let finalStr = part.str.replace(/\n/g, '\n\t');
329
+ if (finalStr.endsWith('\t')) finalStr = finalStr.slice(0, -1);
330
+ cOutput += `\t/* "${finalStr}\t" (${part.buf.length} bytes) */\n`;
331
+ cOutput += bufferToHexCArray(part.buf);
332
+ }
333
+ }
334
+
335
+ cOutput += `\n\t/* raw file data (${file.contentBuffer.length} bytes) */\n`;
336
+ cOutput += bufferToHexCArray(file.contentBuffer);
337
+ cOutput += `};\n\n\n\n`;
338
+ }
339
+
340
+ // redirhome.html
341
+ cOutput += `/* --- Empty arrays --- */\n`;
342
+ cOutput += `static const unsigned char data__redirhome_html[] = {};\n`;
343
+ cOutput += `const struct fsdata_file file__redirhome_html[] = { {\n`;
344
+ cOutput += `\tfile_NULL,\n`;
345
+ cOutput += `\tdata__redirhome_html,\n`;
346
+ cOutput += `\tdata__redirhome_html + 16,\n`;
347
+ cOutput += `\tsizeof(data__redirhome_html) - 16,\n`;
348
+ cOutput += `\t1,\n`;
349
+
350
+ // checksum
351
+ if (opts.precalcChksum) cOutput += `#if HTTPD_PRECALCULATED_CHECKSUM\n\t0, NULL,\n#endif /* HTTPD_PRECALCULATED_CHECKSUM */\n`;
352
+ cOutput += `}};\n\n\n`;
353
+
354
+
355
+ // generate linked-list
356
+ for (let i = fileEntries.length - 1; i >= 0; i--) {
357
+ const current = fileEntries[i]!;
358
+
359
+ // last element didn't points to file_NULL but file__redirhome_html
360
+ const nextVar = fileEntries[i + 1] ? `file_${fileEntries[i+1]!.varName}` : 'file__redirhome_html';
361
+ const nameLen = current.nameBuffer.length;
362
+
363
+ // address: e.g. "/index.html\0" = '/' + 'i' + 'n' + 'd' + 'e' + 'x' + '.' + 'h' + 't' + 'm' + 'l' + '\0' = 12
364
+ cOutput += `const struct fsdata_file file_${current.varName}[] = { {\n`;
365
+ cOutput += `\t${nextVar},\n\tdata_${current.varName},\n\tdata_${current.varName} + ${nameLen},\n\tsizeof(data_${current.varName}) - ${nameLen},\n\t${opts.includeHttpHeader ? 1 : 0},\n`;
366
+
367
+ if (opts.precalcChksum) cOutput += `#if HTTPD_PRECALCULATED_CHECKSUM\n\t${current.chksums.length}, chksums_${current.varName},\n#endif /* HTTPD_PRECALCULATED_CHECKSUM */\n`;
368
+ cOutput += `}};\n\n`;
369
+ }
370
+
371
+ const rootNode = fileEntries[0];
372
+ if (rootNode) {
373
+ cOutput += `#define FS_ROOT file_${rootNode.varName}\n#define FS_NUMFILES ${fileEntries.length + 1}\n`;
374
+ fs.writeFileSync(opts.outputFile, cOutput);
375
+ console.log(`\n✨ Success! Output written to: ${opts.outputFile}`);
376
+ }
377
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "compilerOptions": {
3
+ /* environment */
4
+ "target": "ES2022",
5
+ "module": "NodeNext",
6
+ "moduleResolution": "NodeNext",
7
+ "rootDir": "./src",
8
+ "outDir": "./dist",
9
+
10
+ /* strict type checking */
11
+ "strict": true,
12
+ "noImplicitAny": true, /* no "any" */
13
+ "strictNullChecks": true,
14
+ "noUncheckedIndexedAccess": true,
15
+ "exactOptionalPropertyTypes": true,
16
+
17
+ /* Coding style */
18
+ "noUnusedLocals": true,
19
+ "noUnusedParameters": true,
20
+ "noImplicitReturns": true,
21
+
22
+ /* module and consistency */
23
+ "esModuleInterop": true,
24
+ "forceConsistentCasingInFileNames": true,
25
+ "skipLibCheck": true,
26
+ "types": ["node"]
27
+ },
28
+ "include": ["src/**/*"]
29
+ }