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 +167 -19
- package/dist/cli.js +171 -0
- package/dist/config.js +90 -0
- package/dist/gui.js +50 -281
- package/dist/gzip-options.js +10 -0
- package/dist/makefsdata.js +152 -32
- package/dist/minify-options.js +38 -0
- package/dist/public/index.html +85 -41
- package/dist/server.js +289 -0
- package/package.json +6 -2
|
@@ -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
|
@@ -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; }
|
|
@@ -64,7 +69,7 @@
|
|
|
64
69
|
<div class="form-group">
|
|
65
70
|
<label>Output File (fsdata.c)</label>
|
|
66
71
|
<div class="input-row">
|
|
67
|
-
<input type="text" id="outputPath" placeholder="e.g., ./fsdata.c">
|
|
72
|
+
<input type="text" id="outputPath" value="fsdata.c" placeholder="e.g., ./fsdata.c">
|
|
68
73
|
<button type="button" class="btn-browse" onclick="handleBrowse('outputPath', 'file')">Browse ...</button>
|
|
69
74
|
</div>
|
|
70
75
|
</div>
|
|
@@ -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;
|
|
@@ -204,15 +220,35 @@
|
|
|
204
220
|
}
|
|
205
221
|
}, true);
|
|
206
222
|
|
|
207
|
-
window.addEventListener('DOMContentLoaded', async () => {
|
|
208
|
-
await fetch('/api/cancel-shutdown', { method: 'POST' });
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
223
|
+
window.addEventListener('DOMContentLoaded', async () => {
|
|
224
|
+
await fetch('/api/cancel-shutdown', { method: 'POST' });
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const res = await fetch('/api/version');
|
|
228
|
+
const data = await res.json();
|
|
229
|
+
if (data.version) document.getElementById('app-title-text').innerText = `CC4EmbeddedSystem V3 (${data.version})`;
|
|
230
|
+
}
|
|
231
|
+
catch (error) { console.error('Can\'t get version!', error); }
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
const res = await fetch('/api/config');
|
|
235
|
+
const data = await res.json();
|
|
236
|
+
|
|
237
|
+
if (data.lastBuild) {
|
|
238
|
+
document.getElementById('inputPath').value = data.lastBuild.src || '';
|
|
239
|
+
document.getElementById('outputPath').value = data.lastBuild.dst || '';
|
|
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();
|
|
247
|
+
}
|
|
248
|
+
catch (error) { console.error('Can\'t load saved paths!', error); }
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
document.getElementById('opt-gzip').addEventListener('change', syncGzipLevel);
|
|
216
252
|
|
|
217
253
|
async function changePort() {
|
|
218
254
|
const newPort = document.getElementById('guiPort').value.trim();
|
|
@@ -236,7 +272,7 @@
|
|
|
236
272
|
const outPath = document.getElementById('outputPath').value.trim();
|
|
237
273
|
if (! inPath || ! outPath) { alert('❌ Paths required.'); return; }
|
|
238
274
|
|
|
239
|
-
const minifyOptions = {
|
|
275
|
+
const minifyOptions = {
|
|
240
276
|
collapseWhitespace: document.getElementById('opt-collapseWhitespace').checked,
|
|
241
277
|
removeComments: document.getElementById('opt-removeComments').checked,
|
|
242
278
|
minifyJS: document.getElementById('opt-minifyJS').checked,
|
|
@@ -246,13 +282,19 @@
|
|
|
246
282
|
removeAttributeQuotes: document.getElementById('opt-removeAttributeQuotes').checked,
|
|
247
283
|
removeEmptyAttributes: document.getElementById('opt-removeEmptyAttributes').checked,
|
|
248
284
|
removeRedundantAttributes: document.getElementById('opt-removeRedundantAttributes').checked,
|
|
249
|
-
useShortDoctype: document.getElementById('opt-useShortDoctype').checked
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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 })
|
|
256
298
|
});
|
|
257
299
|
const result = await res.json();
|
|
258
300
|
|
|
@@ -262,19 +304,21 @@
|
|
|
262
304
|
const panel = document.getElementById('statsPanel');
|
|
263
305
|
panel.style.display = 'block';
|
|
264
306
|
|
|
265
|
-
const { originalSize, compressedSize, convertedSize } = result.stats;
|
|
266
|
-
const origKB = (originalSize / 1024).toFixed(2);
|
|
267
|
-
const compKB = (compressedSize / 1024).toFixed(2);
|
|
268
|
-
const
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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;
|
|
272
315
|
// compressedRatio: the remainings percentage
|
|
273
316
|
const compressedRatio = (100 - ratio).toFixed(1);
|
|
274
317
|
|
|
275
|
-
document.getElementById('origSize').innerText = origKB;
|
|
276
|
-
document.getElementById('compSize').innerText = compKB;
|
|
277
|
-
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;
|
|
278
322
|
document.getElementById('barText').innerText = `${ratio}% Size Reduced`;
|
|
279
323
|
|
|
280
324
|
// actual used percentage
|
|
@@ -282,7 +326,7 @@
|
|
|
282
326
|
document.getElementById('barFill').style.width = compressedRatio + '%';
|
|
283
327
|
}, 50);
|
|
284
328
|
|
|
285
|
-
document.getElementById('savingsInfo').innerText = `✨ Total
|
|
329
|
+
document.getElementById('savingsInfo').innerText = `✨ Total stored saving: ${(origKB - storedKB).toFixed(2)} KB`;
|
|
286
330
|
}
|
|
287
331
|
|
|
288
332
|
alert('✨ Success! C Code generated at ' + outPath);
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,289 @@
|
|
|
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, getLastGzipOptions, saveLastBuildPaths } from './config.js';
|
|
9
|
+
import { DEFAULT_GZIP_LEVEL, isGzipLevel } from './gzip-options.js';
|
|
10
|
+
import { runMakeFsData } from './makefsdata.js';
|
|
11
|
+
import { normalizeHtmlMinifyOptions } from './minify-options.js';
|
|
12
|
+
import { getPackageVersion } from './utils.js';
|
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
14
|
+
const __dirname = path.dirname(__filename);
|
|
15
|
+
const buildPowerShellArgs = (script) => {
|
|
16
|
+
const encodedCommand = Buffer.from(script, 'utf16le').toString('base64');
|
|
17
|
+
return [
|
|
18
|
+
'-NoProfile',
|
|
19
|
+
'-STA',
|
|
20
|
+
'-EncodedCommand',
|
|
21
|
+
encodedCommand
|
|
22
|
+
];
|
|
23
|
+
};
|
|
24
|
+
const buildWindowsDialogScript = (dialogLines) => {
|
|
25
|
+
return [
|
|
26
|
+
'Add-Type -AssemblyName System.Windows.Forms',
|
|
27
|
+
'Add-Type -AssemblyName System.Drawing',
|
|
28
|
+
'$screen = [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea',
|
|
29
|
+
'$owner = New-Object System.Windows.Forms.Form',
|
|
30
|
+
'$owner.StartPosition = [System.Windows.Forms.FormStartPosition]::Manual',
|
|
31
|
+
'$owner.FormBorderStyle = [System.Windows.Forms.FormBorderStyle]::None',
|
|
32
|
+
'$owner.ShowInTaskbar = $false',
|
|
33
|
+
'$owner.TopMost = $true',
|
|
34
|
+
'$owner.Width = 1',
|
|
35
|
+
'$owner.Height = 1',
|
|
36
|
+
'$owner.Left = $screen.Left + [int]($screen.Width / 2)',
|
|
37
|
+
'$owner.Top = $screen.Top + [int]($screen.Height / 2)',
|
|
38
|
+
'$owner.Opacity = 0',
|
|
39
|
+
'$null = $owner.Show()',
|
|
40
|
+
'$null = $owner.Activate()',
|
|
41
|
+
'$owner.BringToFront()',
|
|
42
|
+
'[System.Windows.Forms.Application]::DoEvents()',
|
|
43
|
+
...dialogLines,
|
|
44
|
+
'$owner.Close()',
|
|
45
|
+
'$owner.Dispose()'
|
|
46
|
+
].join('\n');
|
|
47
|
+
};
|
|
48
|
+
const isNonEmptyString = (value) => {
|
|
49
|
+
return typeof value === 'string' && value.trim().length > 0;
|
|
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
|
+
};
|
|
64
|
+
export const startGuiServer = (initialPort) => {
|
|
65
|
+
const app = express();
|
|
66
|
+
let port = initialPort;
|
|
67
|
+
let activeBrowseProcess = null;
|
|
68
|
+
let server = null;
|
|
69
|
+
let shutdownTimer = null;
|
|
70
|
+
const publicPath = fs.existsSync(path.join(__dirname, 'public'))
|
|
71
|
+
? path.join(__dirname, 'public')
|
|
72
|
+
: path.join(__dirname, '../public');
|
|
73
|
+
const closeBrowseProcess = () => {
|
|
74
|
+
if (activeBrowseProcess && !activeBrowseProcess.killed) {
|
|
75
|
+
try {
|
|
76
|
+
activeBrowseProcess.kill();
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Ignore child cleanup errors.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
activeBrowseProcess = null;
|
|
83
|
+
};
|
|
84
|
+
const cancelShutdown = () => {
|
|
85
|
+
if (!shutdownTimer)
|
|
86
|
+
return;
|
|
87
|
+
clearTimeout(shutdownTimer);
|
|
88
|
+
shutdownTimer = null;
|
|
89
|
+
};
|
|
90
|
+
const scheduleShutdown = () => {
|
|
91
|
+
if (shutdownTimer)
|
|
92
|
+
return;
|
|
93
|
+
shutdownTimer = setTimeout(() => {
|
|
94
|
+
closeBrowseProcess();
|
|
95
|
+
console.log('👋 Window closed, shutting down ...');
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}, 2000);
|
|
98
|
+
};
|
|
99
|
+
const runDialogCommand = (command, args) => {
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
const child = execFile(command, args, {
|
|
102
|
+
windowsHide: false,
|
|
103
|
+
maxBuffer: 1024 * 1024
|
|
104
|
+
}, (error, stdout, stderr) => {
|
|
105
|
+
if (activeBrowseProcess === child)
|
|
106
|
+
activeBrowseProcess = null;
|
|
107
|
+
if (error) {
|
|
108
|
+
reject(new Error(stderr.trim() || error.message));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
resolve(stdout.trim());
|
|
112
|
+
});
|
|
113
|
+
activeBrowseProcess = child;
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
const startServer = (openBrowser) => {
|
|
117
|
+
server = app.listen(port, () => {
|
|
118
|
+
const serverUrl = `http://localhost:${port}`;
|
|
119
|
+
console.log(`✨ GUI Server started on ${serverUrl}`);
|
|
120
|
+
if (openBrowser)
|
|
121
|
+
void open(serverUrl).catch(() => undefined);
|
|
122
|
+
});
|
|
123
|
+
server.on('error', (error) => {
|
|
124
|
+
if (error.code === 'EADDRINUSE') {
|
|
125
|
+
console.error(`❌ Port ${port} is already in use.`);
|
|
126
|
+
process.exitCode = 1;
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
console.error(`❌ GUI server error: ${error.message}`);
|
|
130
|
+
process.exitCode = 1;
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
app.use(express.json());
|
|
134
|
+
app.use(express.static(publicPath));
|
|
135
|
+
app.get('/api/version', (_req, res) => {
|
|
136
|
+
cancelShutdown();
|
|
137
|
+
res.json({ version: getPackageVersion() });
|
|
138
|
+
});
|
|
139
|
+
app.get('/api/config', (_req, res) => {
|
|
140
|
+
cancelShutdown();
|
|
141
|
+
res.json({
|
|
142
|
+
lastBuild: getLastBuildPaths() ?? null,
|
|
143
|
+
gzipOptions: getLastGzipOptions() ?? { gzip: false, gzipLevel: DEFAULT_GZIP_LEVEL }
|
|
144
|
+
});
|
|
145
|
+
});
|
|
146
|
+
app.post('/api/shutdown', (_req, res) => {
|
|
147
|
+
res.json({ success: true });
|
|
148
|
+
closeBrowseProcess();
|
|
149
|
+
scheduleShutdown();
|
|
150
|
+
});
|
|
151
|
+
app.post('/api/cancel-shutdown', (_req, res) => {
|
|
152
|
+
cancelShutdown();
|
|
153
|
+
res.json({ success: true });
|
|
154
|
+
});
|
|
155
|
+
app.post('/api/build', async (req, res) => {
|
|
156
|
+
cancelShutdown();
|
|
157
|
+
const inputPath = isNonEmptyString(req.body.inputPath) ? req.body.inputPath.trim() : '';
|
|
158
|
+
const outputPath = isNonEmptyString(req.body.outputPath) ? req.body.outputPath.trim() : '';
|
|
159
|
+
if (!inputPath || !outputPath) {
|
|
160
|
+
res.status(400).json({ success: false, message: 'Input and output paths are required.' });
|
|
161
|
+
return;
|
|
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
|
+
}
|
|
172
|
+
const opts = {
|
|
173
|
+
inputDir: path.resolve(inputPath),
|
|
174
|
+
outputFile: path.resolve(outputPath),
|
|
175
|
+
processSubs: true,
|
|
176
|
+
includeHttpHeader: true,
|
|
177
|
+
useHttp11: false,
|
|
178
|
+
supportSsi: true,
|
|
179
|
+
precalcChksum: false,
|
|
180
|
+
minifyOpts: normalizeHtmlMinifyOptions(req.body.minifyOpts),
|
|
181
|
+
optimizeSvg: true,
|
|
182
|
+
svgoMultipass: false,
|
|
183
|
+
...gzipOptions
|
|
184
|
+
};
|
|
185
|
+
console.log(`[Build] Input: ${opts.inputDir}`);
|
|
186
|
+
console.log(`[Build] Output: ${opts.outputFile}`);
|
|
187
|
+
try {
|
|
188
|
+
const stats = await runMakeFsData(opts);
|
|
189
|
+
saveLastBuildPaths(opts.inputDir, opts.outputFile, gzipOptions);
|
|
190
|
+
try {
|
|
191
|
+
await open(path.dirname(opts.outputFile));
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// The generated result is still valid if the folder cannot be opened.
|
|
195
|
+
}
|
|
196
|
+
res.json({ success: true, stats });
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
200
|
+
res.json({ success: false, message });
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
app.post('/api/change-port', (req, res) => {
|
|
204
|
+
cancelShutdown();
|
|
205
|
+
const parsedPort = Number.parseInt(String(req.body.newPort), 10);
|
|
206
|
+
if (Number.isNaN(parsedPort) || parsedPort < 1 || parsedPort > 65535) {
|
|
207
|
+
res.status(400).json({ success: false, message: 'Invalid port number.' });
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
res.json({ success: true });
|
|
211
|
+
setTimeout(() => {
|
|
212
|
+
if (!server) {
|
|
213
|
+
port = parsedPort;
|
|
214
|
+
startServer(false);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
server.closeAllConnections();
|
|
219
|
+
server.close((error) => {
|
|
220
|
+
if (error) {
|
|
221
|
+
console.error(`❌ Could not restart GUI server: ${error.message}`);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
port = parsedPort;
|
|
225
|
+
startServer(false);
|
|
226
|
+
console.log(`🔄 GUI server restarted on port ${port}`);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
catch {
|
|
230
|
+
port = parsedPort;
|
|
231
|
+
startServer(false);
|
|
232
|
+
}
|
|
233
|
+
}, 100);
|
|
234
|
+
});
|
|
235
|
+
app.get('/api/browse', async (req, res) => {
|
|
236
|
+
cancelShutdown();
|
|
237
|
+
res.on('close', () => {
|
|
238
|
+
if (!res.writableEnded) {
|
|
239
|
+
closeBrowseProcess();
|
|
240
|
+
scheduleShutdown();
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
const isDirectory = req.query.type === 'dir';
|
|
244
|
+
const platform = os.platform();
|
|
245
|
+
try {
|
|
246
|
+
let selectedPath = '';
|
|
247
|
+
if (platform === 'win32') {
|
|
248
|
+
const script = isDirectory
|
|
249
|
+
? buildWindowsDialogScript([
|
|
250
|
+
'$dialog = New-Object System.Windows.Forms.FolderBrowserDialog',
|
|
251
|
+
'$dialog.Description = "Select Input Directory"',
|
|
252
|
+
'if ($dialog.ShowDialog($owner) -eq [System.Windows.Forms.DialogResult]::OK) {',
|
|
253
|
+
' $dialog.SelectedPath',
|
|
254
|
+
'}'
|
|
255
|
+
])
|
|
256
|
+
: buildWindowsDialogScript([
|
|
257
|
+
'$dialog = New-Object System.Windows.Forms.SaveFileDialog',
|
|
258
|
+
'$dialog.Filter = "C Files (*.c)|*.c|All Files (*.*)|*.*"',
|
|
259
|
+
'$dialog.DefaultExt = "c"',
|
|
260
|
+
'$dialog.AddExtension = $true',
|
|
261
|
+
'$dialog.OverwritePrompt = $true',
|
|
262
|
+
'if ($dialog.ShowDialog($owner) -eq [System.Windows.Forms.DialogResult]::OK) {',
|
|
263
|
+
' $dialog.FileName',
|
|
264
|
+
'}'
|
|
265
|
+
]);
|
|
266
|
+
selectedPath = await runDialogCommand('powershell.exe', buildPowerShellArgs(script));
|
|
267
|
+
}
|
|
268
|
+
else if (platform === 'darwin') {
|
|
269
|
+
selectedPath = await runDialogCommand('osascript', [
|
|
270
|
+
'-e',
|
|
271
|
+
isDirectory
|
|
272
|
+
? 'POSIX path of (choose folder with prompt "Select Input Directory")'
|
|
273
|
+
: 'POSIX path of (choose file name with prompt "Select Output File")'
|
|
274
|
+
]);
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
selectedPath = await runDialogCommand('zenity', isDirectory
|
|
278
|
+
? ['--file-selection', '--directory', '--title=Select Input Directory']
|
|
279
|
+
: ['--file-selection', '--save', '--confirm-overwrite', '--title=Select Output File', '--filename=fsdata.c']);
|
|
280
|
+
}
|
|
281
|
+
res.json({ success: true, path: selectedPath || null });
|
|
282
|
+
}
|
|
283
|
+
catch {
|
|
284
|
+
res.json({ success: true, path: null });
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
console.log(`[System] Serving static files from: ${publicPath}`);
|
|
288
|
+
startServer(true);
|
|
289
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cc4-embedded-system",
|
|
3
|
-
"version": "3.1
|
|
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,9 +29,12 @@
|
|
|
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",
|
|
33
|
-
"
|
|
35
|
+
"lightningcss": "^1.32.0",
|
|
36
|
+
"open": "^11.0.0",
|
|
37
|
+
"svgo": "^4.0.2"
|
|
34
38
|
},
|
|
35
39
|
"devDependencies": {
|
|
36
40
|
"@types/express": "^5.0.6",
|