cc4-embedded-system 3.1.10 → 3.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,31 +1,40 @@
1
1
  # CC4EmbeddedSystem V3
2
- - This version based on [html-minifier-next](https://github.com/j9t/html-minifier-next) and rewrite [lwIP makefsdata](https://github.com/m-labs/lwip/tree/master/src/apps/httpd/makefsdata), run on localhost, default port: ```3000```.
2
+ - 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).
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.
4
8
 
5
9
  ## Demo
6
- - Select input directory
7
-
8
- ![](./Screenshot/inputs.png)
9
- - Select output file path
10
-
11
- ![](./Screenshot/output.png)
12
- - Result
13
-
14
- ![](./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)
15
19
 
16
20
  ## Structure
17
21
  ```text
18
22
  CC4EmbeddedSystem/
19
23
  ├── src/
20
- │ ├── gui.ts // Express server & CLI entry point
21
- │ └── utils.ts // To get latest tool version
22
- └── makefsdata.ts // Core C code generation & minification logic
24
+ │ ├── gui.ts // CLI entry point
25
+ ├── cli.ts // Command-line parsing and help text
26
+ ├── server.ts // Express GUI server
27
+ │ ├── config.ts // Persistent paths and gzip options
28
+ │ ├── gzip-options.ts // Shared gzip defaults and validation
29
+ │ ├── minify-options.ts // Shared HTML minifier options
30
+ │ ├── makefsdata.ts // Asset routing, gzip, and C code generation
31
+ │ └── utils.ts // Package version lookup
23
32
  ├── public/
24
33
  │ └── index.html // Web GUI dashboard
25
- ├── Sereenshot/
26
- ├── inputs.png // Demo image
27
- ├── output.png // Demo image
28
- │ └── v3.1.10.png // Demo image
34
+ ├── test/
35
+ └── cc4es.test.mjs // Native Node.js regression tests
36
+ ├── Screenshot/
37
+ │ └── ... // GUI and CLI screenshots
29
38
  ├── node_modules/ // Required submodules during development
30
39
  ├── dist/ // Compiled JavaScript output (Auto-generated)
31
40
  ├── package.json // Project configuration & dependencies
@@ -33,7 +42,108 @@ CC4EmbeddedSystem/
33
42
  └── tsconfig.json // TypeScript configuration
34
43
  ```
35
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
+
36
117
  ## Commands
118
+ ### CLI Help Texts
119
+ ```text
120
+ Usage: cc4es [options]
121
+
122
+ Without options, starts the browser GUI.
123
+
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)
130
+ -p, --port <1-65535> GUI server port
131
+ --[no-]optimize-svg Enable or disable SVGO (default: enabled)
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)
135
+
136
+ HTML compression options:
137
+ --[no-]collapse-whitespace --[no-]remove-comments
138
+ --[no-]minify-js --[no-]minify-css
139
+ --[no-]conditional-comments --[no-]decode-entities
140
+ --[no-]remove-attribute-quotes --[no-]remove-empty-attributes
141
+ --[no-]remove-redundant-attributes --[no-]use-short-doctype
142
+
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.
146
+ ```
37
147
  ### Normal Use
38
148
  - Global installation
39
149
  ```bash
@@ -48,19 +158,57 @@ CC4EmbeddedSystem/
48
158
  ```bash
49
159
  cc4es --port 3002
50
160
  ```
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
+ ```
174
+ - CLI help and version
175
+ ```bash
176
+ cc4es --help
177
+ cc4es --version
178
+ cc4es --config-path
179
+ ```
180
+ - Compression flags can be supplied in headless mode. Every flag accepts a `--no-` form.
181
+ ```bash
182
+ cc4es --headless --no-minify-js --remove-attribute-quotes --svgo-multipass
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.
51
190
  - Update
52
191
  ```bash
53
192
  npm install -g cc4-embedded-system@latest
54
193
  ```
55
194
  ### Development
56
195
  ```bash
57
- # 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/
58
203
  npm run build
59
204
 
60
205
  # mimic global installation
61
206
  npm link
62
207
  cc4es
63
208
 
209
+ # headless development test
210
+ npm run dev -- --headless --src C:\Project\web --dst C:\Project\fsdata.c
211
+
64
212
  # publish
65
213
  npm whoami # verify
66
214
  npm login
@@ -68,4 +216,4 @@ npm version patch --no-git-tag-version # if increase version
68
216
  npm run dev # dev mode for testing
69
217
  npm run build # build the newest
70
218
  npm publish --access public
71
- ```
219
+ ```
package/dist/cli.js ADDED
@@ -0,0 +1,171 @@
1
+ import { DEFAULT_HTML_MINIFY_OPTIONS } from './minify-options.js';
2
+ import { DEFAULT_GZIP_LEVEL, isGzipLevel } from './gzip-options.js';
3
+ export class CliUsageError extends Error {
4
+ constructor(message) {
5
+ super(message);
6
+ this.name = 'CliUsageError';
7
+ }
8
+ }
9
+ const MINIFY_FLAGS = {
10
+ '--collapse-whitespace': 'collapseWhitespace',
11
+ '--no-collapse-whitespace': 'collapseWhitespace',
12
+ '--remove-comments': 'removeComments',
13
+ '--no-remove-comments': 'removeComments',
14
+ '--minify-js': 'minifyJS',
15
+ '--no-minify-js': 'minifyJS',
16
+ '--minify-css': 'minifyCSS',
17
+ '--no-minify-css': 'minifyCSS',
18
+ '--conditional-comments': 'processConditionalComments',
19
+ '--no-conditional-comments': 'processConditionalComments',
20
+ '--decode-entities': 'decodeEntities',
21
+ '--no-decode-entities': 'decodeEntities',
22
+ '--remove-attribute-quotes': 'removeAttributeQuotes',
23
+ '--no-remove-attribute-quotes': 'removeAttributeQuotes',
24
+ '--remove-empty-attributes': 'removeEmptyAttributes',
25
+ '--no-remove-empty-attributes': 'removeEmptyAttributes',
26
+ '--remove-redundant-attributes': 'removeRedundantAttributes',
27
+ '--no-remove-redundant-attributes': 'removeRedundantAttributes',
28
+ '--use-short-doctype': 'useShortDoctype',
29
+ '--no-use-short-doctype': 'useShortDoctype'
30
+ };
31
+ const getRequiredValue = (args, index, option) => {
32
+ const value = args[index + 1];
33
+ if (!value || value.startsWith('-'))
34
+ throw new CliUsageError(`${option} requires a value.`);
35
+ return value;
36
+ };
37
+ const parsePort = (value) => {
38
+ if (!/^\d+$/.test(value))
39
+ throw new CliUsageError('--port must be an integer from 1 to 65535.');
40
+ const port = Number.parseInt(value, 10);
41
+ if (port < 1 || port > 65535)
42
+ throw new CliUsageError('--port must be an integer from 1 to 65535.');
43
+ return port;
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
+ };
53
+ export const parseCliArguments = (args) => {
54
+ const parsed = {
55
+ showHelp: false,
56
+ showVersion: false,
57
+ showConfigPath: false,
58
+ headless: false,
59
+ minifyOpts: { ...DEFAULT_HTML_MINIFY_OPTIONS },
60
+ optimizeSvg: true,
61
+ svgoMultipass: false
62
+ };
63
+ for (let index = 0; index < args.length; index++) {
64
+ const argument = args[index];
65
+ if (!argument)
66
+ continue;
67
+ const minifyOption = MINIFY_FLAGS[argument];
68
+ if (minifyOption) {
69
+ parsed.minifyOpts[minifyOption] = !argument.startsWith('--no-');
70
+ continue;
71
+ }
72
+ switch (argument) {
73
+ case '-h':
74
+ case '--help':
75
+ parsed.showHelp = true;
76
+ break;
77
+ case '-V':
78
+ case '--version':
79
+ parsed.showVersion = true;
80
+ break;
81
+ case '--config-path':
82
+ parsed.showConfigPath = true;
83
+ break;
84
+ case '--headless':
85
+ parsed.headless = true;
86
+ break;
87
+ case '-s':
88
+ case '--src':
89
+ case '--input':
90
+ parsed.inputPath = getRequiredValue(args, index, argument);
91
+ index++;
92
+ break;
93
+ case '-d':
94
+ case '--dst':
95
+ case '--output':
96
+ parsed.outputPath = getRequiredValue(args, index, argument);
97
+ index++;
98
+ break;
99
+ case '-p':
100
+ case '--port':
101
+ parsed.port = parsePort(getRequiredValue(args, index, argument));
102
+ index++;
103
+ break;
104
+ case '--optimize-svg':
105
+ parsed.optimizeSvg = true;
106
+ break;
107
+ case '--no-optimize-svg':
108
+ parsed.optimizeSvg = false;
109
+ break;
110
+ case '--svgo-multipass':
111
+ parsed.svgoMultipass = true;
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
+ }
127
+ default:
128
+ throw new CliUsageError(`Unknown option: ${argument}`);
129
+ }
130
+ }
131
+ if (parsed.headless && parsed.port !== undefined) {
132
+ throw new CliUsageError('--port is only available when starting the GUI.');
133
+ }
134
+ if (parsed.headless && !parsed.inputPath && !parsed.outputPath) {
135
+ throw new CliUsageError('Headless mode requires --src or --dst. It will not automatically rerun the last build.');
136
+ }
137
+ if (!parsed.headless && (parsed.inputPath || parsed.outputPath)) {
138
+ throw new CliUsageError('--src and --dst require --headless.');
139
+ }
140
+ return parsed;
141
+ };
142
+ export const getHelpText = () => {
143
+ return [
144
+ 'Usage: cc4es [options]',
145
+ '',
146
+ 'Without options, starts the browser GUI.',
147
+ '',
148
+ ' -h, --help Show this help and exit',
149
+ ' -V, --version Show version and exit',
150
+ ' --config-path Print the full config file path and exit',
151
+ ' --headless Build without server or browser',
152
+ ' -s, --src <directory> Source directory (headless)',
153
+ ' -d, --dst <file|directory> Output C file or directory (default: fsdata.c)',
154
+ ' -p, --port <1-65535> GUI server port',
155
+ ' --[no-]optimize-svg Enable or disable SVGO (default: enabled)',
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})`,
159
+ '',
160
+ 'HTML compression options:',
161
+ ' --[no-]collapse-whitespace --[no-]remove-comments',
162
+ ' --[no-]minify-js --[no-]minify-css',
163
+ ' --[no-]conditional-comments --[no-]decode-entities',
164
+ ' --[no-]remove-attribute-quotes --[no-]remove-empty-attributes',
165
+ ' --[no-]remove-redundant-attributes --[no-]use-short-doctype',
166
+ '',
167
+ 'Headless mode requires --src or --dst and never reruns the last build with',
168
+ 'no path options. A missing counterpart uses the last saved path; if no',
169
+ 'destination is saved, fsdata.c is created in the current working directory.'
170
+ ].join('\n');
171
+ };
package/dist/config.js ADDED
@@ -0,0 +1,90 @@
1
+ import fs from 'node:fs';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ import { isGzipLevel } from './gzip-options.js';
5
+ export const CONFIG_FILE_NAME = 'cc4es_configs.json';
6
+ const getStateDirectory = () => {
7
+ const homeDirectory = os.homedir();
8
+ if (process.platform === 'win32') {
9
+ return process.env.LOCALAPPDATA ?? path.join(homeDirectory, 'AppData', 'Local');
10
+ }
11
+ if (process.platform === 'darwin') {
12
+ return path.join(homeDirectory, 'Library', 'Application Support');
13
+ }
14
+ return process.env.XDG_STATE_HOME ?? path.join(homeDirectory, '.local', 'state');
15
+ };
16
+ export const getConfigFilePath = () => {
17
+ return path.join(getStateDirectory(), 'cc4-embedded-system', CONFIG_FILE_NAME);
18
+ };
19
+ const isRecord = (value) => {
20
+ return typeof value === 'object' && value !== null;
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
+ };
31
+ const readConfig = () => {
32
+ try {
33
+ const parsedConfig = JSON.parse(fs.readFileSync(getConfigFilePath(), 'utf8'));
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;
42
+ const src = parsedConfig.lastBuild.src;
43
+ const dst = parsedConfig.lastBuild.dst;
44
+ if (typeof src !== 'string' || typeof dst !== 'string' || !src || !dst) {
45
+ return config;
46
+ }
47
+ config.lastBuild = { src, dst };
48
+ return config;
49
+ }
50
+ catch {
51
+ return { schemaVersion: 2 };
52
+ }
53
+ };
54
+ export const getLastBuildPaths = () => {
55
+ return readConfig().lastBuild;
56
+ };
57
+ export const getLastGzipOptions = () => {
58
+ return readConfig().gzipOptions;
59
+ };
60
+ export const saveLastBuildPaths = (src, dst, gzipOptions) => {
61
+ const configFilePath = getConfigFilePath();
62
+ const temporaryFilePath = `${configFilePath}.${process.pid}.tmp`;
63
+ const configAlreadyExists = fs.existsSync(configFilePath);
64
+ const config = {
65
+ schemaVersion: 2,
66
+ lastBuild: { src, dst }
67
+ };
68
+ if (gzipOptions)
69
+ config.gzipOptions = gzipOptions;
70
+ try {
71
+ fs.mkdirSync(path.dirname(configFilePath), { recursive: true });
72
+ fs.writeFileSync(temporaryFilePath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
73
+ fs.renameSync(temporaryFilePath, configFilePath);
74
+ if (!configAlreadyExists)
75
+ console.log(`📝 Created config: ${configFilePath}`);
76
+ return true;
77
+ }
78
+ catch (error) {
79
+ const message = error instanceof Error ? error.message : String(error);
80
+ console.warn(`⚠️ Could not save ${CONFIG_FILE_NAME}: ${message}`);
81
+ try {
82
+ if (fs.existsSync(temporaryFilePath))
83
+ fs.unlinkSync(temporaryFilePath);
84
+ }
85
+ catch {
86
+ // Ignore cleanup errors because the build itself succeeded.
87
+ }
88
+ return false;
89
+ }
90
+ };