cc4-embedded-system 3.1.10 → 3.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -5
- package/dist/cli.js +146 -0
- package/dist/config.js +71 -0
- package/dist/gui.js +43 -281
- package/dist/makefsdata.js +29 -16
- package/dist/minify-options.js +38 -0
- package/dist/public/index.html +22 -10
- package/dist/server.js +262 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
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)
|
|
2
|
+
- This version is based on [html-minifier-next](https://github.com/j9t/html-minifier-next), [SVGO](https://github.com/svg/svgo), and a rewrite of [lwIP makefsdata](https://github.com/m-labs/lwip/tree/master/src/apps/httpd/makefsdata). GUI mode runs on localhost, default port: ```3000```.
|
|
3
3
|
- This tool is deployed on [npm package](https://www.npmjs.com/package/cc4-embedded-system).
|
|
4
|
+
- The last successful source and destination paths are automatically saved as `cc4es_configs.json` in the operating system's local application-data directory. On Windows this is `%LOCALAPPDATA%\\cc4-embedded-system\\cc4es_configs.json`.
|
|
5
|
+
- The config file is created only after the first successful build, and its complete path is printed at that time. Use `cc4es --config-path` to print it later.
|
|
4
6
|
|
|
5
7
|
## Demo
|
|
6
8
|
- Select input directory
|
|
@@ -17,9 +19,13 @@
|
|
|
17
19
|
```text
|
|
18
20
|
CC4EmbeddedSystem/
|
|
19
21
|
├── src/
|
|
20
|
-
│ ├── gui.ts
|
|
21
|
-
│
|
|
22
|
-
│
|
|
22
|
+
│ ├── gui.ts // CLI entry point
|
|
23
|
+
│ ├── cli.ts // Command-line parsing and help text
|
|
24
|
+
│ ├── server.ts // Express GUI server
|
|
25
|
+
│ ├── config.ts // Persistent last-build paths
|
|
26
|
+
│ ├── minify-options.ts // Shared HTML minifier options
|
|
27
|
+
│ ├── makefsdata.ts // Core C code generation and minification logic
|
|
28
|
+
│ └── utils.ts // Package version lookup
|
|
23
29
|
├── public/
|
|
24
30
|
│ └── index.html // Web GUI dashboard
|
|
25
31
|
├── Sereenshot/
|
|
@@ -34,6 +40,33 @@ CC4EmbeddedSystem/
|
|
|
34
40
|
```
|
|
35
41
|
|
|
36
42
|
## Commands
|
|
43
|
+
### CLI Help Texts
|
|
44
|
+
```text
|
|
45
|
+
Usage: cc4es [options]
|
|
46
|
+
|
|
47
|
+
Without options, starts the browser GUI.
|
|
48
|
+
|
|
49
|
+
-h, --help Show this help and exit
|
|
50
|
+
-V, --version Show version and exit
|
|
51
|
+
--config-path Print the full config file path and exit
|
|
52
|
+
--headless Build without server or browser
|
|
53
|
+
-s, --src <directory> Source directory (headless)
|
|
54
|
+
-d, --dst <file|directory> Output C file or directory (default: fsdata.c)
|
|
55
|
+
-p, --port <1-65535> GUI server port
|
|
56
|
+
--[no-]optimize-svg Enable or disable SVGO (default: enabled)
|
|
57
|
+
--svgo-multipass Run SVGO optimization repeatedly
|
|
58
|
+
|
|
59
|
+
HTML compression options:
|
|
60
|
+
--[no-]collapse-whitespace --[no-]remove-comments
|
|
61
|
+
--[no-]minify-js --[no-]minify-css
|
|
62
|
+
--[no-]conditional-comments --[no-]decode-entities
|
|
63
|
+
--[no-]remove-attribute-quotes --[no-]remove-empty-attributes
|
|
64
|
+
--[no-]remove-redundant-attributes --[no-]use-short-doctype
|
|
65
|
+
|
|
66
|
+
Headless mode requires --src or --dst and never reruns the last build with
|
|
67
|
+
no path options. A missing counterpart uses the last saved path; if no
|
|
68
|
+
destination is saved, fsdata.c is created in the current working directory.
|
|
69
|
+
```
|
|
37
70
|
### Normal Use
|
|
38
71
|
- Global installation
|
|
39
72
|
```bash
|
|
@@ -48,6 +81,29 @@ CC4EmbeddedSystem/
|
|
|
48
81
|
```bash
|
|
49
82
|
cc4es --port 3002
|
|
50
83
|
```
|
|
84
|
+
- Headless build (does not start a server or open a browser)
|
|
85
|
+
```bash
|
|
86
|
+
cc4es --headless --src C:\Project\web --dst C:\Project\fsdata.c
|
|
87
|
+
```
|
|
88
|
+
- Headless build with the default output filename. An existing output directory uses `fsdata.c` inside it.
|
|
89
|
+
```bash
|
|
90
|
+
cc4es --headless --src C:\Project\web
|
|
91
|
+
cc4es --headless --src C:\Project\web --dst C:\Project\output
|
|
92
|
+
```
|
|
93
|
+
- Headless safety: a parameterless command stops instead of rerunning the last build
|
|
94
|
+
```bash
|
|
95
|
+
cc4es --headless
|
|
96
|
+
```
|
|
97
|
+
- CLI help and version
|
|
98
|
+
```bash
|
|
99
|
+
cc4es --help
|
|
100
|
+
cc4es --version
|
|
101
|
+
cc4es --config-path
|
|
102
|
+
```
|
|
103
|
+
- Compression flags can be supplied in headless mode. Every flag accepts a `--no-` form.
|
|
104
|
+
```bash
|
|
105
|
+
cc4es --headless --no-minify-js --remove-attribute-quotes --svgo-multipass
|
|
106
|
+
```
|
|
51
107
|
- Update
|
|
52
108
|
```bash
|
|
53
109
|
npm install -g cc4-embedded-system@latest
|
|
@@ -61,6 +117,9 @@ npm run build
|
|
|
61
117
|
npm link
|
|
62
118
|
cc4es
|
|
63
119
|
|
|
120
|
+
# headless development test
|
|
121
|
+
npm run dev -- --headless --src C:\Project\web --dst C:\Project\fsdata.c
|
|
122
|
+
|
|
64
123
|
# publish
|
|
65
124
|
npm whoami # verify
|
|
66
125
|
npm login
|
|
@@ -68,4 +127,4 @@ npm version patch --no-git-tag-version # if increase version
|
|
|
68
127
|
npm run dev # dev mode for testing
|
|
69
128
|
npm run build # build the newest
|
|
70
129
|
npm publish --access public
|
|
71
|
-
```
|
|
130
|
+
```
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { DEFAULT_HTML_MINIFY_OPTIONS } from './minify-options.js';
|
|
2
|
+
export class CliUsageError extends Error {
|
|
3
|
+
constructor(message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.name = 'CliUsageError';
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
const MINIFY_FLAGS = {
|
|
9
|
+
'--collapse-whitespace': 'collapseWhitespace',
|
|
10
|
+
'--no-collapse-whitespace': 'collapseWhitespace',
|
|
11
|
+
'--remove-comments': 'removeComments',
|
|
12
|
+
'--no-remove-comments': 'removeComments',
|
|
13
|
+
'--minify-js': 'minifyJS',
|
|
14
|
+
'--no-minify-js': 'minifyJS',
|
|
15
|
+
'--minify-css': 'minifyCSS',
|
|
16
|
+
'--no-minify-css': 'minifyCSS',
|
|
17
|
+
'--conditional-comments': 'processConditionalComments',
|
|
18
|
+
'--no-conditional-comments': 'processConditionalComments',
|
|
19
|
+
'--decode-entities': 'decodeEntities',
|
|
20
|
+
'--no-decode-entities': 'decodeEntities',
|
|
21
|
+
'--remove-attribute-quotes': 'removeAttributeQuotes',
|
|
22
|
+
'--no-remove-attribute-quotes': 'removeAttributeQuotes',
|
|
23
|
+
'--remove-empty-attributes': 'removeEmptyAttributes',
|
|
24
|
+
'--no-remove-empty-attributes': 'removeEmptyAttributes',
|
|
25
|
+
'--remove-redundant-attributes': 'removeRedundantAttributes',
|
|
26
|
+
'--no-remove-redundant-attributes': 'removeRedundantAttributes',
|
|
27
|
+
'--use-short-doctype': 'useShortDoctype',
|
|
28
|
+
'--no-use-short-doctype': 'useShortDoctype'
|
|
29
|
+
};
|
|
30
|
+
const getRequiredValue = (args, index, option) => {
|
|
31
|
+
const value = args[index + 1];
|
|
32
|
+
if (!value || value.startsWith('-'))
|
|
33
|
+
throw new CliUsageError(`${option} requires a value.`);
|
|
34
|
+
return value;
|
|
35
|
+
};
|
|
36
|
+
const parsePort = (value) => {
|
|
37
|
+
if (!/^\d+$/.test(value))
|
|
38
|
+
throw new CliUsageError('--port must be an integer from 1 to 65535.');
|
|
39
|
+
const port = Number.parseInt(value, 10);
|
|
40
|
+
if (port < 1 || port > 65535)
|
|
41
|
+
throw new CliUsageError('--port must be an integer from 1 to 65535.');
|
|
42
|
+
return port;
|
|
43
|
+
};
|
|
44
|
+
export const parseCliArguments = (args) => {
|
|
45
|
+
const parsed = {
|
|
46
|
+
showHelp: false,
|
|
47
|
+
showVersion: false,
|
|
48
|
+
showConfigPath: false,
|
|
49
|
+
headless: false,
|
|
50
|
+
minifyOpts: { ...DEFAULT_HTML_MINIFY_OPTIONS },
|
|
51
|
+
optimizeSvg: true,
|
|
52
|
+
svgoMultipass: false
|
|
53
|
+
};
|
|
54
|
+
for (let index = 0; index < args.length; index++) {
|
|
55
|
+
const argument = args[index];
|
|
56
|
+
if (!argument)
|
|
57
|
+
continue;
|
|
58
|
+
const minifyOption = MINIFY_FLAGS[argument];
|
|
59
|
+
if (minifyOption) {
|
|
60
|
+
parsed.minifyOpts[minifyOption] = !argument.startsWith('--no-');
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
switch (argument) {
|
|
64
|
+
case '-h':
|
|
65
|
+
case '--help':
|
|
66
|
+
parsed.showHelp = true;
|
|
67
|
+
break;
|
|
68
|
+
case '-V':
|
|
69
|
+
case '--version':
|
|
70
|
+
parsed.showVersion = true;
|
|
71
|
+
break;
|
|
72
|
+
case '--config-path':
|
|
73
|
+
parsed.showConfigPath = true;
|
|
74
|
+
break;
|
|
75
|
+
case '--headless':
|
|
76
|
+
parsed.headless = true;
|
|
77
|
+
break;
|
|
78
|
+
case '-s':
|
|
79
|
+
case '--src':
|
|
80
|
+
case '--input':
|
|
81
|
+
parsed.inputPath = getRequiredValue(args, index, argument);
|
|
82
|
+
index++;
|
|
83
|
+
break;
|
|
84
|
+
case '-d':
|
|
85
|
+
case '--dst':
|
|
86
|
+
case '--output':
|
|
87
|
+
parsed.outputPath = getRequiredValue(args, index, argument);
|
|
88
|
+
index++;
|
|
89
|
+
break;
|
|
90
|
+
case '-p':
|
|
91
|
+
case '--port':
|
|
92
|
+
parsed.port = parsePort(getRequiredValue(args, index, argument));
|
|
93
|
+
index++;
|
|
94
|
+
break;
|
|
95
|
+
case '--optimize-svg':
|
|
96
|
+
parsed.optimizeSvg = true;
|
|
97
|
+
break;
|
|
98
|
+
case '--no-optimize-svg':
|
|
99
|
+
parsed.optimizeSvg = false;
|
|
100
|
+
break;
|
|
101
|
+
case '--svgo-multipass':
|
|
102
|
+
parsed.svgoMultipass = true;
|
|
103
|
+
break;
|
|
104
|
+
default:
|
|
105
|
+
throw new CliUsageError(`Unknown option: ${argument}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (parsed.headless && parsed.port !== undefined) {
|
|
109
|
+
throw new CliUsageError('--port is only available when starting the GUI.');
|
|
110
|
+
}
|
|
111
|
+
if (parsed.headless && !parsed.inputPath && !parsed.outputPath) {
|
|
112
|
+
throw new CliUsageError('Headless mode requires --src or --dst. It will not automatically rerun the last build.');
|
|
113
|
+
}
|
|
114
|
+
if (!parsed.headless && (parsed.inputPath || parsed.outputPath)) {
|
|
115
|
+
throw new CliUsageError('--src and --dst require --headless.');
|
|
116
|
+
}
|
|
117
|
+
return parsed;
|
|
118
|
+
};
|
|
119
|
+
export const getHelpText = () => {
|
|
120
|
+
return [
|
|
121
|
+
'Usage: cc4es [options]',
|
|
122
|
+
'',
|
|
123
|
+
'Without options, starts the browser GUI.',
|
|
124
|
+
'',
|
|
125
|
+
' -h, --help Show this help and exit',
|
|
126
|
+
' -V, --version Show version and exit',
|
|
127
|
+
' --config-path Print the full config file path and exit',
|
|
128
|
+
' --headless Build without server or browser',
|
|
129
|
+
' -s, --src <directory> Source directory (headless)',
|
|
130
|
+
' -d, --dst <file|directory> Output C file or directory (default: fsdata.c)',
|
|
131
|
+
' -p, --port <1-65535> GUI server port',
|
|
132
|
+
' --[no-]optimize-svg Enable or disable SVGO (default: enabled)',
|
|
133
|
+
' --svgo-multipass Run SVGO optimization repeatedly',
|
|
134
|
+
'',
|
|
135
|
+
'HTML compression options:',
|
|
136
|
+
' --[no-]collapse-whitespace --[no-]remove-comments',
|
|
137
|
+
' --[no-]minify-js --[no-]minify-css',
|
|
138
|
+
' --[no-]conditional-comments --[no-]decode-entities',
|
|
139
|
+
' --[no-]remove-attribute-quotes --[no-]remove-empty-attributes',
|
|
140
|
+
' --[no-]remove-redundant-attributes --[no-]use-short-doctype',
|
|
141
|
+
'',
|
|
142
|
+
'Headless mode requires --src or --dst and never reruns the last build with',
|
|
143
|
+
'no path options. A missing counterpart uses the last saved path; if no',
|
|
144
|
+
'destination is saved, fsdata.c is created in the current working directory.'
|
|
145
|
+
].join('\n');
|
|
146
|
+
};
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
export const CONFIG_FILE_NAME = 'cc4es_configs.json';
|
|
5
|
+
const getStateDirectory = () => {
|
|
6
|
+
const homeDirectory = os.homedir();
|
|
7
|
+
if (process.platform === 'win32') {
|
|
8
|
+
return process.env.LOCALAPPDATA ?? path.join(homeDirectory, 'AppData', 'Local');
|
|
9
|
+
}
|
|
10
|
+
if (process.platform === 'darwin') {
|
|
11
|
+
return path.join(homeDirectory, 'Library', 'Application Support');
|
|
12
|
+
}
|
|
13
|
+
return process.env.XDG_STATE_HOME ?? path.join(homeDirectory, '.local', 'state');
|
|
14
|
+
};
|
|
15
|
+
export const getConfigFilePath = () => {
|
|
16
|
+
return path.join(getStateDirectory(), 'cc4-embedded-system', CONFIG_FILE_NAME);
|
|
17
|
+
};
|
|
18
|
+
const isRecord = (value) => {
|
|
19
|
+
return typeof value === 'object' && value !== null;
|
|
20
|
+
};
|
|
21
|
+
const readConfig = () => {
|
|
22
|
+
try {
|
|
23
|
+
const parsedConfig = JSON.parse(fs.readFileSync(getConfigFilePath(), 'utf8'));
|
|
24
|
+
if (!isRecord(parsedConfig) || !isRecord(parsedConfig.lastBuild))
|
|
25
|
+
return { schemaVersion: 1 };
|
|
26
|
+
const src = parsedConfig.lastBuild.src;
|
|
27
|
+
const dst = parsedConfig.lastBuild.dst;
|
|
28
|
+
if (typeof src !== 'string' || typeof dst !== 'string' || !src || !dst) {
|
|
29
|
+
return { schemaVersion: 1 };
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
schemaVersion: 1,
|
|
33
|
+
lastBuild: { src, dst }
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return { schemaVersion: 1 };
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
export const getLastBuildPaths = () => {
|
|
41
|
+
return readConfig().lastBuild;
|
|
42
|
+
};
|
|
43
|
+
export const saveLastBuildPaths = (src, dst) => {
|
|
44
|
+
const configFilePath = getConfigFilePath();
|
|
45
|
+
const temporaryFilePath = `${configFilePath}.${process.pid}.tmp`;
|
|
46
|
+
const configAlreadyExists = fs.existsSync(configFilePath);
|
|
47
|
+
const config = {
|
|
48
|
+
schemaVersion: 1,
|
|
49
|
+
lastBuild: { src, dst }
|
|
50
|
+
};
|
|
51
|
+
try {
|
|
52
|
+
fs.mkdirSync(path.dirname(configFilePath), { recursive: true });
|
|
53
|
+
fs.writeFileSync(temporaryFilePath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
54
|
+
fs.renameSync(temporaryFilePath, configFilePath);
|
|
55
|
+
if (!configAlreadyExists)
|
|
56
|
+
console.log(`📝 Created config: ${configFilePath}`);
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
61
|
+
console.warn(`⚠️ Could not save ${CONFIG_FILE_NAME}: ${message}`);
|
|
62
|
+
try {
|
|
63
|
+
if (fs.existsSync(temporaryFilePath))
|
|
64
|
+
fs.unlinkSync(temporaryFilePath);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Ignore cleanup errors because the build itself succeeded.
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
};
|
package/dist/gui.js
CHANGED
|
@@ -1,302 +1,64 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
/*
|
|
3
|
-
* ==============================================================================
|
|
4
|
-
* CC4EmbeddedSystem V3 (makefsdata)
|
|
5
|
-
* Copyright (c) 2026 LunaticGhoulPiano / Emile Su
|
|
6
|
-
* Licensed under the MIT License.
|
|
7
|
-
* ==============================================================================
|
|
8
|
-
*
|
|
9
|
-
* This software integrates concepts, logic, and minification processes derived
|
|
10
|
-
* from the following open-source projects. We deeply appreciate their work:
|
|
11
|
-
*
|
|
12
|
-
* ------------------------------------------------------------------------------
|
|
13
|
-
* 1. lwIP (Lightweight TCP/IP stack)
|
|
14
|
-
* Repository: https://github.com/m-labs/lwip
|
|
15
|
-
* Copyright (c) 2001-2004 Swedish Institute of Computer Science.
|
|
16
|
-
* All rights reserved.
|
|
17
|
-
* * Redistribution and use in source and binary forms, with or without modification,
|
|
18
|
-
* are permitted provided that the following conditions are met:
|
|
19
|
-
* * 1. Redistributions of source code must retain the above copyright notice,
|
|
20
|
-
* this list of conditions and the following disclaimer.
|
|
21
|
-
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
22
|
-
* this list of conditions and the following disclaimer in the documentation
|
|
23
|
-
* and/or other materials provided with the distribution.
|
|
24
|
-
* 3. The name of the author may not be used to endorse or promote products
|
|
25
|
-
* derived from this software without specific prior written permission.
|
|
26
|
-
* * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
|
|
27
|
-
* WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
|
|
28
|
-
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
|
|
29
|
-
* SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
|
30
|
-
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
|
|
31
|
-
* OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
32
|
-
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
33
|
-
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
|
|
34
|
-
* IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
|
|
35
|
-
* OF SUCH DAMAGE.
|
|
36
|
-
*
|
|
37
|
-
* ------------------------------------------------------------------------------
|
|
38
|
-
* 2. html-minifier-next
|
|
39
|
-
* Repository: https://github.com/j9t/html-minifier-next
|
|
40
|
-
* Copyright (c) Jens Oliver Meiert (html-minifier-next)
|
|
41
|
-
* Copyright (c) Daniel Ruf (html-minifier-terser)
|
|
42
|
-
* Copyright (c) Juriy "kangax" Zaytsev (html-minifier)
|
|
43
|
-
* * Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
44
|
-
* of this software and associated documentation files (the "Software"), to deal
|
|
45
|
-
* in the Software without restriction, including without limitation the rights
|
|
46
|
-
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
47
|
-
* copies of the Software, and to permit persons to whom the Software is
|
|
48
|
-
* furnished to do so, subject to the following conditions:
|
|
49
|
-
* * The above copyright notice and this permission notice shall be included in
|
|
50
|
-
* all copies or substantial portions of the Software.
|
|
51
|
-
* * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
52
|
-
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
53
|
-
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
54
|
-
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
55
|
-
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
56
|
-
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
57
|
-
* THE SOFTWARE.
|
|
58
|
-
* ==============================================================================
|
|
59
|
-
*/
|
|
60
|
-
import express from 'express';
|
|
61
|
-
import open from 'open';
|
|
62
|
-
import fs from 'node:fs';
|
|
63
2
|
import path from 'node:path';
|
|
64
|
-
import {
|
|
3
|
+
import { CliUsageError, getHelpText, parseCliArguments } from './cli.js';
|
|
4
|
+
import { getConfigFilePath, getLastBuildPaths, saveLastBuildPaths } from './config.js';
|
|
65
5
|
import { runMakeFsData } from './makefsdata.js';
|
|
6
|
+
import { startGuiServer } from './server.js';
|
|
66
7
|
import { getPackageVersion } from './utils.js';
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
return;
|
|
73
|
-
console.error('⚠️ Uncaught Error:', err);
|
|
74
|
-
});
|
|
75
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
76
|
-
const __dirname = path.dirname(__filename);
|
|
77
|
-
const app = express();
|
|
78
|
-
// port set by command
|
|
79
|
-
let PORT = parseInt(process.env.PORT || '3000', 10);
|
|
80
|
-
const portArgIndex = process.argv.indexOf('--port');
|
|
81
|
-
if (portArgIndex !== -1 && process.argv[portArgIndex + 1]) {
|
|
82
|
-
const parsedPort = parseInt(process.argv[portArgIndex + 1], 10);
|
|
83
|
-
if (!isNaN(parsedPort))
|
|
84
|
-
PORT = parsedPort;
|
|
85
|
-
}
|
|
86
|
-
let activeBrowseProcess = null;
|
|
87
|
-
let server;
|
|
88
|
-
let shutdownTimer = null;
|
|
89
|
-
app.use(express.json());
|
|
90
|
-
const publicPath = fs.existsSync(path.join(__dirname, 'public'))
|
|
91
|
-
? path.join(__dirname, 'public')
|
|
92
|
-
: path.join(__dirname, '../public');
|
|
93
|
-
console.log(`[System] Serving static files from: ${publicPath}`);
|
|
94
|
-
app.use(express.static(publicPath));
|
|
95
|
-
const cancelShutdown = () => {
|
|
96
|
-
if (shutdownTimer) {
|
|
97
|
-
clearTimeout(shutdownTimer);
|
|
98
|
-
shutdownTimer = null;
|
|
99
|
-
// console.log('🛡️ Shutdown cancelled (keep alive)');
|
|
100
|
-
}
|
|
101
|
-
};
|
|
102
|
-
const scheduleShutdown = () => {
|
|
103
|
-
if (!shutdownTimer) {
|
|
104
|
-
shutdownTimer = setTimeout(() => {
|
|
105
|
-
closeBrowseProcess();
|
|
106
|
-
console.log('👋 Window closed, shutting down ...');
|
|
107
|
-
process.exit(0);
|
|
108
|
-
}, 2000);
|
|
109
|
-
}
|
|
8
|
+
const getDefaultGuiPort = () => {
|
|
9
|
+
const configuredPort = Number.parseInt(process.env.PORT ?? '3000', 10);
|
|
10
|
+
if (configuredPort < 1 || configuredPort > 65535 || Number.isNaN(configuredPort))
|
|
11
|
+
return 3000;
|
|
12
|
+
return configuredPort;
|
|
110
13
|
};
|
|
111
|
-
const
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
'
|
|
117
|
-
encodedCommand
|
|
118
|
-
];
|
|
119
|
-
};
|
|
120
|
-
const closeBrowseProcess = () => {
|
|
121
|
-
if (activeBrowseProcess && !activeBrowseProcess.killed) {
|
|
122
|
-
try {
|
|
123
|
-
activeBrowseProcess.kill();
|
|
124
|
-
}
|
|
125
|
-
catch (e) {
|
|
126
|
-
// ignore child cleanup error
|
|
127
|
-
}
|
|
14
|
+
const runHeadlessBuild = async (inputPath, outputPath, minifyOpts, optimizeSvg, svgoMultipass) => {
|
|
15
|
+
const lastBuild = getLastBuildPaths();
|
|
16
|
+
const selectedInputPath = inputPath ?? lastBuild?.src;
|
|
17
|
+
const selectedOutputPath = outputPath ?? lastBuild?.dst ?? 'fsdata.c';
|
|
18
|
+
if (!selectedInputPath) {
|
|
19
|
+
throw new CliUsageError('Headless mode requires --src, or a source path saved by a previous successful build.');
|
|
128
20
|
}
|
|
129
|
-
activeBrowseProcess = null;
|
|
130
|
-
};
|
|
131
|
-
const runDialogCommand = (command, args) => {
|
|
132
|
-
return new Promise((resolve, reject) => {
|
|
133
|
-
const child = execFile(command, args, {
|
|
134
|
-
windowsHide: false,
|
|
135
|
-
maxBuffer: 1024 * 1024
|
|
136
|
-
}, (error, stdout, stderr) => {
|
|
137
|
-
if (activeBrowseProcess === child)
|
|
138
|
-
activeBrowseProcess = null;
|
|
139
|
-
if (error) {
|
|
140
|
-
reject(new Error(stderr.trim() || error.message));
|
|
141
|
-
return;
|
|
142
|
-
}
|
|
143
|
-
resolve(stdout.trim());
|
|
144
|
-
});
|
|
145
|
-
activeBrowseProcess = child;
|
|
146
|
-
});
|
|
147
|
-
};
|
|
148
|
-
const buildWindowsDialogScript = (dialogLines) => {
|
|
149
|
-
return [
|
|
150
|
-
'Add-Type -AssemblyName System.Windows.Forms',
|
|
151
|
-
'Add-Type -AssemblyName System.Drawing',
|
|
152
|
-
'$screen = [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea',
|
|
153
|
-
'$owner = New-Object System.Windows.Forms.Form',
|
|
154
|
-
'$owner.StartPosition = [System.Windows.Forms.FormStartPosition]::Manual',
|
|
155
|
-
'$owner.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::None',
|
|
156
|
-
'$owner.ShowInTaskbar = $false',
|
|
157
|
-
'$owner.TopMost = $true',
|
|
158
|
-
'$owner.Width = 1',
|
|
159
|
-
'$owner.Height = 1',
|
|
160
|
-
'$owner.Left = $screen.Left + [int]($screen.Width / 2)',
|
|
161
|
-
'$owner.Top = $screen.Top + [int]($screen.Height / 2)',
|
|
162
|
-
'$owner.Opacity = 0',
|
|
163
|
-
'$null = $owner.Show()',
|
|
164
|
-
'$null = $owner.Activate()',
|
|
165
|
-
'$owner.BringToFront()',
|
|
166
|
-
'[System.Windows.Forms.Application]::DoEvents()',
|
|
167
|
-
...dialogLines,
|
|
168
|
-
'$owner.Close()',
|
|
169
|
-
'$owner.Dispose()'
|
|
170
|
-
].join('\n');
|
|
171
|
-
};
|
|
172
|
-
app.get('/api/version', (_req, res) => {
|
|
173
|
-
cancelShutdown();
|
|
174
|
-
res.json({ version: getPackageVersion() });
|
|
175
|
-
});
|
|
176
|
-
app.post('/api/shutdown', (_req, res) => {
|
|
177
|
-
res.json({ success: true });
|
|
178
|
-
closeBrowseProcess();
|
|
179
|
-
scheduleShutdown();
|
|
180
|
-
});
|
|
181
|
-
app.post('/api/cancel-shutdown', (_req, res) => {
|
|
182
|
-
cancelShutdown();
|
|
183
|
-
res.json({ success: true });
|
|
184
|
-
});
|
|
185
|
-
app.post('/api/build', async (req, res) => {
|
|
186
|
-
cancelShutdown();
|
|
187
|
-
const { inputPath, outputPath, minifyOpts } = req.body;
|
|
188
|
-
console.log(`[Build] Input: ${inputPath}`);
|
|
189
|
-
console.log(`[Build] Output: ${outputPath}`);
|
|
190
21
|
const opts = {
|
|
191
|
-
inputDir:
|
|
192
|
-
outputFile:
|
|
22
|
+
inputDir: path.resolve(selectedInputPath),
|
|
23
|
+
outputFile: path.resolve(selectedOutputPath),
|
|
193
24
|
processSubs: true,
|
|
194
25
|
includeHttpHeader: true,
|
|
195
26
|
useHttp11: false,
|
|
196
27
|
supportSsi: true,
|
|
197
28
|
precalcChksum: false,
|
|
198
|
-
minifyOpts
|
|
29
|
+
minifyOpts,
|
|
30
|
+
optimizeSvg,
|
|
31
|
+
svgoMultipass
|
|
199
32
|
};
|
|
33
|
+
const stats = await runMakeFsData(opts);
|
|
34
|
+
saveLastBuildPaths(opts.inputDir, opts.outputFile);
|
|
35
|
+
console.log(`✨ Build complete: ${stats.filesCount} file(s), ${stats.originalSize} -> ${stats.compressedSize} bytes.`);
|
|
36
|
+
};
|
|
37
|
+
const main = async () => {
|
|
200
38
|
try {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
39
|
+
const args = parseCliArguments(process.argv.slice(2));
|
|
40
|
+
if (args.showHelp) {
|
|
41
|
+
console.log(getHelpText());
|
|
42
|
+
return;
|
|
205
43
|
}
|
|
206
|
-
|
|
207
|
-
|
|
44
|
+
if (args.showVersion) {
|
|
45
|
+
console.log(getPackageVersion());
|
|
46
|
+
return;
|
|
208
47
|
}
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
stats: stats // originalSize, compressedSize, filesCount
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
catch (error) {
|
|
216
|
-
res.json({ success: false, message: error.message });
|
|
217
|
-
}
|
|
218
|
-
});
|
|
219
|
-
app.post('/api/change-port', (req, res) => {
|
|
220
|
-
cancelShutdown();
|
|
221
|
-
const parsedPort = parseInt(req.body.newPort, 10);
|
|
222
|
-
if (isNaN(parsedPort) || parsedPort < 1 || parsedPort > 65535) {
|
|
223
|
-
res.json({ success: false, message: 'Invalid port number.' });
|
|
224
|
-
return;
|
|
225
|
-
}
|
|
226
|
-
res.json({ success: true });
|
|
227
|
-
setTimeout(() => {
|
|
228
|
-
if (server) {
|
|
229
|
-
try {
|
|
230
|
-
server.closeAllConnections();
|
|
231
|
-
server.close(() => {
|
|
232
|
-
PORT = parsedPort;
|
|
233
|
-
startServer();
|
|
234
|
-
console.log(`🔄 Server successfully restarted on port ${PORT}`);
|
|
235
|
-
});
|
|
236
|
-
}
|
|
237
|
-
catch (e) {
|
|
238
|
-
startServer();
|
|
239
|
-
}
|
|
48
|
+
if (args.showConfigPath) {
|
|
49
|
+
console.log(getConfigFilePath());
|
|
50
|
+
return;
|
|
240
51
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
cancelShutdown();
|
|
245
|
-
res.on('close', () => {
|
|
246
|
-
if (!res.writableEnded) {
|
|
247
|
-
closeBrowseProcess();
|
|
248
|
-
scheduleShutdown();
|
|
52
|
+
if (args.headless) {
|
|
53
|
+
await runHeadlessBuild(args.inputPath, args.outputPath, args.minifyOpts, args.optimizeSvg, args.svgoMultipass);
|
|
54
|
+
return;
|
|
249
55
|
}
|
|
250
|
-
|
|
251
|
-
const isDir = req.query.type === 'dir';
|
|
252
|
-
const platform = os.platform();
|
|
253
|
-
try {
|
|
254
|
-
let result = '';
|
|
255
|
-
if (platform === 'win32') {
|
|
256
|
-
const script = isDir
|
|
257
|
-
? buildWindowsDialogScript([
|
|
258
|
-
'$dialog = New-Object System.Windows.Forms.FolderBrowserDialog',
|
|
259
|
-
'$dialog.Description = "Select Input Directory"',
|
|
260
|
-
'if ($dialog.ShowDialog($owner) -eq [System.Windows.Forms.DialogResult]::OK) {',
|
|
261
|
-
' $dialog.SelectedPath',
|
|
262
|
-
'}'
|
|
263
|
-
])
|
|
264
|
-
: buildWindowsDialogScript([
|
|
265
|
-
'$dialog = New-Object System.Windows.Forms.SaveFileDialog',
|
|
266
|
-
'$dialog.Filter = "C Files (*.c)|*.c|All Files (*.*)|*.*"',
|
|
267
|
-
'$dialog.DefaultExt = "c"',
|
|
268
|
-
'$dialog.AddExtension = $true',
|
|
269
|
-
'$dialog.OverwritePrompt = $true',
|
|
270
|
-
'if ($dialog.ShowDialog($owner) -eq [System.Windows.Forms.DialogResult]::OK) {',
|
|
271
|
-
' $dialog.FileName',
|
|
272
|
-
'}'
|
|
273
|
-
]);
|
|
274
|
-
result = await runDialogCommand('powershell.exe', buildPowerShellArgs(script));
|
|
275
|
-
}
|
|
276
|
-
else if (platform === 'darwin') {
|
|
277
|
-
result = await runDialogCommand('osascript', [
|
|
278
|
-
'-e',
|
|
279
|
-
isDir
|
|
280
|
-
? 'POSIX path of (choose folder with prompt "Select Input Directory")'
|
|
281
|
-
: 'POSIX path of (choose file name with prompt "Select Output File")'
|
|
282
|
-
]);
|
|
283
|
-
}
|
|
284
|
-
else {
|
|
285
|
-
result = await runDialogCommand('zenity', isDir
|
|
286
|
-
? ['--file-selection', '--directory', '--title=Select Input Directory']
|
|
287
|
-
: ['--file-selection', '--save', '--confirm-overwrite', '--title=Select Output File', '--filename=fsdata.c']);
|
|
288
|
-
}
|
|
289
|
-
res.json({ success: true, path: result || null });
|
|
56
|
+
startGuiServer(args.port ?? getDefaultGuiPort());
|
|
290
57
|
}
|
|
291
|
-
catch (
|
|
292
|
-
|
|
58
|
+
catch (error) {
|
|
59
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
60
|
+
console.error(`❌ ${message}`);
|
|
61
|
+
process.exitCode = 1;
|
|
293
62
|
}
|
|
294
|
-
});
|
|
295
|
-
const startServer = () => {
|
|
296
|
-
server = app.listen(PORT, () => {
|
|
297
|
-
console.log(`✨ GUI Server started on http://localhost:${PORT}`);
|
|
298
|
-
});
|
|
299
63
|
};
|
|
300
|
-
|
|
301
|
-
startServer();
|
|
302
|
-
open(`http://localhost:${PORT}`);
|
|
64
|
+
void main();
|
package/dist/makefsdata.js
CHANGED
|
@@ -60,21 +60,14 @@
|
|
|
60
60
|
import fs from 'node:fs';
|
|
61
61
|
import path from 'node:path';
|
|
62
62
|
import { minify } from 'html-minifier-next';
|
|
63
|
+
import { optimize } from 'svgo';
|
|
63
64
|
import { getPackageVersion } from './utils.js';
|
|
65
|
+
import { DEFAULT_HTML_MINIFY_OPTIONS } from './minify-options.js';
|
|
64
66
|
// ----------------------------------------------------------------------
|
|
65
67
|
// 1. Configs & Version Recovery
|
|
66
68
|
// ----------------------------------------------------------------------
|
|
67
69
|
const TCP_MSS = 1460;
|
|
68
70
|
const LWIP_VERSION = "1.3.1"; // this makefsdata.ts is based on lwIP v1.3.1
|
|
69
|
-
// html-minifier-next options
|
|
70
|
-
const COMPRESS_OPTS_DEFAULT = {
|
|
71
|
-
collapseWhitespace: true,
|
|
72
|
-
removeComments: true,
|
|
73
|
-
minifyJS: true,
|
|
74
|
-
minifyCSS: true,
|
|
75
|
-
processConditionalComments: true,
|
|
76
|
-
decodeEntities: true
|
|
77
|
-
};
|
|
78
71
|
const REDIRHOME_PATH = '/redirhome.html';
|
|
79
72
|
// ----------------------------------------------------------------------
|
|
80
73
|
// 3. lwIP Helpers
|
|
@@ -107,6 +100,8 @@ function getMimeType(fileName) {
|
|
|
107
100
|
return 'text/xml';
|
|
108
101
|
case '.json':
|
|
109
102
|
return 'application/json';
|
|
103
|
+
case '.svg':
|
|
104
|
+
return 'image/svg+xml';
|
|
110
105
|
default:
|
|
111
106
|
return 'text/plain';
|
|
112
107
|
}
|
|
@@ -190,12 +185,17 @@ function bufferToHexCArray(buf) {
|
|
|
190
185
|
}
|
|
191
186
|
export async function runMakeFsData(opts) {
|
|
192
187
|
console.log(`🚀 CC4EmbeddedSystem V3: Starting makefsdata compilation...`);
|
|
193
|
-
//
|
|
188
|
+
// Check source directory before making any output directory changes.
|
|
194
189
|
if (!fs.existsSync(opts.inputDir)) {
|
|
195
|
-
|
|
196
|
-
fs.mkdirSync(opts.inputDir, { recursive: true });
|
|
190
|
+
throw new Error(`Input directory not found: ${path.resolve(opts.inputDir)}`);
|
|
197
191
|
}
|
|
198
|
-
if (!opts.
|
|
192
|
+
if (!fs.statSync(opts.inputDir).isDirectory())
|
|
193
|
+
throw new Error(`Input path is not a directory: ${path.resolve(opts.inputDir)}`);
|
|
194
|
+
if (fs.existsSync(opts.outputFile) && fs.statSync(opts.outputFile).isDirectory()) {
|
|
195
|
+
opts.outputFile = path.join(opts.outputFile, 'fsdata.c');
|
|
196
|
+
console.log(`🔧 Output directory selected. Using default file -> ${opts.outputFile}`);
|
|
197
|
+
}
|
|
198
|
+
else if (!opts.outputFile.toLowerCase().endsWith('.c')) {
|
|
199
199
|
opts.outputFile += '.c';
|
|
200
200
|
console.log(`🔧 Auto-appended '.c' to output file -> ${opts.outputFile}`);
|
|
201
201
|
}
|
|
@@ -208,8 +208,7 @@ export async function runMakeFsData(opts) {
|
|
|
208
208
|
let totalCompressedSize = 0;
|
|
209
209
|
if (allFiles.length === 0)
|
|
210
210
|
throw new Error(`Input directory is empty! Please put your web files (.html, .css, etc.) into:\n${path.resolve(opts.inputDir)}`);
|
|
211
|
-
|
|
212
|
-
const activeCompressOpts = opts.minifyOpts || COMPRESS_OPTS_DEFAULT;
|
|
211
|
+
const activeCompressOpts = opts.minifyOpts ?? DEFAULT_HTML_MINIFY_OPTIONS;
|
|
213
212
|
for (let i = 0; i < allFiles.length; i++) {
|
|
214
213
|
const filePath = allFiles[i];
|
|
215
214
|
const relativePath = '/' + path.relative(opts.inputDir, filePath).replace(/\\/g, '/');
|
|
@@ -217,7 +216,21 @@ export async function runMakeFsData(opts) {
|
|
|
217
216
|
let content = fs.readFileSync(filePath);
|
|
218
217
|
const originalFileSize = content.length;
|
|
219
218
|
totalOriginalSize += originalFileSize;
|
|
220
|
-
if (
|
|
219
|
+
if (ext === '.svg' && opts.optimizeSvg !== false) {
|
|
220
|
+
try {
|
|
221
|
+
const optimizedSvg = optimize(content.toString('utf8'), {
|
|
222
|
+
path: filePath,
|
|
223
|
+
multipass: opts.svgoMultipass ?? false
|
|
224
|
+
});
|
|
225
|
+
content = Buffer.from(optimizedSvg.data, 'utf8');
|
|
226
|
+
console.log(`🖼️ Optimized SVG: ${relativePath} (${originalFileSize} -> ${content.length} bytes)`);
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
230
|
+
throw new Error(`Failed to optimize SVG ${relativePath}: ${message}`);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
else if (['.html', '.htm', '.css', '.js'].includes(ext)) {
|
|
221
234
|
const minifiedStr = await minify(content.toString('utf8'), activeCompressOpts);
|
|
222
235
|
if (typeof minifiedStr === 'string') {
|
|
223
236
|
content = Buffer.from(minifiedStr, 'utf8');
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
export const DEFAULT_HTML_MINIFY_OPTIONS = {
|
|
2
|
+
collapseWhitespace: true,
|
|
3
|
+
removeComments: true,
|
|
4
|
+
minifyJS: true,
|
|
5
|
+
minifyCSS: true,
|
|
6
|
+
processConditionalComments: true,
|
|
7
|
+
decodeEntities: true,
|
|
8
|
+
removeAttributeQuotes: false,
|
|
9
|
+
removeEmptyAttributes: false,
|
|
10
|
+
removeRedundantAttributes: false,
|
|
11
|
+
useShortDoctype: false
|
|
12
|
+
};
|
|
13
|
+
const HTML_MINIFY_OPTION_KEYS = [
|
|
14
|
+
'collapseWhitespace',
|
|
15
|
+
'removeComments',
|
|
16
|
+
'minifyJS',
|
|
17
|
+
'minifyCSS',
|
|
18
|
+
'processConditionalComments',
|
|
19
|
+
'decodeEntities',
|
|
20
|
+
'removeAttributeQuotes',
|
|
21
|
+
'removeEmptyAttributes',
|
|
22
|
+
'removeRedundantAttributes',
|
|
23
|
+
'useShortDoctype'
|
|
24
|
+
];
|
|
25
|
+
const isRecord = (value) => {
|
|
26
|
+
return typeof value === 'object' && value !== null;
|
|
27
|
+
};
|
|
28
|
+
export const normalizeHtmlMinifyOptions = (value) => {
|
|
29
|
+
const normalized = { ...DEFAULT_HTML_MINIFY_OPTIONS };
|
|
30
|
+
if (!isRecord(value))
|
|
31
|
+
return normalized;
|
|
32
|
+
for (const key of HTML_MINIFY_OPTION_KEYS) {
|
|
33
|
+
const optionValue = value[key];
|
|
34
|
+
if (typeof optionValue === 'boolean')
|
|
35
|
+
normalized[key] = optionValue;
|
|
36
|
+
}
|
|
37
|
+
return normalized;
|
|
38
|
+
};
|
package/dist/public/index.html
CHANGED
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
<div class="form-group">
|
|
65
65
|
<label>Output File (fsdata.c)</label>
|
|
66
66
|
<div class="input-row">
|
|
67
|
-
<input type="text" id="outputPath" placeholder="e.g., ./fsdata.c">
|
|
67
|
+
<input type="text" id="outputPath" value="fsdata.c" placeholder="e.g., ./fsdata.c">
|
|
68
68
|
<button type="button" class="btn-browse" onclick="handleBrowse('outputPath', 'file')">Browse ...</button>
|
|
69
69
|
</div>
|
|
70
70
|
</div>
|
|
@@ -204,15 +204,27 @@
|
|
|
204
204
|
}
|
|
205
205
|
}, true);
|
|
206
206
|
|
|
207
|
-
window.addEventListener('DOMContentLoaded', async () => {
|
|
208
|
-
await fetch('/api/cancel-shutdown', { method: 'POST' });
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
207
|
+
window.addEventListener('DOMContentLoaded', async () => {
|
|
208
|
+
await fetch('/api/cancel-shutdown', { method: 'POST' });
|
|
209
|
+
|
|
210
|
+
try {
|
|
211
|
+
const res = await fetch('/api/version');
|
|
212
|
+
const data = await res.json();
|
|
213
|
+
if (data.version) document.getElementById('app-title-text').innerText = `CC4EmbeddedSystem V3 (${data.version})`;
|
|
214
|
+
}
|
|
215
|
+
catch (error) { console.error('Can\'t get version!', error); }
|
|
216
|
+
|
|
217
|
+
try {
|
|
218
|
+
const res = await fetch('/api/config');
|
|
219
|
+
const data = await res.json();
|
|
220
|
+
|
|
221
|
+
if (data.lastBuild) {
|
|
222
|
+
document.getElementById('inputPath').value = data.lastBuild.src || '';
|
|
223
|
+
document.getElementById('outputPath').value = data.lastBuild.dst || '';
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
catch (error) { console.error('Can\'t load saved paths!', error); }
|
|
227
|
+
});
|
|
216
228
|
|
|
217
229
|
async function changePort() {
|
|
218
230
|
const newPort = document.getElementById('guiPort').value.trim();
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import { execFile } from 'node:child_process';
|
|
3
|
+
import fs from 'node:fs';
|
|
4
|
+
import os from 'node:os';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import open from 'open';
|
|
8
|
+
import { getLastBuildPaths, saveLastBuildPaths } from './config.js';
|
|
9
|
+
import { runMakeFsData } from './makefsdata.js';
|
|
10
|
+
import { normalizeHtmlMinifyOptions } from './minify-options.js';
|
|
11
|
+
import { getPackageVersion } from './utils.js';
|
|
12
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
13
|
+
const __dirname = path.dirname(__filename);
|
|
14
|
+
const buildPowerShellArgs = (script) => {
|
|
15
|
+
const encodedCommand = Buffer.from(script, 'utf16le').toString('base64');
|
|
16
|
+
return [
|
|
17
|
+
'-NoProfile',
|
|
18
|
+
'-STA',
|
|
19
|
+
'-EncodedCommand',
|
|
20
|
+
encodedCommand
|
|
21
|
+
];
|
|
22
|
+
};
|
|
23
|
+
const buildWindowsDialogScript = (dialogLines) => {
|
|
24
|
+
return [
|
|
25
|
+
'Add-Type -AssemblyName System.Windows.Forms',
|
|
26
|
+
'Add-Type -AssemblyName System.Drawing',
|
|
27
|
+
'$screen = [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea',
|
|
28
|
+
'$owner = New-Object System.Windows.Forms.Form',
|
|
29
|
+
'$owner.StartPosition = [System.Windows.Forms.FormStartPosition]::Manual',
|
|
30
|
+
'$owner.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::None',
|
|
31
|
+
'$owner.ShowInTaskbar = $false',
|
|
32
|
+
'$owner.TopMost = $true',
|
|
33
|
+
'$owner.Width = 1',
|
|
34
|
+
'$owner.Height = 1',
|
|
35
|
+
'$owner.Left = $screen.Left + [int]($screen.Width / 2)',
|
|
36
|
+
'$owner.Top = $screen.Top + [int]($screen.Height / 2)',
|
|
37
|
+
'$owner.Opacity = 0',
|
|
38
|
+
'$null = $owner.Show()',
|
|
39
|
+
'$null = $owner.Activate()',
|
|
40
|
+
'$owner.BringToFront()',
|
|
41
|
+
'[System.Windows.Forms.Application]::DoEvents()',
|
|
42
|
+
...dialogLines,
|
|
43
|
+
'$owner.Close()',
|
|
44
|
+
'$owner.Dispose()'
|
|
45
|
+
].join('\n');
|
|
46
|
+
};
|
|
47
|
+
const isNonEmptyString = (value) => {
|
|
48
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
49
|
+
};
|
|
50
|
+
export const startGuiServer = (initialPort) => {
|
|
51
|
+
const app = express();
|
|
52
|
+
let port = initialPort;
|
|
53
|
+
let activeBrowseProcess = null;
|
|
54
|
+
let server = null;
|
|
55
|
+
let shutdownTimer = null;
|
|
56
|
+
const publicPath = fs.existsSync(path.join(__dirname, 'public'))
|
|
57
|
+
? path.join(__dirname, 'public')
|
|
58
|
+
: path.join(__dirname, '../public');
|
|
59
|
+
const closeBrowseProcess = () => {
|
|
60
|
+
if (activeBrowseProcess && !activeBrowseProcess.killed) {
|
|
61
|
+
try {
|
|
62
|
+
activeBrowseProcess.kill();
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
// Ignore child cleanup errors.
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
activeBrowseProcess = null;
|
|
69
|
+
};
|
|
70
|
+
const cancelShutdown = () => {
|
|
71
|
+
if (!shutdownTimer)
|
|
72
|
+
return;
|
|
73
|
+
clearTimeout(shutdownTimer);
|
|
74
|
+
shutdownTimer = null;
|
|
75
|
+
};
|
|
76
|
+
const scheduleShutdown = () => {
|
|
77
|
+
if (shutdownTimer)
|
|
78
|
+
return;
|
|
79
|
+
shutdownTimer = setTimeout(() => {
|
|
80
|
+
closeBrowseProcess();
|
|
81
|
+
console.log('👋 Window closed, shutting down ...');
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}, 2000);
|
|
84
|
+
};
|
|
85
|
+
const runDialogCommand = (command, args) => {
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
const child = execFile(command, args, {
|
|
88
|
+
windowsHide: false,
|
|
89
|
+
maxBuffer: 1024 * 1024
|
|
90
|
+
}, (error, stdout, stderr) => {
|
|
91
|
+
if (activeBrowseProcess === child)
|
|
92
|
+
activeBrowseProcess = null;
|
|
93
|
+
if (error) {
|
|
94
|
+
reject(new Error(stderr.trim() || error.message));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
resolve(stdout.trim());
|
|
98
|
+
});
|
|
99
|
+
activeBrowseProcess = child;
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
const startServer = (openBrowser) => {
|
|
103
|
+
server = app.listen(port, () => {
|
|
104
|
+
const serverUrl = `http://localhost:${port}`;
|
|
105
|
+
console.log(`✨ GUI Server started on ${serverUrl}`);
|
|
106
|
+
if (openBrowser)
|
|
107
|
+
void open(serverUrl).catch(() => undefined);
|
|
108
|
+
});
|
|
109
|
+
server.on('error', (error) => {
|
|
110
|
+
if (error.code === 'EADDRINUSE') {
|
|
111
|
+
console.error(`❌ Port ${port} is already in use.`);
|
|
112
|
+
process.exitCode = 1;
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
console.error(`❌ GUI server error: ${error.message}`);
|
|
116
|
+
process.exitCode = 1;
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
app.use(express.json());
|
|
120
|
+
app.use(express.static(publicPath));
|
|
121
|
+
app.get('/api/version', (_req, res) => {
|
|
122
|
+
cancelShutdown();
|
|
123
|
+
res.json({ version: getPackageVersion() });
|
|
124
|
+
});
|
|
125
|
+
app.get('/api/config', (_req, res) => {
|
|
126
|
+
cancelShutdown();
|
|
127
|
+
res.json({ lastBuild: getLastBuildPaths() ?? null });
|
|
128
|
+
});
|
|
129
|
+
app.post('/api/shutdown', (_req, res) => {
|
|
130
|
+
res.json({ success: true });
|
|
131
|
+
closeBrowseProcess();
|
|
132
|
+
scheduleShutdown();
|
|
133
|
+
});
|
|
134
|
+
app.post('/api/cancel-shutdown', (_req, res) => {
|
|
135
|
+
cancelShutdown();
|
|
136
|
+
res.json({ success: true });
|
|
137
|
+
});
|
|
138
|
+
app.post('/api/build', async (req, res) => {
|
|
139
|
+
cancelShutdown();
|
|
140
|
+
const inputPath = isNonEmptyString(req.body.inputPath) ? req.body.inputPath.trim() : '';
|
|
141
|
+
const outputPath = isNonEmptyString(req.body.outputPath) ? req.body.outputPath.trim() : '';
|
|
142
|
+
if (!inputPath || !outputPath) {
|
|
143
|
+
res.status(400).json({ success: false, message: 'Input and output paths are required.' });
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
const opts = {
|
|
147
|
+
inputDir: path.resolve(inputPath),
|
|
148
|
+
outputFile: path.resolve(outputPath),
|
|
149
|
+
processSubs: true,
|
|
150
|
+
includeHttpHeader: true,
|
|
151
|
+
useHttp11: false,
|
|
152
|
+
supportSsi: true,
|
|
153
|
+
precalcChksum: false,
|
|
154
|
+
minifyOpts: normalizeHtmlMinifyOptions(req.body.minifyOpts),
|
|
155
|
+
optimizeSvg: true,
|
|
156
|
+
svgoMultipass: false
|
|
157
|
+
};
|
|
158
|
+
console.log(`[Build] Input: ${opts.inputDir}`);
|
|
159
|
+
console.log(`[Build] Output: ${opts.outputFile}`);
|
|
160
|
+
try {
|
|
161
|
+
const stats = await runMakeFsData(opts);
|
|
162
|
+
saveLastBuildPaths(opts.inputDir, opts.outputFile);
|
|
163
|
+
try {
|
|
164
|
+
await open(path.dirname(opts.outputFile));
|
|
165
|
+
}
|
|
166
|
+
catch {
|
|
167
|
+
// The generated result is still valid if the folder cannot be opened.
|
|
168
|
+
}
|
|
169
|
+
res.json({ success: true, stats });
|
|
170
|
+
}
|
|
171
|
+
catch (error) {
|
|
172
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
173
|
+
res.json({ success: false, message });
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
app.post('/api/change-port', (req, res) => {
|
|
177
|
+
cancelShutdown();
|
|
178
|
+
const parsedPort = Number.parseInt(String(req.body.newPort), 10);
|
|
179
|
+
if (Number.isNaN(parsedPort) || parsedPort < 1 || parsedPort > 65535) {
|
|
180
|
+
res.status(400).json({ success: false, message: 'Invalid port number.' });
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
res.json({ success: true });
|
|
184
|
+
setTimeout(() => {
|
|
185
|
+
if (!server) {
|
|
186
|
+
port = parsedPort;
|
|
187
|
+
startServer(false);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
try {
|
|
191
|
+
server.closeAllConnections();
|
|
192
|
+
server.close((error) => {
|
|
193
|
+
if (error) {
|
|
194
|
+
console.error(`❌ Could not restart GUI server: ${error.message}`);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
port = parsedPort;
|
|
198
|
+
startServer(false);
|
|
199
|
+
console.log(`🔄 GUI server restarted on port ${port}`);
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
port = parsedPort;
|
|
204
|
+
startServer(false);
|
|
205
|
+
}
|
|
206
|
+
}, 100);
|
|
207
|
+
});
|
|
208
|
+
app.get('/api/browse', async (req, res) => {
|
|
209
|
+
cancelShutdown();
|
|
210
|
+
res.on('close', () => {
|
|
211
|
+
if (!res.writableEnded) {
|
|
212
|
+
closeBrowseProcess();
|
|
213
|
+
scheduleShutdown();
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
const isDirectory = req.query.type === 'dir';
|
|
217
|
+
const platform = os.platform();
|
|
218
|
+
try {
|
|
219
|
+
let selectedPath = '';
|
|
220
|
+
if (platform === 'win32') {
|
|
221
|
+
const script = isDirectory
|
|
222
|
+
? buildWindowsDialogScript([
|
|
223
|
+
'$dialog = New-Object System.Windows.Forms.FolderBrowserDialog',
|
|
224
|
+
'$dialog.Description = "Select Input Directory"',
|
|
225
|
+
'if ($dialog.ShowDialog($owner) -eq [System.Windows.Forms.DialogResult]::OK) {',
|
|
226
|
+
' $dialog.SelectedPath',
|
|
227
|
+
'}'
|
|
228
|
+
])
|
|
229
|
+
: buildWindowsDialogScript([
|
|
230
|
+
'$dialog = New-Object System.Windows.Forms.SaveFileDialog',
|
|
231
|
+
'$dialog.Filter = "C Files (*.c)|*.c|All Files (*.*)|*.*"',
|
|
232
|
+
'$dialog.DefaultExt = "c"',
|
|
233
|
+
'$dialog.AddExtension = $true',
|
|
234
|
+
'$dialog.OverwritePrompt = $true',
|
|
235
|
+
'if ($dialog.ShowDialog($owner) -eq [System.Windows.Forms.DialogResult]::OK) {',
|
|
236
|
+
' $dialog.FileName',
|
|
237
|
+
'}'
|
|
238
|
+
]);
|
|
239
|
+
selectedPath = await runDialogCommand('powershell.exe', buildPowerShellArgs(script));
|
|
240
|
+
}
|
|
241
|
+
else if (platform === 'darwin') {
|
|
242
|
+
selectedPath = await runDialogCommand('osascript', [
|
|
243
|
+
'-e',
|
|
244
|
+
isDirectory
|
|
245
|
+
? 'POSIX path of (choose folder with prompt "Select Input Directory")'
|
|
246
|
+
: 'POSIX path of (choose file name with prompt "Select Output File")'
|
|
247
|
+
]);
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
selectedPath = await runDialogCommand('zenity', isDirectory
|
|
251
|
+
? ['--file-selection', '--directory', '--title=Select Input Directory']
|
|
252
|
+
: ['--file-selection', '--save', '--confirm-overwrite', '--title=Select Output File', '--filename=fsdata.c']);
|
|
253
|
+
}
|
|
254
|
+
res.json({ success: true, path: selectedPath || null });
|
|
255
|
+
}
|
|
256
|
+
catch {
|
|
257
|
+
res.json({ success: true, path: null });
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
console.log(`[System] Serving static files from: ${publicPath}`);
|
|
261
|
+
startServer(true);
|
|
262
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc4-embedded-system",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.2.0",
|
|
4
4
|
"description": "A modern typescript version of makefsdata based on html-minifier-next and lwIP v1.3.1",
|
|
5
5
|
"bin": {
|
|
6
6
|
"cc4es": "dist/gui.js"
|
|
@@ -30,7 +30,8 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"express": "^5.2.1",
|
|
32
32
|
"html-minifier-next": "^6.1.2",
|
|
33
|
-
"open": "^11.0.0"
|
|
33
|
+
"open": "^11.0.0",
|
|
34
|
+
"svgo": "^4.0.2"
|
|
34
35
|
},
|
|
35
36
|
"devDependencies": {
|
|
36
37
|
"@types/express": "^5.0.6",
|