cc4-embedded-system 3.2.0 → 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 CHANGED
@@ -1,19 +1,21 @@
1
1
  # CC4EmbeddedSystem V3
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```.
2
+ - V3.2.1 is a TypeScript/Node.js 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
+ - Assets are handled by type: [html-minifier-next](https://github.com/j9t/html-minifier-next) processes HTML, [Lightning CSS](https://lightningcss.dev/) processes CSS, [esbuild](https://esbuild.github.io/) processes JavaScript, and [SVGO](https://svgo.dev/) processes SVG.
4
+ - Optional deterministic gzip storage reduces the Flash payload for text assets. The MCU sends the stored bytes unchanged; a gzip-capable HTTP client, normally a browser, decompresses the response.
3
5
  - 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.
6
+ - The last successful source/destination paths and gzip setting 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`.
7
+ - 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.
6
8
 
7
9
  ## 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)
10
+ ### CLI mode
11
+ ![](./Screenshot/cli.png)
12
+ ### Select input directory
13
+ ![](./Screenshot/src.png)
14
+ ### Select output file path
15
+ ![](./Screenshot/dst.png)
16
+ ### Results
17
+ ![](./Screenshot/v3.2.1_result1.png)
18
+ ![](./Screenshot/v3.2.1_result2.png)
17
19
 
18
20
  ## Structure
19
21
  ```text
@@ -22,16 +24,17 @@ CC4EmbeddedSystem/
22
24
  │ ├── gui.ts // CLI entry point
23
25
  │ ├── cli.ts // Command-line parsing and help text
24
26
  │ ├── server.ts // Express GUI server
25
- │ ├── config.ts // Persistent last-build paths
27
+ │ ├── config.ts // Persistent paths and gzip options
28
+ │ ├── gzip-options.ts // Shared gzip defaults and validation
26
29
  │ ├── minify-options.ts // Shared HTML minifier options
27
- │ ├── makefsdata.ts // Core C code generation and minification logic
30
+ │ ├── makefsdata.ts // Asset routing, gzip, and C code generation
28
31
  │ └── utils.ts // Package version lookup
29
32
  ├── public/
30
33
  │ └── index.html // Web GUI dashboard
31
- ├── Sereenshot/
32
- ├── inputs.png // Demo image
33
- ├── output.png // Demo image
34
- │ └── v3.1.10.png // Demo image
34
+ ├── test/
35
+ └── cc4es.test.mjs // Native Node.js regression tests
36
+ ├── Screenshot/
37
+ │ └── ... // GUI and CLI screenshots
35
38
  ├── node_modules/ // Required submodules during development
36
39
  ├── dist/ // Compiled JavaScript output (Auto-generated)
37
40
  ├── package.json // Project configuration & dependencies
@@ -39,6 +42,78 @@ CC4EmbeddedSystem/
39
42
  └── tsconfig.json // TypeScript configuration
40
43
  ```
41
44
 
45
+ ## Build architecture
46
+
47
+ CC4ES never modifies the source directory. For each file it produces one final response body, writes the HTTP header and body into `fsdata.c`, and keeps lwIP's `FS_FILE_FLAGS_HEADER_INCLUDED` model.
48
+
49
+ ```text
50
+ source file
51
+ -> extension-based optimizer
52
+ .html/.htm html-minifier-next
53
+ .css Lightning CSS
54
+ .js/.mjs esbuild (or raw with --no-minify-js)
55
+ .svg SVGO
56
+ other raw copy
57
+ -> final response body
58
+ -> optional deterministic gzip when it is smaller
59
+ -> HTTP header + fsdata.c byte array
60
+ ```
61
+
62
+ HTML-only options such as `--remove-comments` are not applied to external JavaScript. This prevents an external `.js` file from being interpreted as an HTML comment. `--no-minify-js` and `--no-minify-css` keep their respective external assets unchanged, while HTML can still use its own minification options.
63
+
64
+ ### Gzip storage
65
+
66
+ Gzip is optional and disabled by default. `--gzip` considers `.html`, `.htm`, `.css`, `.js`, `.mjs`, `.json`, `.svg`, `.xml`, `.txt`, and `.map`; binary and already-compressed resource types remain raw. A candidate is stored as gzip only when its gzip body is strictly smaller than its final raw body.
67
+
68
+ For gzip bodies, CC4ES writes `Content-Encoding: gzip` before the HTTP header terminator. The gzip mtime and OS metadata are normalized so the same input and options produce byte-identical output. A raw fallback has no `Content-Encoding` header.
69
+
70
+ This is a fixed storage representation, not runtime content negotiation: a gzip-enabled `fsdata.c` does not also store a raw duplicate and does not inspect `Accept-Encoding`. Enable gzip only when the target clients support it; use `--no-gzip` for legacy clients. No MCU decompressor, zlib dependency, lwIP change, or application-code change is required.
71
+
72
+ #### Measurement example
73
+
74
+ The following measured an example app build used `--gzip --gzip-level 9`. The exact result depends on the frontend assets, compiler settings, and firmware image format; treat it as an example rather than a universal saving guarantee.
75
+
76
+ | File | Without gzip | gzip level 9 | Reduction |
77
+ |---|---:|---:|---:|
78
+ | `fsdata.c` | 652,029 B | 197,764 B | 454,265 B (69.67%) |
79
+ | `app.elf` | 2,641,144 B | 2,555,120 B | 86,024 B (3.26%) |
80
+ | `app.hex` | 1,001,729 B | 751,917 B | 249,812 B (24.94%) |
81
+ | `app.img` | 356,108 B | 267,292 B | 88,816 B (24.94%) |
82
+
83
+ The flashed memory regions show where the saving occurs:
84
+
85
+ | Region | Without gzip | gzip level 9 | Reduction |
86
+ |---|---:|---:|---:|
87
+ | `text` | 355,420 B | 266,604 B | 88,816 B |
88
+ | `data` | 680 B | 680 B | 0 B |
89
+ | `bss` | 83,280 B | 83,280 B | 0 B |
90
+
91
+ In this build, gzip saved **88,816 bytes** of Flash, approximately **86.73 KiB**. The ELF file includes debug symbols, so its on-disk size decreases by only 3.26%; use the `.img` size or the `text + data` regions when assessing flashed capacity.
92
+
93
+ ## Path Configs
94
+ After the first successful build, CC4EmbeddedSystem saves the most recently used source/destination paths and gzip settings in `cc4es_configs.json`.
95
+
96
+ The file is stored in the local application-data directory by default:
97
+ - Windows: `%LOCALAPPDATA%\cc4-embedded-system\cc4es_configs.json`
98
+ - macOS: `~/Library/Application Support/cc4-embedded-system/cc4es_configs.json`
99
+ - Linux: `$XDG_STATE_HOME/cc4-embedded-system/cc4es_configs.json` (or `~/.local/state/...`)
100
+
101
+ Run `cc4es --config-path` to print the exact path on the current computer.
102
+
103
+ ```json
104
+ {
105
+ "schemaVersion": 2,
106
+ "lastBuild": {
107
+ "src": "C:\\YourProjects\\src",
108
+ "dst": "C:\\YourProjects\\dst\\fsdata.c"
109
+ },
110
+ "gzipOptions": {
111
+ "gzip": false,
112
+ "gzipLevel": 9
113
+ }
114
+ }
115
+ ```
116
+
42
117
  ## Commands
43
118
  ### CLI Help Texts
44
119
  ```text
@@ -46,15 +121,17 @@ Usage: cc4es [options]
46
121
 
47
122
  Without options, starts the browser GUI.
48
123
 
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)
124
+ -h, --help Show this help and exit
125
+ -V, --version Show version and exit
126
+ --config-path Print the full config file path and exit
127
+ --headless Build without server or browser
128
+ -s, --src <directory> Source directory (headless)
129
+ -d, --dst <file|directory> Output C file or directory (default: fsdata.c)
55
130
  -p, --port <1-65535> GUI server port
56
131
  --[no-]optimize-svg Enable or disable SVGO (default: enabled)
57
132
  --svgo-multipass Run SVGO optimization repeatedly
133
+ --[no-]gzip Enable or disable gzip resource storage (default: disabled)
134
+ --gzip-level <1-9> gzip compression level (default: 9)
58
135
 
59
136
  HTML compression options:
60
137
  --[no-]collapse-whitespace --[no-]remove-comments
@@ -63,9 +140,9 @@ HTML compression options:
63
140
  --[no-]remove-attribute-quotes --[no-]remove-empty-attributes
64
141
  --[no-]remove-redundant-attributes --[no-]use-short-doctype
65
142
 
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.
143
+ Headless mode requires --src or --dst and never reruns the last build with
144
+ no path options. A missing counterpart uses the last saved path; if no
145
+ destination is saved, fsdata.c is created in the current working directory.
69
146
  ```
70
147
  ### Normal Use
71
148
  - Global installation
@@ -81,36 +158,48 @@ destination is saved, fsdata.c is created in the current working directory.
81
158
  ```bash
82
159
  cc4es --port 3002
83
160
  ```
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
- ```
161
+ - Headless build (does not start a server or open a browser)
162
+ ```bash
163
+ cc4es --headless --src C:\Project\web --dst C:\Project\fsdata.c
164
+ ```
165
+ - Headless build with the default output filename. An existing output directory uses `fsdata.c` inside it.
166
+ ```bash
167
+ cc4es --headless --src C:\Project\web
168
+ cc4es --headless --src C:\Project\web --dst C:\Project\output
169
+ ```
170
+ - Headless safety: a parameterless command stops instead of rerunning the last build
171
+ ```bash
172
+ cc4es --headless
173
+ ```
97
174
  - CLI help and version
98
175
  ```bash
99
- cc4es --help
100
- cc4es --version
101
- cc4es --config-path
176
+ cc4es --help
177
+ cc4es --version
178
+ cc4es --config-path
102
179
  ```
103
180
  - Compression flags can be supplied in headless mode. Every flag accepts a `--no-` form.
104
181
  ```bash
105
182
  cc4es --headless --no-minify-js --remove-attribute-quotes --svgo-multipass
106
183
  ```
184
+ - Gzip is optional and disabled by default. Only text resources that become smaller are stored as gzip; their response header contains `Content-Encoding: gzip`.
185
+ ```bash
186
+ cc4es --headless --src C:\Project\web --dst C:\Project\fsdata.c --gzip --gzip-level 9
187
+ ```
188
+ A gzip-enabled image stores one fixed gzip representation for eligible resources. It does not keep a raw duplicate or negotiate `Accept-Encoding`; use `--no-gzip` when a consumer requires non-gzip clients. Test it with a browser or `curl --compressed http://DEVICE_IP/app.js`.
189
+ - Files are processed by type: HTML uses html-minifier-next, CSS uses Lightning CSS, JavaScript uses esbuild, and SVG uses SVGO. `--no-minify-js` and `--no-minify-css` preserve their respective external assets.
107
190
  - Update
108
191
  ```bash
109
192
  npm install -g cc4-embedded-system@latest
110
193
  ```
111
194
  ### Development
112
195
  ```bash
113
- # build
196
+ # install locked dependencies
197
+ npm ci
198
+
199
+ # run regression tests
200
+ npm test
201
+
202
+ # compile TypeScript and copy public GUI assets to dist/
114
203
  npm run build
115
204
 
116
205
  # mimic global installation
package/dist/cli.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { DEFAULT_HTML_MINIFY_OPTIONS } from './minify-options.js';
2
+ import { DEFAULT_GZIP_LEVEL, isGzipLevel } from './gzip-options.js';
2
3
  export class CliUsageError extends Error {
3
4
  constructor(message) {
4
5
  super(message);
@@ -41,6 +42,14 @@ const parsePort = (value) => {
41
42
  throw new CliUsageError('--port must be an integer from 1 to 65535.');
42
43
  return port;
43
44
  };
45
+ const parseGzipLevel = (value) => {
46
+ if (!/^\d+$/.test(value))
47
+ throw new CliUsageError('--gzip-level must be an integer from 1 to 9.');
48
+ const gzipLevel = Number.parseInt(value, 10);
49
+ if (!isGzipLevel(gzipLevel))
50
+ throw new CliUsageError('--gzip-level must be an integer from 1 to 9.');
51
+ return gzipLevel;
52
+ };
44
53
  export const parseCliArguments = (args) => {
45
54
  const parsed = {
46
55
  showHelp: false,
@@ -101,6 +110,20 @@ export const parseCliArguments = (args) => {
101
110
  case '--svgo-multipass':
102
111
  parsed.svgoMultipass = true;
103
112
  break;
113
+ case '--gzip':
114
+ parsed.gzip = true;
115
+ break;
116
+ case '--no-gzip':
117
+ parsed.gzip = false;
118
+ break;
119
+ case '--gzip-level': {
120
+ const value = args[index + 1];
121
+ if (!value || value.startsWith('--'))
122
+ throw new CliUsageError('--gzip-level requires a value.');
123
+ parsed.gzipLevel = parseGzipLevel(value);
124
+ index++;
125
+ break;
126
+ }
104
127
  default:
105
128
  throw new CliUsageError(`Unknown option: ${argument}`);
106
129
  }
@@ -131,6 +154,8 @@ export const getHelpText = () => {
131
154
  ' -p, --port <1-65535> GUI server port',
132
155
  ' --[no-]optimize-svg Enable or disable SVGO (default: enabled)',
133
156
  ' --svgo-multipass Run SVGO optimization repeatedly',
157
+ ' --[no-]gzip Enable or disable gzip resource storage (default: disabled)',
158
+ ` --gzip-level <1-9> gzip compression level (default: ${DEFAULT_GZIP_LEVEL})`,
134
159
  '',
135
160
  'HTML compression options:',
136
161
  ' --[no-]collapse-whitespace --[no-]remove-comments',
package/dist/config.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from 'node:fs';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
+ import { isGzipLevel } from './gzip-options.js';
4
5
  export const CONFIG_FILE_NAME = 'cc4es_configs.json';
5
6
  const getStateDirectory = () => {
6
7
  const homeDirectory = os.homedir();
@@ -18,36 +19,54 @@ export const getConfigFilePath = () => {
18
19
  const isRecord = (value) => {
19
20
  return typeof value === 'object' && value !== null;
20
21
  };
22
+ const getGzipOptions = (value) => {
23
+ if (!isRecord(value))
24
+ return undefined;
25
+ const gzip = value.gzip;
26
+ const gzipLevel = value.gzipLevel;
27
+ if (typeof gzip !== 'boolean' || !isGzipLevel(gzipLevel))
28
+ return undefined;
29
+ return { gzip, gzipLevel };
30
+ };
21
31
  const readConfig = () => {
22
32
  try {
23
33
  const parsedConfig = JSON.parse(fs.readFileSync(getConfigFilePath(), 'utf8'));
24
- if (!isRecord(parsedConfig) || !isRecord(parsedConfig.lastBuild))
25
- return { schemaVersion: 1 };
34
+ if (!isRecord(parsedConfig))
35
+ return { schemaVersion: 2 };
36
+ const config = { schemaVersion: 2 };
37
+ const gzipOptions = getGzipOptions(parsedConfig.gzipOptions);
38
+ if (gzipOptions)
39
+ config.gzipOptions = gzipOptions;
40
+ if (!isRecord(parsedConfig.lastBuild))
41
+ return config;
26
42
  const src = parsedConfig.lastBuild.src;
27
43
  const dst = parsedConfig.lastBuild.dst;
28
44
  if (typeof src !== 'string' || typeof dst !== 'string' || !src || !dst) {
29
- return { schemaVersion: 1 };
45
+ return config;
30
46
  }
31
- return {
32
- schemaVersion: 1,
33
- lastBuild: { src, dst }
34
- };
47
+ config.lastBuild = { src, dst };
48
+ return config;
35
49
  }
36
50
  catch {
37
- return { schemaVersion: 1 };
51
+ return { schemaVersion: 2 };
38
52
  }
39
53
  };
40
54
  export const getLastBuildPaths = () => {
41
55
  return readConfig().lastBuild;
42
56
  };
43
- export const saveLastBuildPaths = (src, dst) => {
57
+ export const getLastGzipOptions = () => {
58
+ return readConfig().gzipOptions;
59
+ };
60
+ export const saveLastBuildPaths = (src, dst, gzipOptions) => {
44
61
  const configFilePath = getConfigFilePath();
45
62
  const temporaryFilePath = `${configFilePath}.${process.pid}.tmp`;
46
63
  const configAlreadyExists = fs.existsSync(configFilePath);
47
64
  const config = {
48
- schemaVersion: 1,
65
+ schemaVersion: 2,
49
66
  lastBuild: { src, dst }
50
67
  };
68
+ if (gzipOptions)
69
+ config.gzipOptions = gzipOptions;
51
70
  try {
52
71
  fs.mkdirSync(path.dirname(configFilePath), { recursive: true });
53
72
  fs.writeFileSync(temporaryFilePath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
package/dist/gui.js CHANGED
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import path from 'node:path';
3
3
  import { CliUsageError, getHelpText, parseCliArguments } from './cli.js';
4
- import { getConfigFilePath, getLastBuildPaths, saveLastBuildPaths } from './config.js';
4
+ import { getConfigFilePath, getLastBuildPaths, getLastGzipOptions, saveLastBuildPaths } from './config.js';
5
+ import { DEFAULT_GZIP_LEVEL } from './gzip-options.js';
5
6
  import { runMakeFsData } from './makefsdata.js';
6
7
  import { startGuiServer } from './server.js';
7
8
  import { getPackageVersion } from './utils.js';
@@ -11,10 +12,15 @@ const getDefaultGuiPort = () => {
11
12
  return 3000;
12
13
  return configuredPort;
13
14
  };
14
- const runHeadlessBuild = async (inputPath, outputPath, minifyOpts, optimizeSvg, svgoMultipass) => {
15
+ const runHeadlessBuild = async (inputPath, outputPath, minifyOpts, optimizeSvg, svgoMultipass, gzipOverride, gzipLevelOverride) => {
15
16
  const lastBuild = getLastBuildPaths();
17
+ const lastGzipOptions = getLastGzipOptions();
16
18
  const selectedInputPath = inputPath ?? lastBuild?.src;
17
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
+ };
18
24
  if (!selectedInputPath) {
19
25
  throw new CliUsageError('Headless mode requires --src, or a source path saved by a previous successful build.');
20
26
  }
@@ -28,11 +34,12 @@ const runHeadlessBuild = async (inputPath, outputPath, minifyOpts, optimizeSvg,
28
34
  precalcChksum: false,
29
35
  minifyOpts,
30
36
  optimizeSvg,
31
- svgoMultipass
37
+ svgoMultipass,
38
+ ...gzipOptions
32
39
  };
33
40
  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.`);
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)).`);
36
43
  };
37
44
  const main = async () => {
38
45
  try {
@@ -50,7 +57,7 @@ const main = async () => {
50
57
  return;
51
58
  }
52
59
  if (args.headless) {
53
- await runHeadlessBuild(args.inputPath, args.outputPath, args.minifyOpts, args.optimizeSvg, args.svgoMultipass);
60
+ await runHeadlessBuild(args.inputPath, args.outputPath, args.minifyOpts, args.optimizeSvg, args.svgoMultipass, args.gzip, args.gzipLevel);
54
61
  return;
55
62
  }
56
63
  startGuiServer(args.port ?? getDefaultGuiPort());
@@ -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,8 +59,12 @@
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';
63
66
  import { optimize } from 'svgo';
67
+ import { DEFAULT_GZIP_LEVEL, isGzipLevel } from './gzip-options.js';
64
68
  import { getPackageVersion } from './utils.js';
65
69
  import { DEFAULT_HTML_MINIFY_OPTIONS } from './minify-options.js';
66
70
  // ----------------------------------------------------------------------
@@ -69,6 +73,9 @@ import { DEFAULT_HTML_MINIFY_OPTIONS } from './minify-options.js';
69
73
  const TCP_MSS = 1460;
70
74
  const LWIP_VERSION = "1.3.1"; // this makefsdata.ts is based on lwIP v1.3.1
71
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
+ ]);
72
79
  // ----------------------------------------------------------------------
73
80
  // 3. lwIP Helpers
74
81
  // ----------------------------------------------------------------------
@@ -85,6 +92,7 @@ function getMimeType(fileName) {
85
92
  case '.css':
86
93
  return 'text/css';
87
94
  case '.js':
95
+ case '.mjs':
88
96
  return 'application/javascript';
89
97
  case '.png':
90
98
  return 'image/png';
@@ -99,6 +107,7 @@ function getMimeType(fileName) {
99
107
  case '.xml':
100
108
  return 'text/xml';
101
109
  case '.json':
110
+ case '.map':
102
111
  return 'application/json';
103
112
  case '.svg':
104
113
  return 'image/svg+xml';
@@ -119,7 +128,7 @@ function inetChksum(buf) {
119
128
  sum = (sum & 0xFFFF) + (sum >> 16);
120
129
  return (~sum) & 0xFFFF;
121
130
  }
122
- function generateHttpHeaders(fileName, dataLength, opts) {
131
+ function generateHttpHeaders(fileName, dataLength, opts, contentEncoding) {
123
132
  const parts = [];
124
133
  const ccVersion = getPackageVersion();
125
134
  const addPart = (str) => { parts.push({ str, buf: Buffer.from(str, 'ascii') }); };
@@ -149,21 +158,29 @@ function generateHttpHeaders(fileName, dataLength, opts) {
149
158
  else
150
159
  addPart(`Connection: close\r\n`);
151
160
  }
152
- 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`);
153
168
  return { parts, totalBuffer: Buffer.concat(parts.map(p => p.buf)) };
154
169
  }
155
- function getFilesRecursive(dir, processSubs) {
170
+ function getFilesRecursive(dir, processSubs, sortEntries) {
156
171
  const results = [];
157
172
  if (!fs.existsSync(dir))
158
173
  return results;
159
174
  const list = fs.readdirSync(dir);
175
+ if (sortEntries)
176
+ list.sort();
160
177
  for (const file of list) {
161
178
  const fullPath = path.join(dir, file);
162
179
  const stat = fs.statSync(fullPath);
163
180
  if (!stat.isDirectory())
164
181
  results.push(fullPath);
165
182
  else if (processSubs)
166
- results.push(...getFilesRecursive(fullPath, processSubs));
183
+ results.push(...getFilesRecursive(fullPath, processSubs, sortEntries));
167
184
  }
168
185
  return results;
169
186
  }
@@ -183,6 +200,74 @@ function bufferToHexCArray(buf) {
183
200
  out += '\n';
184
201
  return out;
185
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
+ }
186
271
  export async function runMakeFsData(opts) {
187
272
  console.log(`🚀 CC4EmbeddedSystem V3: Starting makefsdata compilation...`);
188
273
  // Check source directory before making any output directory changes.
@@ -202,10 +287,16 @@ export async function runMakeFsData(opts) {
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
302
  const activeCompressOpts = opts.minifyOpts ?? DEFAULT_HTML_MINIFY_OPTIONS;
@@ -216,32 +307,29 @@ export async function runMakeFsData(opts) {
216
307
  let content = fs.readFileSync(filePath);
217
308
  const originalFileSize = content.length;
218
309
  totalOriginalSize += originalFileSize;
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)) {
234
- const minifiedStr = await minify(content.toString('utf8'), activeCompressOpts);
235
- if (typeof minifiedStr === 'string') {
236
- content = Buffer.from(minifiedStr, 'utf8');
237
- 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}%)`;
238
324
  }
325
+ else
326
+ compressionNote = `raw, original ${rawContentLength} B, gzip was not smaller`;
239
327
  }
240
- else
241
- console.log(`📄 Copied: ${relativePath}`);
242
- totalCompressedSize += content.length;
328
+ else if (gzipEnabled)
329
+ compressionNote = 'raw, not a gzip candidate';
330
+ totalStoredSize += content.length;
243
331
  // generate header
244
- 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) };
245
333
  const nameBuffer = Buffer.from(relativePath + '\0', 'utf8');
246
334
  const varName = relativePath.replace(/[^A-Za-z0-9]/g, '_');
247
335
  const chksums = [];
@@ -257,7 +345,21 @@ export async function runMakeFsData(opts) {
257
345
  offset += chunkLen;
258
346
  }
259
347
  }
260
- 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);
261
363
  }
262
364
  // find real redirhome.html
263
365
  const redirhomeIndex = fileEntries.findIndex(file => file.pathName.toLowerCase() === REDIRHOME_PATH);
@@ -285,6 +387,8 @@ export async function runMakeFsData(opts) {
285
387
  }
286
388
  // generate data arrays
287
389
  for (const file of orderedFileEntries) {
390
+ if (file.compressionNote)
391
+ cOutput += `/* ${file.pathName}: ${file.compressionNote} */\n`;
288
392
  cOutput += `static const unsigned int dummy_align_${file.varName} = 0;\n`;
289
393
  cOutput += `static const unsigned char data_${file.varName}[] = {\n`;
290
394
  cOutput += `\t/* ${file.pathName} (${file.nameBuffer.length} chars) */\n`;
@@ -299,7 +403,8 @@ export async function runMakeFsData(opts) {
299
403
  cOutput += bufferToHexCArray(part.buf);
300
404
  }
301
405
  }
302
- 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`;
303
408
  cOutput += bufferToHexCArray(file.contentBuffer);
304
409
  cOutput += `};\n\n\n\n`;
305
410
  }
@@ -340,8 +445,10 @@ export async function runMakeFsData(opts) {
340
445
  return {
341
446
  originalSize: totalOriginalSize,
342
447
  compressedSize: totalCompressedSize,
448
+ storedSize: totalStoredSize,
343
449
  convertedSize: convertedSize,
344
- filesCount: orderedFileEntries.length
450
+ filesCount: orderedFileEntries.length,
451
+ gzipFilesCount
345
452
  };
346
453
  }
347
454
  throw new Error("No files processed.");
@@ -28,9 +28,14 @@
28
28
  /* Advanced Settings */
29
29
  details { background: #1e1e1e; padding: 12px; border-radius: 4px; border: 1px solid #444; margin-bottom: 20px; }
30
30
  summary { cursor: pointer; color: #c586c0; font-weight: bold; outline: none; user-select: none; }
31
- .options-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 15px; }
32
- .checkbox-label { display: flex; align-items: center; font-weight: normal; color: #cccccc; cursor: pointer; font-size: 14px; margin-bottom: 0; }
33
- .checkbox-label input { margin-right: 8px; cursor: pointer; accent-color: #0e639c; }
31
+ .options-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 15px; }
32
+ .checkbox-label { display: flex; align-items: center; font-weight: normal; color: #cccccc; cursor: pointer; font-size: 14px; margin-bottom: 0; }
33
+ .checkbox-label input[type="checkbox"] { margin-right: 8px; cursor: pointer; accent-color: #0e639c; }
34
+ .gzip-options { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-top: 15px; padding-top: 12px; border-top: 1px solid #444; }
35
+ .gzip-toggle { white-space: nowrap; }
36
+ .gzip-level-control { display: grid; grid-template-columns: auto 68px; align-items: center; justify-content: start; gap: 8px; margin: 0; font-weight: normal; color: #cccccc; font-size: 14px; }
37
+ .gzip-level-control[hidden] { display: none; }
38
+ .gzip-level { box-sizing: border-box; width: 68px; padding: 4px 6px; background: #3c3c3c; border: 1px solid #555; border-radius: 4px; color: #d4d4d4; text-align: center; font-family: 'Consolas', monospace; }
34
39
 
35
40
  /* Compression Stats Panel */
36
41
  .stats-panel { background: #1e1e1e; border: 1px solid #444; border-radius: 4px; padding: 15px; margin-bottom: 20px; display: none; }
@@ -90,15 +95,20 @@
90
95
  <label class="checkbox-label"><input type="checkbox" id="opt-removeAttributeQuotes"> removeAttributeQuotes</label>
91
96
  <label class="checkbox-label"><input type="checkbox" id="opt-removeEmptyAttributes"> removeEmptyAttributes</label>
92
97
  <label class="checkbox-label"><input type="checkbox" id="opt-removeRedundantAttributes"> redundantAttributes</label>
93
- <label class="checkbox-label"><input type="checkbox" id="opt-useShortDoctype"> useShortDoctype</label>
94
- </div>
98
+ <label class="checkbox-label"><input type="checkbox" id="opt-useShortDoctype"> useShortDoctype</label>
99
+ </div>
100
+ <div class="gzip-options">
101
+ <label class="checkbox-label gzip-toggle"><input type="checkbox" id="opt-gzip">gzip resource storage</label>
102
+ <label id="gzip-level-control" class="gzip-level-control" for="opt-gzipLevel" hidden><span>gzip level</span><input type="number" id="opt-gzipLevel" class="gzip-level" min="1" max="9" value="9" disabled></label>
103
+ </div>
95
104
  </details>
96
105
 
97
106
  <div id="statsPanel" class="stats-panel">
98
107
  <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>
108
+ <div class="stats-info">
109
+ <span>Compressed: <span id="compSize" style="color: #9cdcfe;">0</span> KB</span>
110
+ <span>Stored: <span id="storedSize" style="color: #4ec9b0;">0</span> KB</span>
111
+ <span>Converted: <span id="convSize" style="color: #dcdcaa;">0</span> KB</span>
102
112
  <span>Original: <span id="origSize" style="color: #ce9178;">0</span> KB</span>
103
113
  </div>
104
114
  <div class="bar-container">
@@ -164,10 +174,16 @@
164
174
  let isBrowsePending = false;
165
175
  document.getElementById('guiPort').value = window.location.port || '80';
166
176
 
167
- function setBrowseState(active) {
168
- isBrowsing = active;
169
- document.body.classList.toggle('browse-locked', active);
170
- }
177
+ function setBrowseState(active) {
178
+ isBrowsing = active;
179
+ document.body.classList.toggle('browse-locked', active);
180
+ }
181
+
182
+ function syncGzipLevel() {
183
+ const enabled = document.getElementById('opt-gzip').checked;
184
+ document.getElementById('gzip-level-control').hidden = !enabled;
185
+ document.getElementById('opt-gzipLevel').disabled = !enabled;
186
+ }
171
187
 
172
188
  async function handleBrowse(targetId, type) {
173
189
  if (isBrowsing || isBrowsePending) return;
@@ -222,9 +238,17 @@
222
238
  document.getElementById('inputPath').value = data.lastBuild.src || '';
223
239
  document.getElementById('outputPath').value = data.lastBuild.dst || '';
224
240
  }
241
+
242
+ if (data.gzipOptions) {
243
+ document.getElementById('opt-gzip').checked = data.gzipOptions.gzip === true;
244
+ document.getElementById('opt-gzipLevel').value = data.gzipOptions.gzipLevel || 9;
245
+ }
246
+ syncGzipLevel();
225
247
  }
226
248
  catch (error) { console.error('Can\'t load saved paths!', error); }
227
249
  });
250
+
251
+ document.getElementById('opt-gzip').addEventListener('change', syncGzipLevel);
228
252
 
229
253
  async function changePort() {
230
254
  const newPort = document.getElementById('guiPort').value.trim();
@@ -248,7 +272,7 @@
248
272
  const outPath = document.getElementById('outputPath').value.trim();
249
273
  if (! inPath || ! outPath) { alert('❌ Paths required.'); return; }
250
274
 
251
- const minifyOptions = {
275
+ const minifyOptions = {
252
276
  collapseWhitespace: document.getElementById('opt-collapseWhitespace').checked,
253
277
  removeComments: document.getElementById('opt-removeComments').checked,
254
278
  minifyJS: document.getElementById('opt-minifyJS').checked,
@@ -258,13 +282,19 @@
258
282
  removeAttributeQuotes: document.getElementById('opt-removeAttributeQuotes').checked,
259
283
  removeEmptyAttributes: document.getElementById('opt-removeEmptyAttributes').checked,
260
284
  removeRedundantAttributes: document.getElementById('opt-removeRedundantAttributes').checked,
261
- useShortDoctype: document.getElementById('opt-useShortDoctype').checked
262
- };
263
-
264
- const res = await fetch('/api/build', {
265
- method: 'POST',
266
- headers: { 'Content-Type': 'application/json' },
267
- body: JSON.stringify({ inputPath: inPath, outputPath: outPath, minifyOpts: minifyOptions })
285
+ useShortDoctype: document.getElementById('opt-useShortDoctype').checked
286
+ };
287
+ const gzip = document.getElementById('opt-gzip').checked;
288
+ const gzipLevel = Number(document.getElementById('opt-gzipLevel').value);
289
+ if (!Number.isInteger(gzipLevel) || gzipLevel < 1 || gzipLevel > 9) {
290
+ alert('❌ gzip level must be an integer from 1 to 9.');
291
+ return;
292
+ }
293
+
294
+ const res = await fetch('/api/build', {
295
+ method: 'POST',
296
+ headers: { 'Content-Type': 'application/json' },
297
+ body: JSON.stringify({ inputPath: inPath, outputPath: outPath, minifyOpts: minifyOptions, gzip, gzipLevel })
268
298
  });
269
299
  const result = await res.json();
270
300
 
@@ -274,19 +304,21 @@
274
304
  const panel = document.getElementById('statsPanel');
275
305
  panel.style.display = 'block';
276
306
 
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);
281
-
282
- // ratio: the saved percentage
283
- const ratio = originalSize > 0 ? (((originalSize - compressedSize) / originalSize) * 100).toFixed(1) : 0;
307
+ const { originalSize, compressedSize, storedSize, convertedSize } = result.stats;
308
+ const origKB = (originalSize / 1024).toFixed(2);
309
+ const compKB = (compressedSize / 1024).toFixed(2);
310
+ const storedKB = (storedSize / 1024).toFixed(2);
311
+ const convKB = (convertedSize / 1024).toFixed(2);
312
+
313
+ // ratio: the saved percentage
314
+ const ratio = originalSize > 0 ? (((originalSize - storedSize) / originalSize) * 100).toFixed(1) : 0;
284
315
  // compressedRatio: the remainings percentage
285
316
  const compressedRatio = (100 - ratio).toFixed(1);
286
317
 
287
- document.getElementById('origSize').innerText = origKB;
288
- document.getElementById('compSize').innerText = compKB;
289
- document.getElementById('convSize').innerText = convKB;
318
+ document.getElementById('origSize').innerText = origKB;
319
+ document.getElementById('compSize').innerText = compKB;
320
+ document.getElementById('storedSize').innerText = storedKB;
321
+ document.getElementById('convSize').innerText = convKB;
290
322
  document.getElementById('barText').innerText = `${ratio}% Size Reduced`;
291
323
 
292
324
  // actual used percentage
@@ -294,7 +326,7 @@
294
326
  document.getElementById('barFill').style.width = compressedRatio + '%';
295
327
  }, 50);
296
328
 
297
- document.getElementById('savingsInfo').innerText = `✨ Total saved: ${(origKB - compKB).toFixed(2)} KB`;
329
+ document.getElementById('savingsInfo').innerText = `✨ Total stored saving: ${(origKB - storedKB).toFixed(2)} KB`;
298
330
  }
299
331
 
300
332
  alert('✨ Success! C Code generated at ' + outPath);
package/dist/server.js CHANGED
@@ -5,7 +5,8 @@ import os from 'node:os';
5
5
  import path from 'node:path';
6
6
  import { fileURLToPath } from 'node:url';
7
7
  import open from 'open';
8
- import { getLastBuildPaths, saveLastBuildPaths } from './config.js';
8
+ import { getLastBuildPaths, getLastGzipOptions, saveLastBuildPaths } from './config.js';
9
+ import { DEFAULT_GZIP_LEVEL, isGzipLevel } from './gzip-options.js';
9
10
  import { runMakeFsData } from './makefsdata.js';
10
11
  import { normalizeHtmlMinifyOptions } from './minify-options.js';
11
12
  import { getPackageVersion } from './utils.js';
@@ -47,6 +48,19 @@ const buildWindowsDialogScript = (dialogLines) => {
47
48
  const isNonEmptyString = (value) => {
48
49
  return typeof value === 'string' && value.trim().length > 0;
49
50
  };
51
+ const getGzipOptionsFromRequest = (value) => {
52
+ if (typeof value !== 'object' || value === null) {
53
+ return { gzip: false, gzipLevel: DEFAULT_GZIP_LEVEL };
54
+ }
55
+ const requestOptions = value;
56
+ const gzip = requestOptions.gzip ?? false;
57
+ const gzipLevel = requestOptions.gzipLevel ?? DEFAULT_GZIP_LEVEL;
58
+ if (typeof gzip !== 'boolean')
59
+ throw new Error('gzip must be a boolean.');
60
+ if (!isGzipLevel(gzipLevel))
61
+ throw new Error('gzip level must be an integer from 1 to 9.');
62
+ return { gzip, gzipLevel };
63
+ };
50
64
  export const startGuiServer = (initialPort) => {
51
65
  const app = express();
52
66
  let port = initialPort;
@@ -124,7 +138,10 @@ export const startGuiServer = (initialPort) => {
124
138
  });
125
139
  app.get('/api/config', (_req, res) => {
126
140
  cancelShutdown();
127
- res.json({ lastBuild: getLastBuildPaths() ?? null });
141
+ res.json({
142
+ lastBuild: getLastBuildPaths() ?? null,
143
+ gzipOptions: getLastGzipOptions() ?? { gzip: false, gzipLevel: DEFAULT_GZIP_LEVEL }
144
+ });
128
145
  });
129
146
  app.post('/api/shutdown', (_req, res) => {
130
147
  res.json({ success: true });
@@ -143,6 +160,15 @@ export const startGuiServer = (initialPort) => {
143
160
  res.status(400).json({ success: false, message: 'Input and output paths are required.' });
144
161
  return;
145
162
  }
163
+ let gzipOptions;
164
+ try {
165
+ gzipOptions = getGzipOptionsFromRequest(req.body);
166
+ }
167
+ catch (error) {
168
+ const message = error instanceof Error ? error.message : String(error);
169
+ res.status(400).json({ success: false, message });
170
+ return;
171
+ }
146
172
  const opts = {
147
173
  inputDir: path.resolve(inputPath),
148
174
  outputFile: path.resolve(outputPath),
@@ -153,13 +179,14 @@ export const startGuiServer = (initialPort) => {
153
179
  precalcChksum: false,
154
180
  minifyOpts: normalizeHtmlMinifyOptions(req.body.minifyOpts),
155
181
  optimizeSvg: true,
156
- svgoMultipass: false
182
+ svgoMultipass: false,
183
+ ...gzipOptions
157
184
  };
158
185
  console.log(`[Build] Input: ${opts.inputDir}`);
159
186
  console.log(`[Build] Output: ${opts.outputFile}`);
160
187
  try {
161
188
  const stats = await runMakeFsData(opts);
162
- saveLastBuildPaths(opts.inputDir, opts.outputFile);
189
+ saveLastBuildPaths(opts.inputDir, opts.outputFile, gzipOptions);
163
190
  try {
164
191
  await open(path.dirname(opts.outputFile));
165
192
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc4-embedded-system",
3
- "version": "3.2.0",
3
+ "version": "3.2.1",
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"
@@ -9,6 +9,7 @@
9
9
  "scripts": {
10
10
  "dev": "tsx src/gui.ts",
11
11
  "build": "tsc && xcopy /E /I /Y public dist\\public",
12
+ "test": "tsc && node --test",
12
13
  "prepublishOnly": "npm run build"
13
14
  },
14
15
  "files": [
@@ -28,8 +29,10 @@
28
29
  "author": "LunaticGhoulPiano",
29
30
  "license": "MIT",
30
31
  "dependencies": {
32
+ "esbuild": "^0.27.7",
31
33
  "express": "^5.2.1",
32
34
  "html-minifier-next": "^6.1.2",
35
+ "lightningcss": "^1.32.0",
33
36
  "open": "^11.0.0",
34
37
  "svgo": "^4.0.2"
35
38
  },