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 +133 -44
- package/dist/cli.js +25 -0
- package/dist/config.js +29 -10
- package/dist/gui.js +13 -6
- package/dist/gzip-options.js +10 -0
- package/dist/makefsdata.js +138 -31
- package/dist/public/index.html +63 -31
- package/dist/server.js +31 -4
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
# CC4EmbeddedSystem V3
|
|
2
|
-
-
|
|
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
|
|
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
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-

|
|
12
|
+
### Select input directory
|
|
13
|
+

|
|
14
|
+
### Select output file path
|
|
15
|
+

|
|
16
|
+
### Results
|
|
17
|
+

|
|
18
|
+

|
|
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
|
|
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 //
|
|
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
|
-
├──
|
|
32
|
-
│
|
|
33
|
-
|
|
34
|
-
│ └──
|
|
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
|
-
#
|
|
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)
|
|
25
|
-
return { schemaVersion:
|
|
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
|
|
45
|
+
return config;
|
|
30
46
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
lastBuild: { src, dst }
|
|
34
|
-
};
|
|
47
|
+
config.lastBuild = { src, dst };
|
|
48
|
+
return config;
|
|
35
49
|
}
|
|
36
50
|
catch {
|
|
37
|
-
return { schemaVersion:
|
|
51
|
+
return { schemaVersion: 2 };
|
|
38
52
|
}
|
|
39
53
|
};
|
|
40
54
|
export const getLastBuildPaths = () => {
|
|
41
55
|
return readConfig().lastBuild;
|
|
42
56
|
};
|
|
43
|
-
export const
|
|
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:
|
|
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
|
+
};
|
package/dist/makefsdata.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
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
|
-
|
|
242
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.");
|
package/dist/public/index.html
CHANGED
|
@@ -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>
|
|
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
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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('
|
|
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
|
|
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({
|
|
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.
|
|
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
|
},
|