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/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 { fileURLToPath } from 'node:url';
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
- import { execFile } from 'node:child_process';
68
- import os from 'node:os';
69
- // prevent socket errors
70
- process.on('uncaughtException', (err) => {
71
- if (err.code === 'EADDRINUSE')
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 buildPowerShellArgs = (script) => {
112
- const encodedCommand = Buffer.from(script, 'utf16le').toString('base64');
113
- return [
114
- '-NoProfile',
115
- '-STA',
116
- '-EncodedCommand',
117
- encodedCommand
118
- ];
119
- };
120
- const closeBrowseProcess = () => {
121
- if (activeBrowseProcess && !activeBrowseProcess.killed) {
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: inputPath,
192
- outputFile: outputPath,
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: 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
- // get stats
202
- const stats = await runMakeFsData(opts);
203
- try {
204
- await open(path.dirname(path.resolve(outputPath)));
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
- }, 100);
242
- });
243
- app.get('/api/browse', async (req, res) => {
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
- const isDir = req.query.type === 'dir';
252
- const platform = os.platform();
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
- else if (platform === 'darwin') {
277
- result = await runDialogCommand('osascript', [
278
- '-e',
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
- else {
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 (e) {
292
- res.json({ success: true, path: null });
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
- // main run
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
+ };
@@ -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
- addPart(`Content-type: ${getMimeType(fileName)}\r\n\r\n`);
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
- // checkers
273
+ // Check source directory before making any output directory changes.
194
274
  if (!fs.existsSync(opts.inputDir)) {
195
- console.log(`⚠️ Input directory not found. Auto-creating directory: \n${opts.inputDir}`);
196
- fs.mkdirSync(opts.inputDir, { recursive: true });
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 allFiles = getFilesRecursive(opts.inputDir, opts.processSubs);
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
- // html-minifier-next options
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
- if (['.html', '.htm', '.css', '.js'].includes(ext)) {
221
- const minifiedStr = await minify(content.toString('utf8'), activeCompressOpts);
222
- if (typeof minifiedStr === 'string') {
223
- content = Buffer.from(minifiedStr, 'utf8');
224
- console.log(`📦 Minified: ${relativePath} (${originalFileSize} -> ${content.length} bytes)`);
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
- console.log(`📄 Copied: ${relativePath}`);
229
- totalCompressedSize += content.length;
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
- fileEntries.push({ varName, pathName: relativePath, nameBuffer, headerParts: headerData.parts, headerTotalBuffer: headerData.totalBuffer, contentBuffer: content, chksums });
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
- cOutput += `\n\t/* raw file data (${file.contentBuffer.length} bytes) */\n`;
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.");