cc4-embedded-system 3.1.9 → 3.2.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 CHANGED
@@ -1,20 +1,37 @@
1
1
  # CC4EmbeddedSystem V3
2
- - This version based on [html-minifier-next](https://github.com/j9t/html-minifier-next) and rewrite [lwIP makefsdata](https://github.com/m-labs/lwip/tree/master/src/apps/httpd/makefsdata), run on localhost, default port: ```3000```.
2
+ - This version is based on [html-minifier-next](https://github.com/j9t/html-minifier-next), [SVGO](https://github.com/svg/svgo), and a rewrite of [lwIP makefsdata](https://github.com/m-labs/lwip/tree/master/src/apps/httpd/makefsdata). GUI mode runs on localhost, default port: ```3000```.
3
3
  - This tool is deployed on [npm package](https://www.npmjs.com/package/cc4-embedded-system).
4
+ - The last successful source and destination paths are automatically saved as `cc4es_configs.json` in the operating system's local application-data directory. On Windows this is `%LOCALAPPDATA%\\cc4-embedded-system\\cc4es_configs.json`.
5
+ - The config file is created only after the first successful build, and its complete path is printed at that time. Use `cc4es --config-path` to print it later.
4
6
 
5
- ![](./Screenshot/v3.1.9.png)
7
+ ## Demo
8
+ - Select input directory
9
+
10
+ ![](./Screenshot/inputs.png)
11
+ - Select output file path
12
+
13
+ ![](./Screenshot/output.png)
14
+ - Result
15
+
16
+ ![](./Screenshot/v3.1.10.png)
6
17
 
7
18
  ## Structure
8
19
  ```text
9
20
  CC4EmbeddedSystem/
10
21
  ├── src/
11
- │ ├── gui.ts // Express server & CLI entry point
12
- ├── utils.ts // To get latest tool version
13
- └── makefsdata.ts // Core C code generation & minification logic
22
+ │ ├── gui.ts // CLI entry point
23
+ │ ├── cli.ts // Command-line parsing and help text
24
+ ├── server.ts // Express GUI server
25
+ │ ├── config.ts // Persistent last-build paths
26
+ │ ├── minify-options.ts // Shared HTML minifier options
27
+ │ ├── makefsdata.ts // Core C code generation and minification logic
28
+ │ └── utils.ts // Package version lookup
14
29
  ├── public/
15
30
  │ └── index.html // Web GUI dashboard
16
31
  ├── Sereenshot/
17
- └── v3.1.9.png // Demo image
32
+ ├── inputs.png // Demo image
33
+ │ ├── output.png // Demo image
34
+ │ └── v3.1.10.png // Demo image
18
35
  ├── node_modules/ // Required submodules during development
19
36
  ├── dist/ // Compiled JavaScript output (Auto-generated)
20
37
  ├── package.json // Project configuration & dependencies
@@ -23,6 +40,33 @@ CC4EmbeddedSystem/
23
40
  ```
24
41
 
25
42
  ## Commands
43
+ ### CLI Help Texts
44
+ ```text
45
+ Usage: cc4es [options]
46
+
47
+ Without options, starts the browser GUI.
48
+
49
+ -h, --help Show this help and exit
50
+ -V, --version Show version and exit
51
+ --config-path Print the full config file path and exit
52
+ --headless Build without server or browser
53
+ -s, --src <directory> Source directory (headless)
54
+ -d, --dst <file|directory> Output C file or directory (default: fsdata.c)
55
+ -p, --port <1-65535> GUI server port
56
+ --[no-]optimize-svg Enable or disable SVGO (default: enabled)
57
+ --svgo-multipass Run SVGO optimization repeatedly
58
+
59
+ HTML compression options:
60
+ --[no-]collapse-whitespace --[no-]remove-comments
61
+ --[no-]minify-js --[no-]minify-css
62
+ --[no-]conditional-comments --[no-]decode-entities
63
+ --[no-]remove-attribute-quotes --[no-]remove-empty-attributes
64
+ --[no-]remove-redundant-attributes --[no-]use-short-doctype
65
+
66
+ Headless mode requires --src or --dst and never reruns the last build with
67
+ no path options. A missing counterpart uses the last saved path; if no
68
+ destination is saved, fsdata.c is created in the current working directory.
69
+ ```
26
70
  ### Normal Use
27
71
  - Global installation
28
72
  ```bash
@@ -37,6 +81,33 @@ CC4EmbeddedSystem/
37
81
  ```bash
38
82
  cc4es --port 3002
39
83
  ```
84
+ - Headless build (does not start a server or open a browser)
85
+ ```bash
86
+ cc4es --headless --src C:\Project\web --dst C:\Project\fsdata.c
87
+ ```
88
+ - Headless build with the default output filename. An existing output directory uses `fsdata.c` inside it.
89
+ ```bash
90
+ cc4es --headless --src C:\Project\web
91
+ cc4es --headless --src C:\Project\web --dst C:\Project\output
92
+ ```
93
+ - Headless safety: a parameterless command stops instead of rerunning the last build
94
+ ```bash
95
+ cc4es --headless
96
+ ```
97
+ - CLI help and version
98
+ ```bash
99
+ cc4es --help
100
+ cc4es --version
101
+ cc4es --config-path
102
+ ```
103
+ - Compression flags can be supplied in headless mode. Every flag accepts a `--no-` form.
104
+ ```bash
105
+ cc4es --headless --no-minify-js --remove-attribute-quotes --svgo-multipass
106
+ ```
107
+ - Update
108
+ ```bash
109
+ npm install -g cc4-embedded-system@latest
110
+ ```
40
111
  ### Development
41
112
  ```bash
42
113
  # build
@@ -46,9 +117,14 @@ npm run build
46
117
  npm link
47
118
  cc4es
48
119
 
120
+ # headless development test
121
+ npm run dev -- --headless --src C:\Project\web --dst C:\Project\fsdata.c
122
+
49
123
  # publish
124
+ npm whoami # verify
50
125
  npm login
51
- # npm version patch # if increase version
52
- npm run build
126
+ npm version patch --no-git-tag-version # if increase version
127
+ npm run dev # dev mode for testing
128
+ npm run build # build the newest
53
129
  npm publish --access public
54
- ```
130
+ ```
package/dist/cli.js ADDED
@@ -0,0 +1,146 @@
1
+ import { DEFAULT_HTML_MINIFY_OPTIONS } from './minify-options.js';
2
+ export class CliUsageError extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = 'CliUsageError';
6
+ }
7
+ }
8
+ const MINIFY_FLAGS = {
9
+ '--collapse-whitespace': 'collapseWhitespace',
10
+ '--no-collapse-whitespace': 'collapseWhitespace',
11
+ '--remove-comments': 'removeComments',
12
+ '--no-remove-comments': 'removeComments',
13
+ '--minify-js': 'minifyJS',
14
+ '--no-minify-js': 'minifyJS',
15
+ '--minify-css': 'minifyCSS',
16
+ '--no-minify-css': 'minifyCSS',
17
+ '--conditional-comments': 'processConditionalComments',
18
+ '--no-conditional-comments': 'processConditionalComments',
19
+ '--decode-entities': 'decodeEntities',
20
+ '--no-decode-entities': 'decodeEntities',
21
+ '--remove-attribute-quotes': 'removeAttributeQuotes',
22
+ '--no-remove-attribute-quotes': 'removeAttributeQuotes',
23
+ '--remove-empty-attributes': 'removeEmptyAttributes',
24
+ '--no-remove-empty-attributes': 'removeEmptyAttributes',
25
+ '--remove-redundant-attributes': 'removeRedundantAttributes',
26
+ '--no-remove-redundant-attributes': 'removeRedundantAttributes',
27
+ '--use-short-doctype': 'useShortDoctype',
28
+ '--no-use-short-doctype': 'useShortDoctype'
29
+ };
30
+ const getRequiredValue = (args, index, option) => {
31
+ const value = args[index + 1];
32
+ if (!value || value.startsWith('-'))
33
+ throw new CliUsageError(`${option} requires a value.`);
34
+ return value;
35
+ };
36
+ const parsePort = (value) => {
37
+ if (!/^\d+$/.test(value))
38
+ throw new CliUsageError('--port must be an integer from 1 to 65535.');
39
+ const port = Number.parseInt(value, 10);
40
+ if (port < 1 || port > 65535)
41
+ throw new CliUsageError('--port must be an integer from 1 to 65535.');
42
+ return port;
43
+ };
44
+ export const parseCliArguments = (args) => {
45
+ const parsed = {
46
+ showHelp: false,
47
+ showVersion: false,
48
+ showConfigPath: false,
49
+ headless: false,
50
+ minifyOpts: { ...DEFAULT_HTML_MINIFY_OPTIONS },
51
+ optimizeSvg: true,
52
+ svgoMultipass: false
53
+ };
54
+ for (let index = 0; index < args.length; index++) {
55
+ const argument = args[index];
56
+ if (!argument)
57
+ continue;
58
+ const minifyOption = MINIFY_FLAGS[argument];
59
+ if (minifyOption) {
60
+ parsed.minifyOpts[minifyOption] = !argument.startsWith('--no-');
61
+ continue;
62
+ }
63
+ switch (argument) {
64
+ case '-h':
65
+ case '--help':
66
+ parsed.showHelp = true;
67
+ break;
68
+ case '-V':
69
+ case '--version':
70
+ parsed.showVersion = true;
71
+ break;
72
+ case '--config-path':
73
+ parsed.showConfigPath = true;
74
+ break;
75
+ case '--headless':
76
+ parsed.headless = true;
77
+ break;
78
+ case '-s':
79
+ case '--src':
80
+ case '--input':
81
+ parsed.inputPath = getRequiredValue(args, index, argument);
82
+ index++;
83
+ break;
84
+ case '-d':
85
+ case '--dst':
86
+ case '--output':
87
+ parsed.outputPath = getRequiredValue(args, index, argument);
88
+ index++;
89
+ break;
90
+ case '-p':
91
+ case '--port':
92
+ parsed.port = parsePort(getRequiredValue(args, index, argument));
93
+ index++;
94
+ break;
95
+ case '--optimize-svg':
96
+ parsed.optimizeSvg = true;
97
+ break;
98
+ case '--no-optimize-svg':
99
+ parsed.optimizeSvg = false;
100
+ break;
101
+ case '--svgo-multipass':
102
+ parsed.svgoMultipass = true;
103
+ break;
104
+ default:
105
+ throw new CliUsageError(`Unknown option: ${argument}`);
106
+ }
107
+ }
108
+ if (parsed.headless && parsed.port !== undefined) {
109
+ throw new CliUsageError('--port is only available when starting the GUI.');
110
+ }
111
+ if (parsed.headless && !parsed.inputPath && !parsed.outputPath) {
112
+ throw new CliUsageError('Headless mode requires --src or --dst. It will not automatically rerun the last build.');
113
+ }
114
+ if (!parsed.headless && (parsed.inputPath || parsed.outputPath)) {
115
+ throw new CliUsageError('--src and --dst require --headless.');
116
+ }
117
+ return parsed;
118
+ };
119
+ export const getHelpText = () => {
120
+ return [
121
+ 'Usage: cc4es [options]',
122
+ '',
123
+ 'Without options, starts the browser GUI.',
124
+ '',
125
+ ' -h, --help Show this help and exit',
126
+ ' -V, --version Show version and exit',
127
+ ' --config-path Print the full config file path and exit',
128
+ ' --headless Build without server or browser',
129
+ ' -s, --src <directory> Source directory (headless)',
130
+ ' -d, --dst <file|directory> Output C file or directory (default: fsdata.c)',
131
+ ' -p, --port <1-65535> GUI server port',
132
+ ' --[no-]optimize-svg Enable or disable SVGO (default: enabled)',
133
+ ' --svgo-multipass Run SVGO optimization repeatedly',
134
+ '',
135
+ 'HTML compression options:',
136
+ ' --[no-]collapse-whitespace --[no-]remove-comments',
137
+ ' --[no-]minify-js --[no-]minify-css',
138
+ ' --[no-]conditional-comments --[no-]decode-entities',
139
+ ' --[no-]remove-attribute-quotes --[no-]remove-empty-attributes',
140
+ ' --[no-]remove-redundant-attributes --[no-]use-short-doctype',
141
+ '',
142
+ 'Headless mode requires --src or --dst and never reruns the last build with',
143
+ 'no path options. A missing counterpart uses the last saved path; if no',
144
+ 'destination is saved, fsdata.c is created in the current working directory.'
145
+ ].join('\n');
146
+ };
package/dist/config.js ADDED
@@ -0,0 +1,71 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ export const CONFIG_FILE_NAME = 'cc4es_configs.json';
5
+ const getStateDirectory = () => {
6
+ const homeDirectory = os.homedir();
7
+ if (process.platform === 'win32') {
8
+ return process.env.LOCALAPPDATA ?? path.join(homeDirectory, 'AppData', 'Local');
9
+ }
10
+ if (process.platform === 'darwin') {
11
+ return path.join(homeDirectory, 'Library', 'Application Support');
12
+ }
13
+ return process.env.XDG_STATE_HOME ?? path.join(homeDirectory, '.local', 'state');
14
+ };
15
+ export const getConfigFilePath = () => {
16
+ return path.join(getStateDirectory(), 'cc4-embedded-system', CONFIG_FILE_NAME);
17
+ };
18
+ const isRecord = (value) => {
19
+ return typeof value === 'object' && value !== null;
20
+ };
21
+ const readConfig = () => {
22
+ try {
23
+ const parsedConfig = JSON.parse(fs.readFileSync(getConfigFilePath(), 'utf8'));
24
+ if (!isRecord(parsedConfig) || !isRecord(parsedConfig.lastBuild))
25
+ return { schemaVersion: 1 };
26
+ const src = parsedConfig.lastBuild.src;
27
+ const dst = parsedConfig.lastBuild.dst;
28
+ if (typeof src !== 'string' || typeof dst !== 'string' || !src || !dst) {
29
+ return { schemaVersion: 1 };
30
+ }
31
+ return {
32
+ schemaVersion: 1,
33
+ lastBuild: { src, dst }
34
+ };
35
+ }
36
+ catch {
37
+ return { schemaVersion: 1 };
38
+ }
39
+ };
40
+ export const getLastBuildPaths = () => {
41
+ return readConfig().lastBuild;
42
+ };
43
+ export const saveLastBuildPaths = (src, dst) => {
44
+ const configFilePath = getConfigFilePath();
45
+ const temporaryFilePath = `${configFilePath}.${process.pid}.tmp`;
46
+ const configAlreadyExists = fs.existsSync(configFilePath);
47
+ const config = {
48
+ schemaVersion: 1,
49
+ lastBuild: { src, dst }
50
+ };
51
+ try {
52
+ fs.mkdirSync(path.dirname(configFilePath), { recursive: true });
53
+ fs.writeFileSync(temporaryFilePath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
54
+ fs.renameSync(temporaryFilePath, configFilePath);
55
+ if (!configAlreadyExists)
56
+ console.log(`📝 Created config: ${configFilePath}`);
57
+ return true;
58
+ }
59
+ catch (error) {
60
+ const message = error instanceof Error ? error.message : String(error);
61
+ console.warn(`⚠️ Could not save ${CONFIG_FILE_NAME}: ${message}`);
62
+ try {
63
+ if (fs.existsSync(temporaryFilePath))
64
+ fs.unlinkSync(temporaryFilePath);
65
+ }
66
+ catch {
67
+ // Ignore cleanup errors because the build itself succeeded.
68
+ }
69
+ return false;
70
+ }
71
+ };
package/dist/gui.js CHANGED
@@ -1,302 +1,64 @@
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, saveLastBuildPaths } from './config.js';
65
5
  import { runMakeFsData } from './makefsdata.js';
6
+ import { startGuiServer } from './server.js';
66
7
  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
- }
8
+ const getDefaultGuiPort = () => {
9
+ const configuredPort = Number.parseInt(process.env.PORT ?? '3000', 10);
10
+ if (configuredPort < 1 || configuredPort > 65535 || Number.isNaN(configuredPort))
11
+ return 3000;
12
+ return configuredPort;
110
13
  };
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
- }
14
+ const runHeadlessBuild = async (inputPath, outputPath, minifyOpts, optimizeSvg, svgoMultipass) => {
15
+ const lastBuild = getLastBuildPaths();
16
+ const selectedInputPath = inputPath ?? lastBuild?.src;
17
+ const selectedOutputPath = outputPath ?? lastBuild?.dst ?? 'fsdata.c';
18
+ if (!selectedInputPath) {
19
+ throw new CliUsageError('Headless mode requires --src, or a source path saved by a previous successful build.');
128
20
  }
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
21
  const opts = {
191
- inputDir: inputPath,
192
- outputFile: outputPath,
22
+ inputDir: path.resolve(selectedInputPath),
23
+ outputFile: path.resolve(selectedOutputPath),
193
24
  processSubs: true,
194
25
  includeHttpHeader: true,
195
26
  useHttp11: false,
196
27
  supportSsi: true,
197
28
  precalcChksum: false,
198
- minifyOpts: minifyOpts
29
+ minifyOpts,
30
+ optimizeSvg,
31
+ svgoMultipass
199
32
  };
33
+ const stats = await runMakeFsData(opts);
34
+ saveLastBuildPaths(opts.inputDir, opts.outputFile);
35
+ console.log(`✨ Build complete: ${stats.filesCount} file(s), ${stats.originalSize} -> ${stats.compressedSize} bytes.`);
36
+ };
37
+ const main = async () => {
200
38
  try {
201
- // get stats
202
- const stats = await runMakeFsData(opts);
203
- try {
204
- await open(path.dirname(path.resolve(outputPath)));
39
+ const args = parseCliArguments(process.argv.slice(2));
40
+ if (args.showHelp) {
41
+ console.log(getHelpText());
42
+ return;
205
43
  }
206
- catch (e) {
207
- // ignore open folder error
44
+ if (args.showVersion) {
45
+ console.log(getPackageVersion());
46
+ return;
208
47
  }
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
- }
48
+ if (args.showConfigPath) {
49
+ console.log(getConfigFilePath());
50
+ return;
240
51
  }
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();
52
+ if (args.headless) {
53
+ await runHeadlessBuild(args.inputPath, args.outputPath, args.minifyOpts, args.optimizeSvg, args.svgoMultipass);
54
+ return;
249
55
  }
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));
275
- }
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
- ]);
283
- }
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 });
56
+ startGuiServer(args.port ?? getDefaultGuiPort());
290
57
  }
291
- catch (e) {
292
- res.json({ success: true, path: null });
58
+ catch (error) {
59
+ const message = error instanceof Error ? error.message : String(error);
60
+ console.error(`❌ ${message}`);
61
+ process.exitCode = 1;
293
62
  }
294
- });
295
- const startServer = () => {
296
- server = app.listen(PORT, () => {
297
- console.log(`✨ GUI Server started on http://localhost:${PORT}`);
298
- });
299
63
  };
300
- // main run
301
- startServer();
302
- open(`http://localhost:${PORT}`);
64
+ void main();
@@ -60,21 +60,14 @@
60
60
  import fs from 'node:fs';
61
61
  import path from 'node:path';
62
62
  import { minify } from 'html-minifier-next';
63
+ import { optimize } from 'svgo';
63
64
  import { getPackageVersion } from './utils.js';
65
+ import { DEFAULT_HTML_MINIFY_OPTIONS } from './minify-options.js';
64
66
  // ----------------------------------------------------------------------
65
67
  // 1. Configs & Version Recovery
66
68
  // ----------------------------------------------------------------------
67
69
  const TCP_MSS = 1460;
68
70
  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
71
  const REDIRHOME_PATH = '/redirhome.html';
79
72
  // ----------------------------------------------------------------------
80
73
  // 3. lwIP Helpers
@@ -107,6 +100,8 @@ function getMimeType(fileName) {
107
100
  return 'text/xml';
108
101
  case '.json':
109
102
  return 'application/json';
103
+ case '.svg':
104
+ return 'image/svg+xml';
110
105
  default:
111
106
  return 'text/plain';
112
107
  }
@@ -190,12 +185,17 @@ function bufferToHexCArray(buf) {
190
185
  }
191
186
  export async function runMakeFsData(opts) {
192
187
  console.log(`🚀 CC4EmbeddedSystem V3: Starting makefsdata compilation...`);
193
- // checkers
188
+ // Check source directory before making any output directory changes.
194
189
  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 });
190
+ throw new Error(`Input directory not found: ${path.resolve(opts.inputDir)}`);
197
191
  }
198
- if (!opts.outputFile.toLowerCase().endsWith('.c')) {
192
+ if (!fs.statSync(opts.inputDir).isDirectory())
193
+ throw new Error(`Input path is not a directory: ${path.resolve(opts.inputDir)}`);
194
+ if (fs.existsSync(opts.outputFile) && fs.statSync(opts.outputFile).isDirectory()) {
195
+ opts.outputFile = path.join(opts.outputFile, 'fsdata.c');
196
+ console.log(`🔧 Output directory selected. Using default file -> ${opts.outputFile}`);
197
+ }
198
+ else if (!opts.outputFile.toLowerCase().endsWith('.c')) {
199
199
  opts.outputFile += '.c';
200
200
  console.log(`🔧 Auto-appended '.c' to output file -> ${opts.outputFile}`);
201
201
  }
@@ -208,8 +208,7 @@ export async function runMakeFsData(opts) {
208
208
  let totalCompressedSize = 0;
209
209
  if (allFiles.length === 0)
210
210
  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;
211
+ const activeCompressOpts = opts.minifyOpts ?? DEFAULT_HTML_MINIFY_OPTIONS;
213
212
  for (let i = 0; i < allFiles.length; i++) {
214
213
  const filePath = allFiles[i];
215
214
  const relativePath = '/' + path.relative(opts.inputDir, filePath).replace(/\\/g, '/');
@@ -217,7 +216,21 @@ export async function runMakeFsData(opts) {
217
216
  let content = fs.readFileSync(filePath);
218
217
  const originalFileSize = content.length;
219
218
  totalOriginalSize += originalFileSize;
220
- if (['.html', '.htm', '.css', '.js'].includes(ext)) {
219
+ if (ext === '.svg' && opts.optimizeSvg !== false) {
220
+ try {
221
+ const optimizedSvg = optimize(content.toString('utf8'), {
222
+ path: filePath,
223
+ multipass: opts.svgoMultipass ?? false
224
+ });
225
+ content = Buffer.from(optimizedSvg.data, 'utf8');
226
+ console.log(`🖼️ Optimized SVG: ${relativePath} (${originalFileSize} -> ${content.length} bytes)`);
227
+ }
228
+ catch (error) {
229
+ const message = error instanceof Error ? error.message : String(error);
230
+ throw new Error(`Failed to optimize SVG ${relativePath}: ${message}`);
231
+ }
232
+ }
233
+ else if (['.html', '.htm', '.css', '.js'].includes(ext)) {
221
234
  const minifiedStr = await minify(content.toString('utf8'), activeCompressOpts);
222
235
  if (typeof minifiedStr === 'string') {
223
236
  content = Buffer.from(minifiedStr, 'utf8');
@@ -0,0 +1,38 @@
1
+ export const DEFAULT_HTML_MINIFY_OPTIONS = {
2
+ collapseWhitespace: true,
3
+ removeComments: true,
4
+ minifyJS: true,
5
+ minifyCSS: true,
6
+ processConditionalComments: true,
7
+ decodeEntities: true,
8
+ removeAttributeQuotes: false,
9
+ removeEmptyAttributes: false,
10
+ removeRedundantAttributes: false,
11
+ useShortDoctype: false
12
+ };
13
+ const HTML_MINIFY_OPTION_KEYS = [
14
+ 'collapseWhitespace',
15
+ 'removeComments',
16
+ 'minifyJS',
17
+ 'minifyCSS',
18
+ 'processConditionalComments',
19
+ 'decodeEntities',
20
+ 'removeAttributeQuotes',
21
+ 'removeEmptyAttributes',
22
+ 'removeRedundantAttributes',
23
+ 'useShortDoctype'
24
+ ];
25
+ const isRecord = (value) => {
26
+ return typeof value === 'object' && value !== null;
27
+ };
28
+ export const normalizeHtmlMinifyOptions = (value) => {
29
+ const normalized = { ...DEFAULT_HTML_MINIFY_OPTIONS };
30
+ if (!isRecord(value))
31
+ return normalized;
32
+ for (const key of HTML_MINIFY_OPTION_KEYS) {
33
+ const optionValue = value[key];
34
+ if (typeof optionValue === 'boolean')
35
+ normalized[key] = optionValue;
36
+ }
37
+ return normalized;
38
+ };
@@ -57,15 +57,15 @@
57
57
  <label>Input Directory</label>
58
58
  <div class="input-row">
59
59
  <input type="text" id="inputPath" placeholder="e.g., C:\Project\web">
60
- <button type="button" class="btn-browse" onclick="handleBrowse('inputPath', 'dir')">Brows ...</button>
60
+ <button type="button" class="btn-browse" onclick="handleBrowse('inputPath', 'dir')">Browse ...</button>
61
61
  </div>
62
62
  </div>
63
63
 
64
64
  <div class="form-group">
65
65
  <label>Output File (fsdata.c)</label>
66
66
  <div class="input-row">
67
- <input type="text" id="outputPath" placeholder="e.g., ./fsdata.c">
68
- <button type="button" class="btn-browse" onclick="handleBrowse('outputPath', 'file')">Brows ...</button>
67
+ <input type="text" id="outputPath" value="fsdata.c" placeholder="e.g., ./fsdata.c">
68
+ <button type="button" class="btn-browse" onclick="handleBrowse('outputPath', 'file')">Browse ...</button>
69
69
  </div>
70
70
  </div>
71
71
 
@@ -94,13 +94,13 @@
94
94
  </div>
95
95
  </details>
96
96
 
97
- <div id="statsPanel" class="stats-panel">
98
- <div class="stats-title">📊 Compression Analysis</div>
99
- <div class="stats-info">
100
- <span>Compressed: <span id="compSize" style="color: #9cdcfe;">0</span> KB</span>
101
- <span>Converted: <span id="convSize" style="color: #dcdcaa;">0</span> KB</span>
102
- <span>Original: <span id="origSize" style="color: #ce9178;">0</span> KB</span>
103
- </div>
97
+ <div id="statsPanel" class="stats-panel">
98
+ <div class="stats-title">📊 Compression Analysis</div>
99
+ <div class="stats-info">
100
+ <span>Compressed: <span id="compSize" style="color: #9cdcfe;">0</span> KB</span>
101
+ <span>Converted: <span id="convSize" style="color: #dcdcaa;">0</span> KB</span>
102
+ <span>Original: <span id="origSize" style="color: #ce9178;">0</span> KB</span>
103
+ </div>
104
104
  <div class="bar-container">
105
105
  <div id="barFill" class="bar-fill"></div>
106
106
  <div id="barText" class="bar-text">0% Saved</div>
@@ -204,15 +204,27 @@
204
204
  }
205
205
  }, true);
206
206
 
207
- window.addEventListener('DOMContentLoaded', async () => {
208
- await fetch('/api/cancel-shutdown', { method: 'POST' });
209
- try {
210
- const res = await fetch('/api/version');
211
- const data = await res.json();
212
- if (data.version) document.getElementById('app-title-text').innerText = `CC4EmbeddedSystem V3 (${data.version})`;
213
- }
214
- catch (error) { console.error('Can\'t get version!', error); }
215
- });
207
+ window.addEventListener('DOMContentLoaded', async () => {
208
+ await fetch('/api/cancel-shutdown', { method: 'POST' });
209
+
210
+ try {
211
+ const res = await fetch('/api/version');
212
+ const data = await res.json();
213
+ if (data.version) document.getElementById('app-title-text').innerText = `CC4EmbeddedSystem V3 (${data.version})`;
214
+ }
215
+ catch (error) { console.error('Can\'t get version!', error); }
216
+
217
+ try {
218
+ const res = await fetch('/api/config');
219
+ const data = await res.json();
220
+
221
+ if (data.lastBuild) {
222
+ document.getElementById('inputPath').value = data.lastBuild.src || '';
223
+ document.getElementById('outputPath').value = data.lastBuild.dst || '';
224
+ }
225
+ }
226
+ catch (error) { console.error('Can\'t load saved paths!', error); }
227
+ });
216
228
 
217
229
  async function changePort() {
218
230
  const newPort = document.getElementById('guiPort').value.trim();
@@ -262,20 +274,20 @@
262
274
  const panel = document.getElementById('statsPanel');
263
275
  panel.style.display = 'block';
264
276
 
265
- const { originalSize, compressedSize, convertedSize } = result.stats;
266
- const origKB = (originalSize / 1024).toFixed(2);
267
- const compKB = (compressedSize / 1024).toFixed(2);
268
- const convKB = (convertedSize / 1024).toFixed(2);
277
+ const { originalSize, compressedSize, convertedSize } = result.stats;
278
+ const origKB = (originalSize / 1024).toFixed(2);
279
+ const compKB = (compressedSize / 1024).toFixed(2);
280
+ const convKB = (convertedSize / 1024).toFixed(2);
269
281
 
270
282
  // ratio: the saved percentage
271
283
  const ratio = originalSize > 0 ? (((originalSize - compressedSize) / originalSize) * 100).toFixed(1) : 0;
272
284
  // compressedRatio: the remainings percentage
273
285
  const compressedRatio = (100 - ratio).toFixed(1);
274
286
 
275
- document.getElementById('origSize').innerText = origKB;
276
- document.getElementById('compSize').innerText = compKB;
277
- document.getElementById('convSize').innerText = convKB;
278
- document.getElementById('barText').innerText = `${ratio}% Size Reduced`;
287
+ document.getElementById('origSize').innerText = origKB;
288
+ document.getElementById('compSize').innerText = compKB;
289
+ document.getElementById('convSize').innerText = convKB;
290
+ document.getElementById('barText').innerText = `${ratio}% Size Reduced`;
279
291
 
280
292
  // actual used percentage
281
293
  setTimeout(() => {
@@ -317,4 +329,4 @@
317
329
  </div>
318
330
 
319
331
  </body>
320
- </html>
332
+ </html>
package/dist/server.js ADDED
@@ -0,0 +1,262 @@
1
+ import express from 'express';
2
+ import { execFile } from 'node:child_process';
3
+ import fs from 'node:fs';
4
+ import os from 'node:os';
5
+ import path from 'node:path';
6
+ import { fileURLToPath } from 'node:url';
7
+ import open from 'open';
8
+ import { getLastBuildPaths, saveLastBuildPaths } from './config.js';
9
+ import { runMakeFsData } from './makefsdata.js';
10
+ import { normalizeHtmlMinifyOptions } from './minify-options.js';
11
+ import { getPackageVersion } from './utils.js';
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = path.dirname(__filename);
14
+ const buildPowerShellArgs = (script) => {
15
+ const encodedCommand = Buffer.from(script, 'utf16le').toString('base64');
16
+ return [
17
+ '-NoProfile',
18
+ '-STA',
19
+ '-EncodedCommand',
20
+ encodedCommand
21
+ ];
22
+ };
23
+ const buildWindowsDialogScript = (dialogLines) => {
24
+ return [
25
+ 'Add-Type -AssemblyName System.Windows.Forms',
26
+ 'Add-Type -AssemblyName System.Drawing',
27
+ '$screen = [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea',
28
+ '$owner = New-Object System.Windows.Forms.Form',
29
+ '$owner.StartPosition = [System.Windows.Forms.FormStartPosition]::Manual',
30
+ '$owner.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::None',
31
+ '$owner.ShowInTaskbar = $false',
32
+ '$owner.TopMost = $true',
33
+ '$owner.Width = 1',
34
+ '$owner.Height = 1',
35
+ '$owner.Left = $screen.Left + [int]($screen.Width / 2)',
36
+ '$owner.Top = $screen.Top + [int]($screen.Height / 2)',
37
+ '$owner.Opacity = 0',
38
+ '$null = $owner.Show()',
39
+ '$null = $owner.Activate()',
40
+ '$owner.BringToFront()',
41
+ '[System.Windows.Forms.Application]::DoEvents()',
42
+ ...dialogLines,
43
+ '$owner.Close()',
44
+ '$owner.Dispose()'
45
+ ].join('\n');
46
+ };
47
+ const isNonEmptyString = (value) => {
48
+ return typeof value === 'string' && value.trim().length > 0;
49
+ };
50
+ export const startGuiServer = (initialPort) => {
51
+ const app = express();
52
+ let port = initialPort;
53
+ let activeBrowseProcess = null;
54
+ let server = null;
55
+ let shutdownTimer = null;
56
+ const publicPath = fs.existsSync(path.join(__dirname, 'public'))
57
+ ? path.join(__dirname, 'public')
58
+ : path.join(__dirname, '../public');
59
+ const closeBrowseProcess = () => {
60
+ if (activeBrowseProcess && !activeBrowseProcess.killed) {
61
+ try {
62
+ activeBrowseProcess.kill();
63
+ }
64
+ catch {
65
+ // Ignore child cleanup errors.
66
+ }
67
+ }
68
+ activeBrowseProcess = null;
69
+ };
70
+ const cancelShutdown = () => {
71
+ if (!shutdownTimer)
72
+ return;
73
+ clearTimeout(shutdownTimer);
74
+ shutdownTimer = null;
75
+ };
76
+ const scheduleShutdown = () => {
77
+ if (shutdownTimer)
78
+ return;
79
+ shutdownTimer = setTimeout(() => {
80
+ closeBrowseProcess();
81
+ console.log('👋 Window closed, shutting down ...');
82
+ process.exit(0);
83
+ }, 2000);
84
+ };
85
+ const runDialogCommand = (command, args) => {
86
+ return new Promise((resolve, reject) => {
87
+ const child = execFile(command, args, {
88
+ windowsHide: false,
89
+ maxBuffer: 1024 * 1024
90
+ }, (error, stdout, stderr) => {
91
+ if (activeBrowseProcess === child)
92
+ activeBrowseProcess = null;
93
+ if (error) {
94
+ reject(new Error(stderr.trim() || error.message));
95
+ return;
96
+ }
97
+ resolve(stdout.trim());
98
+ });
99
+ activeBrowseProcess = child;
100
+ });
101
+ };
102
+ const startServer = (openBrowser) => {
103
+ server = app.listen(port, () => {
104
+ const serverUrl = `http://localhost:${port}`;
105
+ console.log(`✨ GUI Server started on ${serverUrl}`);
106
+ if (openBrowser)
107
+ void open(serverUrl).catch(() => undefined);
108
+ });
109
+ server.on('error', (error) => {
110
+ if (error.code === 'EADDRINUSE') {
111
+ console.error(`❌ Port ${port} is already in use.`);
112
+ process.exitCode = 1;
113
+ return;
114
+ }
115
+ console.error(`❌ GUI server error: ${error.message}`);
116
+ process.exitCode = 1;
117
+ });
118
+ };
119
+ app.use(express.json());
120
+ app.use(express.static(publicPath));
121
+ app.get('/api/version', (_req, res) => {
122
+ cancelShutdown();
123
+ res.json({ version: getPackageVersion() });
124
+ });
125
+ app.get('/api/config', (_req, res) => {
126
+ cancelShutdown();
127
+ res.json({ lastBuild: getLastBuildPaths() ?? null });
128
+ });
129
+ app.post('/api/shutdown', (_req, res) => {
130
+ res.json({ success: true });
131
+ closeBrowseProcess();
132
+ scheduleShutdown();
133
+ });
134
+ app.post('/api/cancel-shutdown', (_req, res) => {
135
+ cancelShutdown();
136
+ res.json({ success: true });
137
+ });
138
+ app.post('/api/build', async (req, res) => {
139
+ cancelShutdown();
140
+ const inputPath = isNonEmptyString(req.body.inputPath) ? req.body.inputPath.trim() : '';
141
+ const outputPath = isNonEmptyString(req.body.outputPath) ? req.body.outputPath.trim() : '';
142
+ if (!inputPath || !outputPath) {
143
+ res.status(400).json({ success: false, message: 'Input and output paths are required.' });
144
+ return;
145
+ }
146
+ const opts = {
147
+ inputDir: path.resolve(inputPath),
148
+ outputFile: path.resolve(outputPath),
149
+ processSubs: true,
150
+ includeHttpHeader: true,
151
+ useHttp11: false,
152
+ supportSsi: true,
153
+ precalcChksum: false,
154
+ minifyOpts: normalizeHtmlMinifyOptions(req.body.minifyOpts),
155
+ optimizeSvg: true,
156
+ svgoMultipass: false
157
+ };
158
+ console.log(`[Build] Input: ${opts.inputDir}`);
159
+ console.log(`[Build] Output: ${opts.outputFile}`);
160
+ try {
161
+ const stats = await runMakeFsData(opts);
162
+ saveLastBuildPaths(opts.inputDir, opts.outputFile);
163
+ try {
164
+ await open(path.dirname(opts.outputFile));
165
+ }
166
+ catch {
167
+ // The generated result is still valid if the folder cannot be opened.
168
+ }
169
+ res.json({ success: true, stats });
170
+ }
171
+ catch (error) {
172
+ const message = error instanceof Error ? error.message : String(error);
173
+ res.json({ success: false, message });
174
+ }
175
+ });
176
+ app.post('/api/change-port', (req, res) => {
177
+ cancelShutdown();
178
+ const parsedPort = Number.parseInt(String(req.body.newPort), 10);
179
+ if (Number.isNaN(parsedPort) || parsedPort < 1 || parsedPort > 65535) {
180
+ res.status(400).json({ success: false, message: 'Invalid port number.' });
181
+ return;
182
+ }
183
+ res.json({ success: true });
184
+ setTimeout(() => {
185
+ if (!server) {
186
+ port = parsedPort;
187
+ startServer(false);
188
+ return;
189
+ }
190
+ try {
191
+ server.closeAllConnections();
192
+ server.close((error) => {
193
+ if (error) {
194
+ console.error(`❌ Could not restart GUI server: ${error.message}`);
195
+ return;
196
+ }
197
+ port = parsedPort;
198
+ startServer(false);
199
+ console.log(`🔄 GUI server restarted on port ${port}`);
200
+ });
201
+ }
202
+ catch {
203
+ port = parsedPort;
204
+ startServer(false);
205
+ }
206
+ }, 100);
207
+ });
208
+ app.get('/api/browse', async (req, res) => {
209
+ cancelShutdown();
210
+ res.on('close', () => {
211
+ if (!res.writableEnded) {
212
+ closeBrowseProcess();
213
+ scheduleShutdown();
214
+ }
215
+ });
216
+ const isDirectory = req.query.type === 'dir';
217
+ const platform = os.platform();
218
+ try {
219
+ let selectedPath = '';
220
+ if (platform === 'win32') {
221
+ const script = isDirectory
222
+ ? buildWindowsDialogScript([
223
+ '$dialog = New-Object System.Windows.Forms.FolderBrowserDialog',
224
+ '$dialog.Description = "Select Input Directory"',
225
+ 'if ($dialog.ShowDialog($owner) -eq [System.Windows.Forms.DialogResult]::OK) {',
226
+ ' $dialog.SelectedPath',
227
+ '}'
228
+ ])
229
+ : buildWindowsDialogScript([
230
+ '$dialog = New-Object System.Windows.Forms.SaveFileDialog',
231
+ '$dialog.Filter = "C Files (*.c)|*.c|All Files (*.*)|*.*"',
232
+ '$dialog.DefaultExt = "c"',
233
+ '$dialog.AddExtension = $true',
234
+ '$dialog.OverwritePrompt = $true',
235
+ 'if ($dialog.ShowDialog($owner) -eq [System.Windows.Forms.DialogResult]::OK) {',
236
+ ' $dialog.FileName',
237
+ '}'
238
+ ]);
239
+ selectedPath = await runDialogCommand('powershell.exe', buildPowerShellArgs(script));
240
+ }
241
+ else if (platform === 'darwin') {
242
+ selectedPath = await runDialogCommand('osascript', [
243
+ '-e',
244
+ isDirectory
245
+ ? 'POSIX path of (choose folder with prompt "Select Input Directory")'
246
+ : 'POSIX path of (choose file name with prompt "Select Output File")'
247
+ ]);
248
+ }
249
+ else {
250
+ selectedPath = await runDialogCommand('zenity', isDirectory
251
+ ? ['--file-selection', '--directory', '--title=Select Input Directory']
252
+ : ['--file-selection', '--save', '--confirm-overwrite', '--title=Select Output File', '--filename=fsdata.c']);
253
+ }
254
+ res.json({ success: true, path: selectedPath || null });
255
+ }
256
+ catch {
257
+ res.json({ success: true, path: null });
258
+ }
259
+ });
260
+ console.log(`[System] Serving static files from: ${publicPath}`);
261
+ startServer(true);
262
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc4-embedded-system",
3
- "version": "3.1.9",
3
+ "version": "3.2.0",
4
4
  "description": "A modern typescript version of makefsdata based on html-minifier-next and lwIP v1.3.1",
5
5
  "bin": {
6
6
  "cc4es": "dist/gui.js"
@@ -30,7 +30,8 @@
30
30
  "dependencies": {
31
31
  "express": "^5.2.1",
32
32
  "html-minifier-next": "^6.1.2",
33
- "open": "^11.0.0"
33
+ "open": "^11.0.0",
34
+ "svgo": "^4.0.2"
34
35
  },
35
36
  "devDependencies": {
36
37
  "@types/express": "^5.0.6",