cc4-embedded-system 3.1.10 → 3.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +167 -19
- package/dist/cli.js +171 -0
- package/dist/config.js +90 -0
- package/dist/gui.js +50 -281
- package/dist/gzip-options.js +10 -0
- package/dist/makefsdata.js +152 -32
- package/dist/minify-options.js +38 -0
- package/dist/public/index.html +85 -41
- package/dist/server.js +289 -0
- package/package.json +6 -2
package/dist/gui.js
CHANGED
|
@@ -1,302 +1,71 @@
|
|
|
1
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
|
-
import express from 'express';
|
|
61
|
-
import open from 'open';
|
|
62
|
-
import fs from 'node:fs';
|
|
63
2
|
import path from 'node:path';
|
|
64
|
-
import {
|
|
3
|
+
import { CliUsageError, getHelpText, parseCliArguments } from './cli.js';
|
|
4
|
+
import { getConfigFilePath, getLastBuildPaths, getLastGzipOptions, saveLastBuildPaths } from './config.js';
|
|
5
|
+
import { DEFAULT_GZIP_LEVEL } from './gzip-options.js';
|
|
65
6
|
import { runMakeFsData } from './makefsdata.js';
|
|
7
|
+
import { startGuiServer } from './server.js';
|
|
66
8
|
import { getPackageVersion } from './utils.js';
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return;
|
|
73
|
-
console.error('⚠️ Uncaught Error:', err);
|
|
74
|
-
});
|
|
75
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
76
|
-
const __dirname = path.dirname(__filename);
|
|
77
|
-
const app = express();
|
|
78
|
-
// port set by command
|
|
79
|
-
let PORT = parseInt(process.env.PORT || '3000', 10);
|
|
80
|
-
const portArgIndex = process.argv.indexOf('--port');
|
|
81
|
-
if (portArgIndex !== -1 && process.argv[portArgIndex + 1]) {
|
|
82
|
-
const parsedPort = parseInt(process.argv[portArgIndex + 1], 10);
|
|
83
|
-
if (!isNaN(parsedPort))
|
|
84
|
-
PORT = parsedPort;
|
|
85
|
-
}
|
|
86
|
-
let activeBrowseProcess = null;
|
|
87
|
-
let server;
|
|
88
|
-
let shutdownTimer = null;
|
|
89
|
-
app.use(express.json());
|
|
90
|
-
const publicPath = fs.existsSync(path.join(__dirname, 'public'))
|
|
91
|
-
? path.join(__dirname, 'public')
|
|
92
|
-
: path.join(__dirname, '../public');
|
|
93
|
-
console.log(`[System] Serving static files from: ${publicPath}`);
|
|
94
|
-
app.use(express.static(publicPath));
|
|
95
|
-
const cancelShutdown = () => {
|
|
96
|
-
if (shutdownTimer) {
|
|
97
|
-
clearTimeout(shutdownTimer);
|
|
98
|
-
shutdownTimer = null;
|
|
99
|
-
// console.log('🛡️ Shutdown cancelled (keep alive)');
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
const scheduleShutdown = () => {
|
|
103
|
-
if (!shutdownTimer) {
|
|
104
|
-
shutdownTimer = setTimeout(() => {
|
|
105
|
-
closeBrowseProcess();
|
|
106
|
-
console.log('👋 Window closed, shutting down ...');
|
|
107
|
-
process.exit(0);
|
|
108
|
-
}, 2000);
|
|
109
|
-
}
|
|
9
|
+
const getDefaultGuiPort = () => {
|
|
10
|
+
const configuredPort = Number.parseInt(process.env.PORT ?? '3000', 10);
|
|
11
|
+
if (configuredPort < 1 || configuredPort > 65535 || Number.isNaN(configuredPort))
|
|
12
|
+
return 3000;
|
|
13
|
+
return configuredPort;
|
|
110
14
|
};
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
};
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
try {
|
|
123
|
-
activeBrowseProcess.kill();
|
|
124
|
-
}
|
|
125
|
-
catch (e) {
|
|
126
|
-
// ignore child cleanup error
|
|
127
|
-
}
|
|
15
|
+
const runHeadlessBuild = async (inputPath, outputPath, minifyOpts, optimizeSvg, svgoMultipass, gzipOverride, gzipLevelOverride) => {
|
|
16
|
+
const lastBuild = getLastBuildPaths();
|
|
17
|
+
const lastGzipOptions = getLastGzipOptions();
|
|
18
|
+
const selectedInputPath = inputPath ?? lastBuild?.src;
|
|
19
|
+
const selectedOutputPath = outputPath ?? lastBuild?.dst ?? 'fsdata.c';
|
|
20
|
+
const gzipOptions = {
|
|
21
|
+
gzip: gzipOverride ?? lastGzipOptions?.gzip ?? false,
|
|
22
|
+
gzipLevel: gzipLevelOverride ?? lastGzipOptions?.gzipLevel ?? DEFAULT_GZIP_LEVEL
|
|
23
|
+
};
|
|
24
|
+
if (!selectedInputPath) {
|
|
25
|
+
throw new CliUsageError('Headless mode requires --src, or a source path saved by a previous successful build.');
|
|
128
26
|
}
|
|
129
|
-
activeBrowseProcess = null;
|
|
130
|
-
};
|
|
131
|
-
const runDialogCommand = (command, args) => {
|
|
132
|
-
return new Promise((resolve, reject) => {
|
|
133
|
-
const child = execFile(command, args, {
|
|
134
|
-
windowsHide: false,
|
|
135
|
-
maxBuffer: 1024 * 1024
|
|
136
|
-
}, (error, stdout, stderr) => {
|
|
137
|
-
if (activeBrowseProcess === child)
|
|
138
|
-
activeBrowseProcess = null;
|
|
139
|
-
if (error) {
|
|
140
|
-
reject(new Error(stderr.trim() || error.message));
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
resolve(stdout.trim());
|
|
144
|
-
});
|
|
145
|
-
activeBrowseProcess = child;
|
|
146
|
-
});
|
|
147
|
-
};
|
|
148
|
-
const buildWindowsDialogScript = (dialogLines) => {
|
|
149
|
-
return [
|
|
150
|
-
'Add-Type -AssemblyName System.Windows.Forms',
|
|
151
|
-
'Add-Type -AssemblyName System.Drawing',
|
|
152
|
-
'$screen = [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea',
|
|
153
|
-
'$owner = New-Object System.Windows.Forms.Form',
|
|
154
|
-
'$owner.StartPosition = [System.Windows.Forms.FormStartPosition]::Manual',
|
|
155
|
-
'$owner.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::None',
|
|
156
|
-
'$owner.ShowInTaskbar = $false',
|
|
157
|
-
'$owner.TopMost = $true',
|
|
158
|
-
'$owner.Width = 1',
|
|
159
|
-
'$owner.Height = 1',
|
|
160
|
-
'$owner.Left = $screen.Left + [int]($screen.Width / 2)',
|
|
161
|
-
'$owner.Top = $screen.Top + [int]($screen.Height / 2)',
|
|
162
|
-
'$owner.Opacity = 0',
|
|
163
|
-
'$null = $owner.Show()',
|
|
164
|
-
'$null = $owner.Activate()',
|
|
165
|
-
'$owner.BringToFront()',
|
|
166
|
-
'[System.Windows.Forms.Application]::DoEvents()',
|
|
167
|
-
...dialogLines,
|
|
168
|
-
'$owner.Close()',
|
|
169
|
-
'$owner.Dispose()'
|
|
170
|
-
].join('\n');
|
|
171
|
-
};
|
|
172
|
-
app.get('/api/version', (_req, res) => {
|
|
173
|
-
cancelShutdown();
|
|
174
|
-
res.json({ version: getPackageVersion() });
|
|
175
|
-
});
|
|
176
|
-
app.post('/api/shutdown', (_req, res) => {
|
|
177
|
-
res.json({ success: true });
|
|
178
|
-
closeBrowseProcess();
|
|
179
|
-
scheduleShutdown();
|
|
180
|
-
});
|
|
181
|
-
app.post('/api/cancel-shutdown', (_req, res) => {
|
|
182
|
-
cancelShutdown();
|
|
183
|
-
res.json({ success: true });
|
|
184
|
-
});
|
|
185
|
-
app.post('/api/build', async (req, res) => {
|
|
186
|
-
cancelShutdown();
|
|
187
|
-
const { inputPath, outputPath, minifyOpts } = req.body;
|
|
188
|
-
console.log(`[Build] Input: ${inputPath}`);
|
|
189
|
-
console.log(`[Build] Output: ${outputPath}`);
|
|
190
27
|
const opts = {
|
|
191
|
-
inputDir:
|
|
192
|
-
outputFile:
|
|
28
|
+
inputDir: path.resolve(selectedInputPath),
|
|
29
|
+
outputFile: path.resolve(selectedOutputPath),
|
|
193
30
|
processSubs: true,
|
|
194
31
|
includeHttpHeader: true,
|
|
195
32
|
useHttp11: false,
|
|
196
33
|
supportSsi: true,
|
|
197
34
|
precalcChksum: false,
|
|
198
|
-
minifyOpts
|
|
35
|
+
minifyOpts,
|
|
36
|
+
optimizeSvg,
|
|
37
|
+
svgoMultipass,
|
|
38
|
+
...gzipOptions
|
|
199
39
|
};
|
|
40
|
+
const stats = await runMakeFsData(opts);
|
|
41
|
+
saveLastBuildPaths(opts.inputDir, opts.outputFile, gzipOptions);
|
|
42
|
+
console.log(`✨ Build complete: ${stats.filesCount} file(s), ${stats.originalSize} -> ${stats.compressedSize} -> ${stats.storedSize} bytes (${stats.gzipFilesCount} gzip file(s)).`);
|
|
43
|
+
};
|
|
44
|
+
const main = async () => {
|
|
200
45
|
try {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
catch (e) {
|
|
207
|
-
// ignore open folder error
|
|
208
|
-
}
|
|
209
|
-
// return stats
|
|
210
|
-
res.json({
|
|
211
|
-
success: true,
|
|
212
|
-
stats: stats // originalSize, compressedSize, filesCount
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
catch (error) {
|
|
216
|
-
res.json({ success: false, message: error.message });
|
|
217
|
-
}
|
|
218
|
-
});
|
|
219
|
-
app.post('/api/change-port', (req, res) => {
|
|
220
|
-
cancelShutdown();
|
|
221
|
-
const parsedPort = parseInt(req.body.newPort, 10);
|
|
222
|
-
if (isNaN(parsedPort) || parsedPort < 1 || parsedPort > 65535) {
|
|
223
|
-
res.json({ success: false, message: 'Invalid port number.' });
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
res.json({ success: true });
|
|
227
|
-
setTimeout(() => {
|
|
228
|
-
if (server) {
|
|
229
|
-
try {
|
|
230
|
-
server.closeAllConnections();
|
|
231
|
-
server.close(() => {
|
|
232
|
-
PORT = parsedPort;
|
|
233
|
-
startServer();
|
|
234
|
-
console.log(`🔄 Server successfully restarted on port ${PORT}`);
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
catch (e) {
|
|
238
|
-
startServer();
|
|
239
|
-
}
|
|
46
|
+
const args = parseCliArguments(process.argv.slice(2));
|
|
47
|
+
if (args.showHelp) {
|
|
48
|
+
console.log(getHelpText());
|
|
49
|
+
return;
|
|
240
50
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
cancelShutdown();
|
|
245
|
-
res.on('close', () => {
|
|
246
|
-
if (!res.writableEnded) {
|
|
247
|
-
closeBrowseProcess();
|
|
248
|
-
scheduleShutdown();
|
|
51
|
+
if (args.showVersion) {
|
|
52
|
+
console.log(getPackageVersion());
|
|
53
|
+
return;
|
|
249
54
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
try {
|
|
254
|
-
let result = '';
|
|
255
|
-
if (platform === 'win32') {
|
|
256
|
-
const script = isDir
|
|
257
|
-
? buildWindowsDialogScript([
|
|
258
|
-
'$dialog = New-Object System.Windows.Forms.FolderBrowserDialog',
|
|
259
|
-
'$dialog.Description = "Select Input Directory"',
|
|
260
|
-
'if ($dialog.ShowDialog($owner) -eq [System.Windows.Forms.DialogResult]::OK) {',
|
|
261
|
-
' $dialog.SelectedPath',
|
|
262
|
-
'}'
|
|
263
|
-
])
|
|
264
|
-
: buildWindowsDialogScript([
|
|
265
|
-
'$dialog = New-Object System.Windows.Forms.SaveFileDialog',
|
|
266
|
-
'$dialog.Filter = "C Files (*.c)|*.c|All Files (*.*)|*.*"',
|
|
267
|
-
'$dialog.DefaultExt = "c"',
|
|
268
|
-
'$dialog.AddExtension = $true',
|
|
269
|
-
'$dialog.OverwritePrompt = $true',
|
|
270
|
-
'if ($dialog.ShowDialog($owner) -eq [System.Windows.Forms.DialogResult]::OK) {',
|
|
271
|
-
' $dialog.FileName',
|
|
272
|
-
'}'
|
|
273
|
-
]);
|
|
274
|
-
result = await runDialogCommand('powershell.exe', buildPowerShellArgs(script));
|
|
55
|
+
if (args.showConfigPath) {
|
|
56
|
+
console.log(getConfigFilePath());
|
|
57
|
+
return;
|
|
275
58
|
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
isDir
|
|
280
|
-
? 'POSIX path of (choose folder with prompt "Select Input Directory")'
|
|
281
|
-
: 'POSIX path of (choose file name with prompt "Select Output File")'
|
|
282
|
-
]);
|
|
59
|
+
if (args.headless) {
|
|
60
|
+
await runHeadlessBuild(args.inputPath, args.outputPath, args.minifyOpts, args.optimizeSvg, args.svgoMultipass, args.gzip, args.gzipLevel);
|
|
61
|
+
return;
|
|
283
62
|
}
|
|
284
|
-
|
|
285
|
-
result = await runDialogCommand('zenity', isDir
|
|
286
|
-
? ['--file-selection', '--directory', '--title=Select Input Directory']
|
|
287
|
-
: ['--file-selection', '--save', '--confirm-overwrite', '--title=Select Output File', '--filename=fsdata.c']);
|
|
288
|
-
}
|
|
289
|
-
res.json({ success: true, path: result || null });
|
|
63
|
+
startGuiServer(args.port ?? getDefaultGuiPort());
|
|
290
64
|
}
|
|
291
|
-
catch (
|
|
292
|
-
|
|
65
|
+
catch (error) {
|
|
66
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
67
|
+
console.error(`❌ ${message}`);
|
|
68
|
+
process.exitCode = 1;
|
|
293
69
|
}
|
|
294
|
-
});
|
|
295
|
-
const startServer = () => {
|
|
296
|
-
server = app.listen(PORT, () => {
|
|
297
|
-
console.log(`✨ GUI Server started on http://localhost:${PORT}`);
|
|
298
|
-
});
|
|
299
70
|
};
|
|
300
|
-
|
|
301
|
-
startServer();
|
|
302
|
-
open(`http://localhost:${PORT}`);
|
|
71
|
+
void main();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export const DEFAULT_GZIP_LEVEL = 9;
|
|
2
|
+
export const isGzipLevel = (value) => {
|
|
3
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 1 && value <= 9;
|
|
4
|
+
};
|
|
5
|
+
export const getDefaultGzipBuildOptions = () => {
|
|
6
|
+
return {
|
|
7
|
+
gzip: false,
|
|
8
|
+
gzipLevel: DEFAULT_GZIP_LEVEL
|
|
9
|
+
};
|
|
10
|
+
};
|
package/dist/makefsdata.js
CHANGED
|
@@ -59,23 +59,23 @@
|
|
|
59
59
|
// modern ESM
|
|
60
60
|
import fs from 'node:fs';
|
|
61
61
|
import path from 'node:path';
|
|
62
|
+
import { gzipSync } from 'node:zlib';
|
|
63
|
+
import { transform as transformJavaScript } from 'esbuild';
|
|
62
64
|
import { minify } from 'html-minifier-next';
|
|
65
|
+
import { transform as transformCss } from 'lightningcss';
|
|
66
|
+
import { optimize } from 'svgo';
|
|
67
|
+
import { DEFAULT_GZIP_LEVEL, isGzipLevel } from './gzip-options.js';
|
|
63
68
|
import { getPackageVersion } from './utils.js';
|
|
69
|
+
import { DEFAULT_HTML_MINIFY_OPTIONS } from './minify-options.js';
|
|
64
70
|
// ----------------------------------------------------------------------
|
|
65
71
|
// 1. Configs & Version Recovery
|
|
66
72
|
// ----------------------------------------------------------------------
|
|
67
73
|
const TCP_MSS = 1460;
|
|
68
74
|
const LWIP_VERSION = "1.3.1"; // this makefsdata.ts is based on lwIP v1.3.1
|
|
69
|
-
// html-minifier-next options
|
|
70
|
-
const COMPRESS_OPTS_DEFAULT = {
|
|
71
|
-
collapseWhitespace: true,
|
|
72
|
-
removeComments: true,
|
|
73
|
-
minifyJS: true,
|
|
74
|
-
minifyCSS: true,
|
|
75
|
-
processConditionalComments: true,
|
|
76
|
-
decodeEntities: true
|
|
77
|
-
};
|
|
78
75
|
const REDIRHOME_PATH = '/redirhome.html';
|
|
76
|
+
const GZIP_CANDIDATE_EXTENSIONS = new Set([
|
|
77
|
+
'.html', '.htm', '.css', '.js', '.mjs', '.json', '.svg', '.xml', '.txt', '.map'
|
|
78
|
+
]);
|
|
79
79
|
// ----------------------------------------------------------------------
|
|
80
80
|
// 3. lwIP Helpers
|
|
81
81
|
// ----------------------------------------------------------------------
|
|
@@ -92,6 +92,7 @@ function getMimeType(fileName) {
|
|
|
92
92
|
case '.css':
|
|
93
93
|
return 'text/css';
|
|
94
94
|
case '.js':
|
|
95
|
+
case '.mjs':
|
|
95
96
|
return 'application/javascript';
|
|
96
97
|
case '.png':
|
|
97
98
|
return 'image/png';
|
|
@@ -106,7 +107,10 @@ function getMimeType(fileName) {
|
|
|
106
107
|
case '.xml':
|
|
107
108
|
return 'text/xml';
|
|
108
109
|
case '.json':
|
|
110
|
+
case '.map':
|
|
109
111
|
return 'application/json';
|
|
112
|
+
case '.svg':
|
|
113
|
+
return 'image/svg+xml';
|
|
110
114
|
default:
|
|
111
115
|
return 'text/plain';
|
|
112
116
|
}
|
|
@@ -124,7 +128,7 @@ function inetChksum(buf) {
|
|
|
124
128
|
sum = (sum & 0xFFFF) + (sum >> 16);
|
|
125
129
|
return (~sum) & 0xFFFF;
|
|
126
130
|
}
|
|
127
|
-
function generateHttpHeaders(fileName, dataLength, opts) {
|
|
131
|
+
function generateHttpHeaders(fileName, dataLength, opts, contentEncoding) {
|
|
128
132
|
const parts = [];
|
|
129
133
|
const ccVersion = getPackageVersion();
|
|
130
134
|
const addPart = (str) => { parts.push({ str, buf: Buffer.from(str, 'ascii') }); };
|
|
@@ -154,21 +158,29 @@ function generateHttpHeaders(fileName, dataLength, opts) {
|
|
|
154
158
|
else
|
|
155
159
|
addPart(`Connection: close\r\n`);
|
|
156
160
|
}
|
|
157
|
-
|
|
161
|
+
if (contentEncoding === 'gzip') {
|
|
162
|
+
addPart(`Content-type: ${getMimeType(fileName)}\r\n`);
|
|
163
|
+
addPart('Content-Encoding: gzip\r\n');
|
|
164
|
+
addPart('\r\n');
|
|
165
|
+
}
|
|
166
|
+
else
|
|
167
|
+
addPart(`Content-type: ${getMimeType(fileName)}\r\n\r\n`);
|
|
158
168
|
return { parts, totalBuffer: Buffer.concat(parts.map(p => p.buf)) };
|
|
159
169
|
}
|
|
160
|
-
function getFilesRecursive(dir, processSubs) {
|
|
170
|
+
function getFilesRecursive(dir, processSubs, sortEntries) {
|
|
161
171
|
const results = [];
|
|
162
172
|
if (!fs.existsSync(dir))
|
|
163
173
|
return results;
|
|
164
174
|
const list = fs.readdirSync(dir);
|
|
175
|
+
if (sortEntries)
|
|
176
|
+
list.sort();
|
|
165
177
|
for (const file of list) {
|
|
166
178
|
const fullPath = path.join(dir, file);
|
|
167
179
|
const stat = fs.statSync(fullPath);
|
|
168
180
|
if (!stat.isDirectory())
|
|
169
181
|
results.push(fullPath);
|
|
170
182
|
else if (processSubs)
|
|
171
|
-
results.push(...getFilesRecursive(fullPath, processSubs));
|
|
183
|
+
results.push(...getFilesRecursive(fullPath, processSubs, sortEntries));
|
|
172
184
|
}
|
|
173
185
|
return results;
|
|
174
186
|
}
|
|
@@ -188,28 +200,106 @@ function bufferToHexCArray(buf) {
|
|
|
188
200
|
out += '\n';
|
|
189
201
|
return out;
|
|
190
202
|
}
|
|
203
|
+
function gzipDeterministic(content, gzipLevel) {
|
|
204
|
+
const compressed = gzipSync(content, { level: gzipLevel });
|
|
205
|
+
// RFC 1952 mtime and OS fields: avoid build-time and host-specific metadata.
|
|
206
|
+
compressed.fill(0, 4, 8);
|
|
207
|
+
compressed[9] = 0xff;
|
|
208
|
+
return compressed;
|
|
209
|
+
}
|
|
210
|
+
async function optimizeContent(filePath, relativePath, content, minifyOpts, opts) {
|
|
211
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
212
|
+
if (ext === '.svg' && opts.optimizeSvg !== false) {
|
|
213
|
+
try {
|
|
214
|
+
const optimizedSvg = optimize(content.toString('utf8'), {
|
|
215
|
+
path: filePath,
|
|
216
|
+
multipass: opts.svgoMultipass ?? false
|
|
217
|
+
});
|
|
218
|
+
const optimizedContent = Buffer.from(optimizedSvg.data, 'utf8');
|
|
219
|
+
console.log(`🖼️ Optimized SVG: ${relativePath} (${content.length} -> ${optimizedContent.length} bytes)`);
|
|
220
|
+
return optimizedContent;
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
224
|
+
throw new Error(`Failed to optimize SVG ${relativePath}: ${message}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (['.html', '.htm'].includes(ext)) {
|
|
228
|
+
const minifiedHtml = await minify(content.toString('utf8'), minifyOpts);
|
|
229
|
+
if (typeof minifiedHtml === 'string') {
|
|
230
|
+
const optimizedContent = Buffer.from(minifiedHtml, 'utf8');
|
|
231
|
+
console.log(`📦 Minified HTML: ${relativePath} (${content.length} -> ${optimizedContent.length} bytes)`);
|
|
232
|
+
return optimizedContent;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (ext === '.css' && minifyOpts.minifyCSS) {
|
|
236
|
+
try {
|
|
237
|
+
const optimizedCss = transformCss({
|
|
238
|
+
filename: filePath,
|
|
239
|
+
code: content,
|
|
240
|
+
minify: true
|
|
241
|
+
});
|
|
242
|
+
const optimizedContent = Buffer.from(optimizedCss.code);
|
|
243
|
+
console.log(`🎨 Minified CSS: ${relativePath} (${content.length} -> ${optimizedContent.length} bytes)`);
|
|
244
|
+
return optimizedContent;
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
248
|
+
throw new Error(`Failed to minify CSS ${relativePath}: ${message}`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
if (['.js', '.mjs'].includes(ext) && minifyOpts.minifyJS) {
|
|
252
|
+
try {
|
|
253
|
+
const optimizedJs = await transformJavaScript(content.toString('utf8'), {
|
|
254
|
+
loader: 'js',
|
|
255
|
+
minify: true,
|
|
256
|
+
legalComments: 'none',
|
|
257
|
+
sourcefile: filePath
|
|
258
|
+
});
|
|
259
|
+
const optimizedContent = Buffer.from(optimizedJs.code, 'utf8');
|
|
260
|
+
console.log(`📦 Minified JavaScript: ${relativePath} (${content.length} -> ${optimizedContent.length} bytes)`);
|
|
261
|
+
return optimizedContent;
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
265
|
+
throw new Error(`Failed to minify JavaScript ${relativePath}: ${message}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
console.log(`📄 Copied: ${relativePath}`);
|
|
269
|
+
return content;
|
|
270
|
+
}
|
|
191
271
|
export async function runMakeFsData(opts) {
|
|
192
272
|
console.log(`🚀 CC4EmbeddedSystem V3: Starting makefsdata compilation...`);
|
|
193
|
-
//
|
|
273
|
+
// Check source directory before making any output directory changes.
|
|
194
274
|
if (!fs.existsSync(opts.inputDir)) {
|
|
195
|
-
|
|
196
|
-
|
|
275
|
+
throw new Error(`Input directory not found: ${path.resolve(opts.inputDir)}`);
|
|
276
|
+
}
|
|
277
|
+
if (!fs.statSync(opts.inputDir).isDirectory())
|
|
278
|
+
throw new Error(`Input path is not a directory: ${path.resolve(opts.inputDir)}`);
|
|
279
|
+
if (fs.existsSync(opts.outputFile) && fs.statSync(opts.outputFile).isDirectory()) {
|
|
280
|
+
opts.outputFile = path.join(opts.outputFile, 'fsdata.c');
|
|
281
|
+
console.log(`🔧 Output directory selected. Using default file -> ${opts.outputFile}`);
|
|
197
282
|
}
|
|
198
|
-
if (!opts.outputFile.toLowerCase().endsWith('.c')) {
|
|
283
|
+
else if (!opts.outputFile.toLowerCase().endsWith('.c')) {
|
|
199
284
|
opts.outputFile += '.c';
|
|
200
285
|
console.log(`🔧 Auto-appended '.c' to output file -> ${opts.outputFile}`);
|
|
201
286
|
}
|
|
202
287
|
const outDir = path.dirname(opts.outputFile);
|
|
203
288
|
if (!fs.existsSync(outDir))
|
|
204
289
|
fs.mkdirSync(outDir, { recursive: true });
|
|
205
|
-
const
|
|
290
|
+
const gzipEnabled = opts.gzip ?? false;
|
|
291
|
+
const gzipLevel = opts.gzipLevel ?? DEFAULT_GZIP_LEVEL;
|
|
292
|
+
if (!isGzipLevel(gzipLevel))
|
|
293
|
+
throw new Error('gzipLevel must be an integer from 1 to 9.');
|
|
294
|
+
const allFiles = getFilesRecursive(opts.inputDir, opts.processSubs, gzipEnabled);
|
|
206
295
|
const fileEntries = [];
|
|
207
296
|
let totalOriginalSize = 0;
|
|
208
297
|
let totalCompressedSize = 0;
|
|
298
|
+
let totalStoredSize = 0;
|
|
299
|
+
let gzipFilesCount = 0;
|
|
209
300
|
if (allFiles.length === 0)
|
|
210
301
|
throw new Error(`Input directory is empty! Please put your web files (.html, .css, etc.) into:\n${path.resolve(opts.inputDir)}`);
|
|
211
|
-
|
|
212
|
-
const activeCompressOpts = opts.minifyOpts || COMPRESS_OPTS_DEFAULT;
|
|
302
|
+
const activeCompressOpts = opts.minifyOpts ?? DEFAULT_HTML_MINIFY_OPTIONS;
|
|
213
303
|
for (let i = 0; i < allFiles.length; i++) {
|
|
214
304
|
const filePath = allFiles[i];
|
|
215
305
|
const relativePath = '/' + path.relative(opts.inputDir, filePath).replace(/\\/g, '/');
|
|
@@ -217,18 +307,29 @@ export async function runMakeFsData(opts) {
|
|
|
217
307
|
let content = fs.readFileSync(filePath);
|
|
218
308
|
const originalFileSize = content.length;
|
|
219
309
|
totalOriginalSize += originalFileSize;
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
310
|
+
content = await optimizeContent(filePath, relativePath, content, activeCompressOpts, opts);
|
|
311
|
+
const rawContentLength = content.length;
|
|
312
|
+
totalCompressedSize += rawContentLength;
|
|
313
|
+
let contentEncoding;
|
|
314
|
+
let compressionNote;
|
|
315
|
+
if (gzipEnabled && GZIP_CANDIDATE_EXTENSIONS.has(ext)) {
|
|
316
|
+
const gzipContent = gzipDeterministic(content, gzipLevel);
|
|
317
|
+
if (gzipContent.length < content.length) {
|
|
318
|
+
content = gzipContent;
|
|
319
|
+
contentEncoding = 'gzip';
|
|
320
|
+
gzipFilesCount++;
|
|
321
|
+
const savedSize = rawContentLength - content.length;
|
|
322
|
+
const savedPercent = ((savedSize / rawContentLength) * 100).toFixed(1);
|
|
323
|
+
compressionNote = `gzip level ${gzipLevel}, original ${rawContentLength} B, stored ${content.length} B, saved ${savedSize} B (${savedPercent}%)`;
|
|
225
324
|
}
|
|
325
|
+
else
|
|
326
|
+
compressionNote = `raw, original ${rawContentLength} B, gzip was not smaller`;
|
|
226
327
|
}
|
|
227
|
-
else
|
|
228
|
-
|
|
229
|
-
|
|
328
|
+
else if (gzipEnabled)
|
|
329
|
+
compressionNote = 'raw, not a gzip candidate';
|
|
330
|
+
totalStoredSize += content.length;
|
|
230
331
|
// generate header
|
|
231
|
-
const headerData = opts.includeHttpHeader ? generateHttpHeaders(filePath, content.length, opts) : { parts: [], totalBuffer: Buffer.alloc(0) };
|
|
332
|
+
const headerData = opts.includeHttpHeader ? generateHttpHeaders(filePath, content.length, opts, contentEncoding) : { parts: [], totalBuffer: Buffer.alloc(0) };
|
|
232
333
|
const nameBuffer = Buffer.from(relativePath + '\0', 'utf8');
|
|
233
334
|
const varName = relativePath.replace(/[^A-Za-z0-9]/g, '_');
|
|
234
335
|
const chksums = [];
|
|
@@ -244,7 +345,21 @@ export async function runMakeFsData(opts) {
|
|
|
244
345
|
offset += chunkLen;
|
|
245
346
|
}
|
|
246
347
|
}
|
|
247
|
-
|
|
348
|
+
const fileEntry = {
|
|
349
|
+
varName,
|
|
350
|
+
pathName: relativePath,
|
|
351
|
+
nameBuffer,
|
|
352
|
+
headerParts: headerData.parts,
|
|
353
|
+
headerTotalBuffer: headerData.totalBuffer,
|
|
354
|
+
contentBuffer: content,
|
|
355
|
+
chksums,
|
|
356
|
+
rawContentLength
|
|
357
|
+
};
|
|
358
|
+
if (contentEncoding)
|
|
359
|
+
fileEntry.contentEncoding = contentEncoding;
|
|
360
|
+
if (compressionNote)
|
|
361
|
+
fileEntry.compressionNote = compressionNote;
|
|
362
|
+
fileEntries.push(fileEntry);
|
|
248
363
|
}
|
|
249
364
|
// find real redirhome.html
|
|
250
365
|
const redirhomeIndex = fileEntries.findIndex(file => file.pathName.toLowerCase() === REDIRHOME_PATH);
|
|
@@ -272,6 +387,8 @@ export async function runMakeFsData(opts) {
|
|
|
272
387
|
}
|
|
273
388
|
// generate data arrays
|
|
274
389
|
for (const file of orderedFileEntries) {
|
|
390
|
+
if (file.compressionNote)
|
|
391
|
+
cOutput += `/* ${file.pathName}: ${file.compressionNote} */\n`;
|
|
275
392
|
cOutput += `static const unsigned int dummy_align_${file.varName} = 0;\n`;
|
|
276
393
|
cOutput += `static const unsigned char data_${file.varName}[] = {\n`;
|
|
277
394
|
cOutput += `\t/* ${file.pathName} (${file.nameBuffer.length} chars) */\n`;
|
|
@@ -286,7 +403,8 @@ export async function runMakeFsData(opts) {
|
|
|
286
403
|
cOutput += bufferToHexCArray(part.buf);
|
|
287
404
|
}
|
|
288
405
|
}
|
|
289
|
-
|
|
406
|
+
const contentDescription = file.contentEncoding === 'gzip' ? 'gzip file data' : 'raw file data';
|
|
407
|
+
cOutput += `\n\t/* ${contentDescription} (${file.contentBuffer.length} bytes) */\n`;
|
|
290
408
|
cOutput += bufferToHexCArray(file.contentBuffer);
|
|
291
409
|
cOutput += `};\n\n\n\n`;
|
|
292
410
|
}
|
|
@@ -327,8 +445,10 @@ export async function runMakeFsData(opts) {
|
|
|
327
445
|
return {
|
|
328
446
|
originalSize: totalOriginalSize,
|
|
329
447
|
compressedSize: totalCompressedSize,
|
|
448
|
+
storedSize: totalStoredSize,
|
|
330
449
|
convertedSize: convertedSize,
|
|
331
|
-
filesCount: orderedFileEntries.length
|
|
450
|
+
filesCount: orderedFileEntries.length,
|
|
451
|
+
gzipFilesCount
|
|
332
452
|
};
|
|
333
453
|
}
|
|
334
454
|
throw new Error("No files processed.");
|