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.
@@ -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
+ };
@@ -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>Converted: <span id="convSize" style="color: #dcdcaa;">0</span> KB</span>
108
+ <div class="stats-info">
109
+ <span>Compressed: <span id="compSize" style="color: #9cdcfe;">0</span> KB</span>
110
+ <span>Stored: <span id="storedSize" style="color: #4ec9b0;">0</span> KB</span>
111
+ <span>Converted: <span id="convSize" style="color: #dcdcaa;">0</span> KB</span>
102
112
  <span>Original: <span id="origSize" style="color: #ce9178;">0</span> KB</span>
103
113
  </div>
104
114
  <div class="bar-container">
@@ -164,10 +174,16 @@
164
174
  let isBrowsePending = false;
165
175
  document.getElementById('guiPort').value = window.location.port || '80';
166
176
 
167
- function setBrowseState(active) {
168
- isBrowsing = active;
169
- document.body.classList.toggle('browse-locked', active);
170
- }
177
+ function setBrowseState(active) {
178
+ isBrowsing = active;
179
+ document.body.classList.toggle('browse-locked', active);
180
+ }
181
+
182
+ function syncGzipLevel() {
183
+ const enabled = document.getElementById('opt-gzip').checked;
184
+ document.getElementById('gzip-level-control').hidden = !enabled;
185
+ document.getElementById('opt-gzipLevel').disabled = !enabled;
186
+ }
171
187
 
172
188
  async function handleBrowse(targetId, type) {
173
189
  if (isBrowsing || isBrowsePending) return;
@@ -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
- try {
210
- const res = await fetch('/api/version');
211
- const data = await res.json();
212
- if (data.version) document.getElementById('app-title-text').innerText = `CC4EmbeddedSystem V3 (${data.version})`;
213
- }
214
- catch (error) { console.error('Can\'t get version!', error); }
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 res = await fetch('/api/build', {
253
- method: 'POST',
254
- headers: { 'Content-Type': 'application/json' },
255
- body: JSON.stringify({ inputPath: inPath, outputPath: outPath, minifyOpts: minifyOptions })
285
+ useShortDoctype: document.getElementById('opt-useShortDoctype').checked
286
+ };
287
+ const gzip = document.getElementById('opt-gzip').checked;
288
+ const gzipLevel = Number(document.getElementById('opt-gzipLevel').value);
289
+ if (!Number.isInteger(gzipLevel) || gzipLevel < 1 || gzipLevel > 9) {
290
+ alert('❌ gzip level must be an integer from 1 to 9.');
291
+ return;
292
+ }
293
+
294
+ const res = await fetch('/api/build', {
295
+ method: 'POST',
296
+ headers: { 'Content-Type': 'application/json' },
297
+ body: JSON.stringify({ inputPath: inPath, outputPath: outPath, minifyOpts: minifyOptions, gzip, gzipLevel })
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 convKB = (convertedSize / 1024).toFixed(2);
269
-
270
- // ratio: the saved percentage
271
- const ratio = originalSize > 0 ? (((originalSize - compressedSize) / originalSize) * 100).toFixed(1) : 0;
307
+ const { originalSize, compressedSize, storedSize, convertedSize } = result.stats;
308
+ const origKB = (originalSize / 1024).toFixed(2);
309
+ const compKB = (compressedSize / 1024).toFixed(2);
310
+ const storedKB = (storedSize / 1024).toFixed(2);
311
+ const convKB = (convertedSize / 1024).toFixed(2);
312
+
313
+ // ratio: the saved percentage
314
+ const ratio = originalSize > 0 ? (((originalSize - storedSize) / originalSize) * 100).toFixed(1) : 0;
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('convSize').innerText = convKB;
318
+ document.getElementById('origSize').innerText = origKB;
319
+ document.getElementById('compSize').innerText = compKB;
320
+ document.getElementById('storedSize').innerText = storedKB;
321
+ document.getElementById('convSize').innerText = convKB;
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 saved: ${(origKB - compKB).toFixed(2)} KB`;
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.10",
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
- "open": "^11.0.0"
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",