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 +44 -0
- package/Screenshot/v3.0.0.png +0 -0
- package/dist/gui.js +145 -0
- package/dist/makefsdata.js +322 -0
- package/package.json +37 -0
- package/public/index.html +166 -0
- package/src/gui.ts +160 -0
- package/src/makefsdata.ts +377 -0
- package/tsconfig.json +29 -0
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
|
+

|
|
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
|
+
}
|