svelteesp32 1.16.1 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -6,6 +6,10 @@
6
6
 
7
7
  [Changelog](CHANGELOG.md)
8
8
 
9
+ <p align="center">
10
+ <img src="svelteesp32.png" alt="svelteesp32" />
11
+ </p>
12
+
9
13
  ---
10
14
 
11
15
  ## Why SvelteESP32?
@@ -21,7 +25,7 @@
21
25
  - **Smart Caching** — Built-in SHA256 ETags deliver HTTP 304 responses, slashing bandwidth on constrained devices.
22
26
  - **CI/CD Ready** — Simple npm package that slots into any build pipeline.
23
27
  - **Zero Runtime Overhead** — Data served directly from flash. No filesystem reads, no RAM allocation.
24
- - **4 Web Server Engines** — PsychicHttp V1/V2, ESPAsyncWebServer, and native ESP-IDF supported.
28
+ - **3 Web Server Engines** — PsychicHttpServer, ESPAsyncWebServer, and native ESP-IDF supported.
25
29
 
26
30
  ---
27
31
 
@@ -75,13 +79,14 @@ void setup() {
75
79
 
76
80
  ## What's New
77
81
 
82
+ - **v2.0.1** — Dependency updates (ESLint v10, eslint-plugin-unicorn 63), improved error cause chaining
83
+ - **v2.0.0** — **BREAKING**: PsychicHttpServer V2 is now the default `psychic` engine. The `psychic2` engine has been removed. Dry run mode, C++ identifier validation, improved MIME type warnings
78
84
  - **v1.16.0** — Size budget constraints (`--maxsize`, `--maxgzipsize`)
79
85
  - **v1.15.0** — `--basepath` for multiple frontends (e.g., `/admin`, `/app`)
80
86
  - **v1.13.0** — npm package variable interpolation in RC files
81
87
  - **v1.12.0** — RC file configuration support
82
88
  - **v1.11.0** — File exclusion patterns
83
89
  - **v1.9.0** — Native ESP-IDF engine
84
- - **v1.5.0** — PsychicHttp V2 support
85
90
 
86
91
  ---
87
92
 
@@ -108,9 +113,6 @@ Choose your web server engine:
108
113
  # PsychicHttpServer (recommended for ESP32)
109
114
  npx svelteesp32 -e psychic -s ./dist -o ./esp32/svelteesp32.h --etag=true
110
115
 
111
- # PsychicHttpServer V2
112
- npx svelteesp32 -e psychic2 -s ./dist -o ./esp32/svelteesp32.h --etag=true
113
-
114
116
  # ESPAsyncWebServer (ESP32 + ESP8266)
115
117
  npx svelteesp32 -e async -s ./dist -o ./esp32/svelteesp32.h --etag=true
116
118
 
@@ -141,7 +143,7 @@ Watch your files get optimized in real-time:
141
143
 
142
144
  ### ESP32 Integration
143
145
 
144
- **PsychicHttpServer (Recommended)**
146
+ **PsychicHttpServer V2 (Recommended)**
145
147
 
146
148
  ```c
147
149
  #include <PsychicHttp.h>
@@ -184,14 +186,14 @@ void app_main() {
184
186
  }
185
187
  ```
186
188
 
187
- Working examples: [Arduino/PlatformIO](demo/esp32) | [ESP-IDF](demo/esp32idf)
189
+ Working examples with LED control via web interface: [Arduino/PlatformIO](demo/esp32) | [ESP-IDF](demo/esp32idf)
188
190
 
189
191
  ### What Gets Generated
190
192
 
191
193
  The generated header file includes everything your ESP needs:
192
194
 
193
195
  ```c
194
- //engine: PsychicHttpServer
196
+ //engine: PsychicHttpServer V2
195
197
  //config: engine=psychic sourcepath=./dist outputfile=./output.h etag=true gzip=true cachetime=0 espmethod=initSvelteStaticFiles define=SVELTEESP32
196
198
  //
197
199
  #define SVELTEESP32_COUNT 5
@@ -232,23 +234,21 @@ const size_t SVELTEESP32_FILE_COUNT = sizeof(SVELTEESP32_FILES) / sizeof(SVELTEE
232
234
  extern "C" void __attribute__((weak)) SVELTEESP32_onFileServed(const char* path, int statusCode) {}
233
235
 
234
236
  void initSvelteStaticFiles(PsychicHttpServer * server) {
235
- server->on("/assets/index-KwubEIf-.js", HTTP_GET, [](PsychicRequest * request) {
237
+ server->on("/assets/index-KwubEIf-.js", HTTP_GET, [](PsychicRequest * request, PsychicResponse * response) {
236
238
  if (request->hasHeader("If-None-Match") &&
237
239
  request->header("If-None-Match").equals(etag_assets_index_KwubEIf__js)) {
238
- PsychicResponse response304(request);
239
- response304.setCode(304);
240
+ response->setCode(304);
240
241
  SVELTEESP32_onFileServed("/assets/index-KwubEIf-.js", 304);
241
- return response304.send();
242
+ return response->send();
242
243
  }
243
244
 
244
- PsychicResponse response(request);
245
- response.setContentType("text/javascript");
246
- response.addHeader("Content-Encoding", "gzip");
247
- response.addHeader("Cache-Control", "no-cache");
248
- response.addHeader("ETag", etag_assets_index_KwubEIf__js);
249
- response.setContent(datagzip_assets_index_KwubEIf__js, 12547);
245
+ response->setContentType("text/javascript");
246
+ response->addHeader("Content-Encoding", "gzip");
247
+ response->addHeader("Cache-Control", "no-cache");
248
+ response->addHeader("ETag", etag_assets_index_KwubEIf__js);
249
+ response->setContent(datagzip_assets_index_KwubEIf__js, 12547);
250
250
  SVELTEESP32_onFileServed("/assets/index-KwubEIf-.js", 200);
251
- return response.send();
251
+ return response->send();
252
252
  });
253
253
 
254
254
  // ... more routes
@@ -259,14 +259,13 @@ void initSvelteStaticFiles(PsychicHttpServer * server) {
259
259
 
260
260
  ## Supported Web Server Engines
261
261
 
262
- | Engine | Flag | Best For | Platform |
263
- | --------------------- | ------------- | ---------------------------- | --------------- |
264
- | **PsychicHttp V1** | `-e psychic` | Maximum performance | ESP32 only |
265
- | **PsychicHttp V2** | `-e psychic2` | Modern API + performance | ESP32 only |
266
- | **ESPAsyncWebServer** | `-e async` | Cross-platform compatibility | ESP32 + ESP8266 |
267
- | **Native ESP-IDF** | `-e espidf` | Pure ESP-IDF projects | ESP32 only |
262
+ | Engine | Flag | Best For | Platform |
263
+ | ------------------------ | ------------ | ---------------------------- | --------------- |
264
+ | **PsychicHttpServer V2** | `-e psychic` | Maximum performance | ESP32 only |
265
+ | **ESPAsyncWebServer** | `-e async` | Cross-platform compatibility | ESP32 + ESP8266 |
266
+ | **Native ESP-IDF** | `-e espidf` | Pure ESP-IDF projects | ESP32 only |
268
267
 
269
- **Recommendation:** For ESP32-only projects, use PsychicHttpServer for the fastest, most stable experience.
268
+ **Recommendation:** For ESP32-only projects, use PsychicHttpServer V2 (`-e psychic`) for the fastest, most stable experience.
270
269
 
271
270
  **Note:** For PsychicHttp, configure `server.config.max_uri_handlers` to match your file count.
272
271
 
@@ -289,7 +288,7 @@ Reduce bandwidth dramatically with HTTP 304 "Not Modified" responses. When a bro
289
288
  - **Minimal overhead** — adds ~1-3% code size for significant bandwidth savings
290
289
  - **Compiler mode** — use `--etag=compiler` and control via `-D SVELTEESP32_ENABLE_ETAG`
291
290
 
292
- All four engines support full ETag validation.
291
+ All three engines support full ETag validation.
293
292
 
294
293
  ### Browser Cache Control
295
294
 
@@ -403,24 +402,25 @@ Called for every response (200 = content served, 304 = cache hit).
403
402
 
404
403
  ## CLI Reference
405
404
 
406
- | Option | Description | Default |
407
- | ---------------- | ------------------------------------------------- | ----------------------- |
408
- | `-s` | Source folder with compiled web files | (required) |
409
- | `-e` | Web server engine (psychic/psychic2/async/espidf) | `psychic` |
410
- | `-o` | Output header file path | `svelteesp32.h` |
411
- | `--etag` | ETag caching (true/false/compiler) | `false` |
412
- | `--gzip` | Gzip compression (true/false/compiler) | `true` |
413
- | `--exclude` | Exclude files by glob pattern | System files |
414
- | `--basepath` | URL prefix for all routes | (none) |
415
- | `--maxsize` | Max total uncompressed size (e.g., `400k`, `1m`) | (none) |
416
- | `--maxgzipsize` | Max total gzip size (e.g., `150k`, `500k`) | (none) |
417
- | `--cachetime` | Cache-Control max-age in seconds | `0` |
418
- | `--version` | Version string in header | (none) |
419
- | `--define` | C++ define prefix | `SVELTEESP32` |
420
- | `--espmethod` | Init function name | `initSvelteStaticFiles` |
421
- | `--config` | Custom RC file path | `.svelteesp32rc.json` |
422
- | `--noindexcheck` | Skip index.html validation | `false` |
423
- | `-h` | Show help | |
405
+ | Option | Description | Default |
406
+ | ---------------- | ------------------------------------------------ | ----------------------- |
407
+ | `-s` | Source folder with compiled web files | (required) |
408
+ | `-e` | Web server engine (psychic/async/espidf) | `psychic` |
409
+ | `-o` | Output header file path | `svelteesp32.h` |
410
+ | `--etag` | ETag caching (true/false/compiler) | `false` |
411
+ | `--gzip` | Gzip compression (true/false/compiler) | `true` |
412
+ | `--exclude` | Exclude files by glob pattern | System files |
413
+ | `--basepath` | URL prefix for all routes | (none) |
414
+ | `--maxsize` | Max total uncompressed size (e.g., `400k`, `1m`) | (none) |
415
+ | `--maxgzipsize` | Max total gzip size (e.g., `150k`, `500k`) | (none) |
416
+ | `--cachetime` | Cache-Control max-age in seconds | `0` |
417
+ | `--version` | Version string in header | (none) |
418
+ | `--define` | C++ define prefix | `SVELTEESP32` |
419
+ | `--espmethod` | Init function name | `initSvelteStaticFiles` |
420
+ | `--config` | Custom RC file path | `.svelteesp32rc.json` |
421
+ | `--dryrun` | Show summary without writing output | `false` |
422
+ | `--noindexcheck` | Skip index.html validation | `false` |
423
+ | `-h` | Show help | |
424
424
 
425
425
  ---
426
426
 
@@ -439,7 +439,8 @@ Store your settings in `.svelteesp32rc.json` for zero-argument builds:
439
439
  "basepath": "/ui",
440
440
  "maxsize": "400k",
441
441
  "maxgzipsize": "150k",
442
- "noindexcheck": false
442
+ "noindexcheck": false,
443
+ "dryrun": false
443
444
  }
444
445
  ```
445
446
 
@@ -1,5 +1,5 @@
1
1
  interface ICopyFilesArguments {
2
- engine: 'psychic' | 'psychic2' | 'async' | 'espidf';
2
+ engine: 'psychic' | 'async' | 'espidf';
3
3
  sourcepath: string;
4
4
  outputfile: string;
5
5
  espmethod: string;
@@ -14,10 +14,11 @@ interface ICopyFilesArguments {
14
14
  maxSize?: number;
15
15
  maxGzipSize?: number;
16
16
  noIndexCheck?: boolean;
17
+ dryRun?: boolean;
17
18
  help?: boolean;
18
19
  }
19
20
  interface IRcFileConfig {
20
- engine?: 'psychic' | 'psychic2' | 'async' | 'espidf';
21
+ engine?: 'psychic' | 'async' | 'espidf';
21
22
  sourcepath?: string;
22
23
  outputfile?: string;
23
24
  espmethod?: string;
@@ -32,11 +33,13 @@ interface IRcFileConfig {
32
33
  maxsize?: number | string;
33
34
  maxgzipsize?: number | string;
34
35
  noindexcheck?: boolean;
36
+ dryrun?: boolean;
35
37
  }
38
+ declare function validateCppIdentifier(value: string, name: string): string;
36
39
  declare function parseSize(value: string, name: string): number;
37
40
  declare function getNpmPackageVariable(packageJson: Record<string, unknown>, variableName: string): string | undefined;
38
41
  declare function hasNpmVariables(config: IRcFileConfig): boolean;
39
42
  declare function interpolateNpmVariables(config: IRcFileConfig, rcFilePath: string): IRcFileConfig;
40
- export { getNpmPackageVariable, hasNpmVariables, interpolateNpmVariables, parseSize };
43
+ export { getNpmPackageVariable, hasNpmVariables, interpolateNpmVariables, parseSize, validateCppIdentifier };
41
44
  export declare function formatConfiguration(cmdLine: ICopyFilesArguments): string;
42
45
  export declare const cmdLine: ICopyFilesArguments;
@@ -8,6 +8,7 @@ exports.getNpmPackageVariable = getNpmPackageVariable;
8
8
  exports.hasNpmVariables = hasNpmVariables;
9
9
  exports.interpolateNpmVariables = interpolateNpmVariables;
10
10
  exports.parseSize = parseSize;
11
+ exports.validateCppIdentifier = validateCppIdentifier;
11
12
  exports.formatConfiguration = formatConfiguration;
12
13
  const node_fs_1 = require("node:fs");
13
14
  const node_os_1 = require("node:os");
@@ -23,7 +24,7 @@ Configuration:
23
24
 
24
25
  Options:
25
26
  -e, --engine <value> The engine for which the include file is created
26
- (psychic|psychic2|async|espidf) (default: "psychic")
27
+ (psychic|async|espidf) (default: "psychic")
27
28
  -s, --sourcepath <path> Source dist folder contains compiled web files (required)
28
29
  -o, --outputfile <path> Generated output file with path (default: "svelteesp32.h")
29
30
  --etag <value> Use ETAG header for cache (true|false|compiler) (default: "false")
@@ -38,6 +39,7 @@ Options:
38
39
  --basepath <path> URL prefix for all routes (e.g., "/ui") (default: "")
39
40
  --maxsize <size> Maximum total uncompressed size (e.g., 400k, 1.5m, 409600)
40
41
  --maxgzipsize <size> Maximum total gzip size (e.g., 150k, 1m, 153600)
42
+ --dryrun Show summary without writing the output file (default: false)
41
43
  -h, --help Shows this help
42
44
 
43
45
  RC File:
@@ -64,7 +66,7 @@ RC File:
64
66
  process.exit(0);
65
67
  }
66
68
  function validateEngine(value) {
67
- if (value === 'psychic' || value === 'psychic2' || value === 'async' || value === 'espidf')
69
+ if (value === 'psychic' || value === 'async' || value === 'espidf')
68
70
  return value;
69
71
  console.error((0, errorMessages_1.getInvalidEngineError)(value));
70
72
  process.exit(1);
@@ -74,6 +76,11 @@ function validateTriState(value, name) {
74
76
  return value;
75
77
  throw new Error(`Invalid ${name}: ${value}`);
76
78
  }
79
+ function validateCppIdentifier(value, name) {
80
+ if (!/^[A-Z_a-z]\w*$/.test(value))
81
+ throw new Error(`${name} must be a valid C++ identifier (letters, digits, underscores, not starting with a digit): ${value}`);
82
+ return value;
83
+ }
77
84
  function validateBasePath(value) {
78
85
  if (value === '')
79
86
  return value;
@@ -144,7 +151,9 @@ function parsePackageJson(packageJsonPath) {
144
151
  return JSON.parse(content);
145
152
  }
146
153
  catch (error) {
147
- throw new Error(`Failed to parse package.json at ${packageJsonPath}: ${error.message}`);
154
+ throw new Error(`Failed to parse package.json at ${packageJsonPath}: ${error.message}`, {
155
+ cause: error
156
+ });
148
157
  }
149
158
  }
150
159
  function getNpmPackageVariable(packageJson, variableName) {
@@ -235,7 +244,7 @@ function loadRcFile(rcPath) {
235
244
  }
236
245
  catch (error) {
237
246
  if (error instanceof SyntaxError)
238
- throw new Error(`Invalid JSON in RC file ${rcPath}: ${error.message}`);
247
+ throw new Error(`Invalid JSON in RC file ${rcPath}: ${error.message}`, { cause: error });
239
248
  throw error;
240
249
  }
241
250
  }
@@ -258,7 +267,8 @@ function validateRcConfig(config, rcPath) {
258
267
  'basepath',
259
268
  'maxsize',
260
269
  'maxgzipsize',
261
- 'noindexcheck'
270
+ 'noindexcheck',
271
+ 'dryrun'
262
272
  ]);
263
273
  for (const key of Object.keys(configObject))
264
274
  if (!validKeys.has(key))
@@ -269,9 +279,16 @@ function validateRcConfig(config, rcPath) {
269
279
  configObject['etag'] = validateTriState(configObject['etag'], 'etag');
270
280
  if (configObject['gzip'] !== undefined)
271
281
  configObject['gzip'] = validateTriState(configObject['gzip'], 'gzip');
272
- if (configObject['cachetime'] !== undefined &&
273
- (typeof configObject['cachetime'] !== 'number' || Number.isNaN(configObject['cachetime'])))
274
- throw new TypeError(`Invalid cachetime in RC file: ${configObject['cachetime']}`);
282
+ if (configObject['espmethod'] !== undefined)
283
+ validateCppIdentifier(configObject['espmethod'], 'espmethod');
284
+ if (configObject['define'] !== undefined)
285
+ validateCppIdentifier(configObject['define'], 'define');
286
+ if (configObject['cachetime'] !== undefined) {
287
+ if (typeof configObject['cachetime'] !== 'number' || Number.isNaN(configObject['cachetime']))
288
+ throw new TypeError(`Invalid cachetime in RC file: ${configObject['cachetime']}`);
289
+ if (configObject['cachetime'] < 0)
290
+ throw new TypeError(`Invalid cachetime in RC file: ${configObject['cachetime']} (must be non-negative)`);
291
+ }
275
292
  if (configObject['exclude'] !== undefined) {
276
293
  if (!Array.isArray(configObject['exclude']))
277
294
  throw new TypeError("'exclude' in RC file must be an array");
@@ -305,6 +322,8 @@ function validateRcConfig(config, rcPath) {
305
322
  }
306
323
  if (configObject['noindexcheck'] !== undefined && typeof configObject['noindexcheck'] !== 'boolean')
307
324
  throw new TypeError(`Invalid noindexcheck in RC file: ${configObject['noindexcheck']} (must be boolean)`);
325
+ if (configObject['dryrun'] !== undefined && typeof configObject['dryrun'] !== 'boolean')
326
+ throw new TypeError(`Invalid dryrun in RC file: ${configObject['dryrun']} (must be boolean)`);
308
327
  return configObject;
309
328
  }
310
329
  function parseArguments() {
@@ -368,9 +387,67 @@ function parseArguments() {
368
387
  result.maxGzipSize = rcConfig.maxgzipsize;
369
388
  if (rcConfig.noindexcheck !== undefined)
370
389
  result.noIndexCheck = rcConfig.noindexcheck;
390
+ if (rcConfig.dryrun !== undefined)
391
+ result.dryRun = rcConfig.dryrun;
371
392
  if (rcConfig.exclude && rcConfig.exclude.length > 0)
372
393
  result.exclude = [...rcConfig.exclude];
373
394
  const cliExclude = [];
395
+ function applyFlag(flagName, value) {
396
+ switch (flagName) {
397
+ case 'config':
398
+ break;
399
+ case 'engine':
400
+ result.engine = validateEngine(value);
401
+ break;
402
+ case 'sourcepath':
403
+ result.sourcepath = value;
404
+ break;
405
+ case 'outputfile':
406
+ result.outputfile = value;
407
+ break;
408
+ case 'etag':
409
+ result.etag = validateTriState(value, 'etag');
410
+ break;
411
+ case 'gzip':
412
+ result.gzip = validateTriState(value, 'gzip');
413
+ break;
414
+ case 'version':
415
+ result.version = value;
416
+ break;
417
+ case 'espmethod':
418
+ result.espmethod = validateCppIdentifier(value, 'espmethod');
419
+ break;
420
+ case 'define':
421
+ result.define = validateCppIdentifier(value, 'define');
422
+ break;
423
+ case 'cachetime':
424
+ result.cachetime = Number.parseInt(value, 10);
425
+ if (Number.isNaN(result.cachetime))
426
+ throw new TypeError(`Invalid cachetime: ${value}`);
427
+ if (result.cachetime < 0)
428
+ throw new TypeError(`Invalid cachetime: ${value} (must be non-negative)`);
429
+ break;
430
+ case 'exclude': {
431
+ const patterns = value
432
+ .split(',')
433
+ .map((p) => p.trim())
434
+ .filter(Boolean);
435
+ cliExclude.push(...patterns);
436
+ break;
437
+ }
438
+ case 'basepath':
439
+ result.basePath = validateBasePath(value);
440
+ break;
441
+ case 'maxsize':
442
+ result.maxSize = parseSize(value, '--maxsize');
443
+ break;
444
+ case 'maxgzipsize':
445
+ result.maxGzipSize = parseSize(value, '--maxgzipsize');
446
+ break;
447
+ default:
448
+ throw new Error(`Unknown flag: --${flagName}`);
449
+ }
450
+ }
374
451
  for (let index = 0; index < arguments_.length; index++) {
375
452
  const argument = arguments_[index];
376
453
  if (!argument)
@@ -383,59 +460,7 @@ function parseArguments() {
383
460
  const value = parts.slice(1).join('=');
384
461
  if (!flag || !value)
385
462
  throw new Error(`Invalid argument format: ${argument}`);
386
- const flagName = flag.slice(2);
387
- switch (flagName) {
388
- case 'config':
389
- break;
390
- case 'engine':
391
- result.engine = validateEngine(value);
392
- break;
393
- case 'sourcepath':
394
- result.sourcepath = value;
395
- break;
396
- case 'outputfile':
397
- result.outputfile = value;
398
- break;
399
- case 'etag':
400
- result.etag = validateTriState(value, 'etag');
401
- break;
402
- case 'gzip':
403
- result.gzip = validateTriState(value, 'gzip');
404
- break;
405
- case 'version':
406
- result.version = value;
407
- break;
408
- case 'espmethod':
409
- result.espmethod = value;
410
- break;
411
- case 'define':
412
- result.define = value;
413
- break;
414
- case 'cachetime':
415
- result.cachetime = Number.parseInt(value, 10);
416
- if (Number.isNaN(result.cachetime))
417
- throw new TypeError(`Invalid cachetime: ${value}`);
418
- break;
419
- case 'exclude': {
420
- const patterns = value
421
- .split(',')
422
- .map((p) => p.trim())
423
- .filter(Boolean);
424
- cliExclude.push(...patterns);
425
- break;
426
- }
427
- case 'basepath':
428
- result.basePath = validateBasePath(value);
429
- break;
430
- case 'maxsize':
431
- result.maxSize = parseSize(value, '--maxsize');
432
- break;
433
- case 'maxgzipsize':
434
- result.maxGzipSize = parseSize(value, '--maxgzipsize');
435
- break;
436
- default:
437
- throw new Error(`Unknown flag: ${flag}`);
438
- }
463
+ applyFlag(flag.slice(2), value);
439
464
  continue;
440
465
  }
441
466
  if (argument === '--created') {
@@ -446,6 +471,10 @@ function parseArguments() {
446
471
  result.noIndexCheck = true;
447
472
  continue;
448
473
  }
474
+ if (argument === '--dryrun' || argument === '--dry-run') {
475
+ result.dryRun = true;
476
+ continue;
477
+ }
449
478
  if (argument.startsWith('-') && !argument.startsWith('--')) {
450
479
  const flag = argument.slice(1);
451
480
  const nextArgument = arguments_[index + 1];
@@ -474,72 +503,8 @@ function parseArguments() {
474
503
  const nextArgument = arguments_[index + 1];
475
504
  if (!nextArgument || nextArgument.startsWith('-'))
476
505
  throw new Error(`Missing value for flag: ${argument}`);
477
- switch (flag) {
478
- case 'config':
479
- index++;
480
- break;
481
- case 'engine':
482
- result.engine = validateEngine(nextArgument);
483
- index++;
484
- break;
485
- case 'sourcepath':
486
- result.sourcepath = nextArgument;
487
- index++;
488
- break;
489
- case 'outputfile':
490
- result.outputfile = nextArgument;
491
- index++;
492
- break;
493
- case 'etag':
494
- result.etag = validateTriState(nextArgument, 'etag');
495
- index++;
496
- break;
497
- case 'gzip':
498
- result.gzip = validateTriState(nextArgument, 'gzip');
499
- index++;
500
- break;
501
- case 'version':
502
- result.version = nextArgument;
503
- index++;
504
- break;
505
- case 'espmethod':
506
- result.espmethod = nextArgument;
507
- index++;
508
- break;
509
- case 'define':
510
- result.define = nextArgument;
511
- index++;
512
- break;
513
- case 'cachetime':
514
- result.cachetime = Number.parseInt(nextArgument, 10);
515
- if (Number.isNaN(result.cachetime))
516
- throw new TypeError(`Invalid cachetime: ${nextArgument}`);
517
- index++;
518
- break;
519
- case 'exclude': {
520
- const patterns = nextArgument
521
- .split(',')
522
- .map((p) => p.trim())
523
- .filter(Boolean);
524
- cliExclude.push(...patterns);
525
- index++;
526
- break;
527
- }
528
- case 'basepath':
529
- result.basePath = validateBasePath(nextArgument);
530
- index++;
531
- break;
532
- case 'maxsize':
533
- result.maxSize = parseSize(nextArgument, '--maxsize');
534
- index++;
535
- break;
536
- case 'maxgzipsize':
537
- result.maxGzipSize = parseSize(nextArgument, '--maxgzipsize');
538
- index++;
539
- break;
540
- default:
541
- throw new Error(`Unknown flag: --${flag}`);
542
- }
506
+ applyFlag(flag, nextArgument);
507
+ index++;
543
508
  continue;
544
509
  }
545
510
  throw new Error(`Unknown argument: ${argument}`);
package/dist/cppCode.js CHANGED
@@ -120,212 +120,6 @@ extern "C" void __attribute__((weak)) {{definePrefix}}_onFileServed(const char*
120
120
  const psychicTemplate = `
121
121
  //engine: PsychicHttpServer
122
122
  //config: {{{config}}}
123
- //You should use server.config.max_uri_handlers = {{fileCount}}; or higher value to proper handles all files
124
- {{#if created }}
125
- //created: {{now}}
126
- {{/if}}
127
- //
128
- ${commonHeaderSection}
129
-
130
- //
131
- #include <Arduino.h>
132
- #include <PsychicHttp.h>
133
- #include <PsychicHttpsServer.h>
134
-
135
- //
136
- ${dataArraysSection(false)}
137
-
138
- //
139
- ${etagArraysSection}
140
-
141
- //
142
- ${manifestSection}
143
-
144
- //
145
- ${hookSection}
146
-
147
- //
148
- // Http Handlers
149
- void {{methodName}}(PsychicHttpServer * server) {
150
- {{#each sources}}
151
- //
152
- // {{this.filename}}
153
- {{#if this.isDefault}}{{#unless ../basePath}}server->defaultEndpoint = {{/unless}}{{/if}}server->on("{{../basePath}}/{{this.filename}}", HTTP_GET, [](PsychicRequest * request) {
154
-
155
- {{#switch ../etag}}
156
- {{#case "true"}}
157
- if (request->hasHeader("If-None-Match") && request->header("If-None-Match").equals(etag_{{this.dataname}})) {
158
- PsychicResponse response304(request);
159
- response304.setCode(304);
160
- {{../definePrefix}}_onFileServed("{{../basePath}}/{{this.filename}}", 304);
161
- return response304.send();
162
- }
163
- {{/case}}
164
- {{#case "compiler"}}
165
- #ifdef {{../definePrefix}}_ENABLE_ETAG
166
- if (request->hasHeader("If-None-Match") && request->header("If-None-Match").equals(etag_{{this.dataname}})) {
167
- PsychicResponse response304(request);
168
- response304.setCode(304);
169
- {{../definePrefix}}_onFileServed("{{../basePath}}/{{this.filename}}", 304);
170
- return response304.send();
171
- }
172
- #endif
173
- {{/case}}
174
- {{/switch}}
175
-
176
- PsychicResponse response(request);
177
- response.setContentType("{{this.mime}}");
178
-
179
- {{#switch ../gzip}}
180
- {{#case "true"}}
181
- {{#if this.isGzip}}
182
- response.addHeader("Content-Encoding", "gzip");
183
- {{/if}}
184
- {{/case}}
185
- {{#case "compiler"}}
186
- {{#if this.isGzip}}
187
- #ifdef {{../definePrefix}}_ENABLE_GZIP
188
- response.addHeader("Content-Encoding", "gzip");
189
- #endif
190
- {{/if}}
191
- {{/case}}
192
- {{/switch}}
193
-
194
- {{#switch ../etag}}
195
- {{#case "true"}}
196
- {{#../cacheTime}}
197
- response.addHeader("Cache-Control", "max-age={{value}}");
198
- {{/../cacheTime}}
199
- {{^../cacheTime}}
200
- response.addHeader("Cache-Control", "no-cache");
201
- {{/../cacheTime}}
202
- response.addHeader("ETag", etag_{{this.dataname}});
203
- {{/case}}
204
- {{#case "compiler"}}
205
- #ifdef {{../definePrefix}}_ENABLE_ETAG
206
- {{#../cacheTime}}
207
- response.addHeader("Cache-Control", "max-age={{value}}");
208
- {{/../cacheTime}}
209
- {{^../cacheTime}}
210
- response.addHeader("Cache-Control", "no-cache");
211
- {{/../cacheTime}}
212
- response.addHeader("ETag", etag_{{this.dataname}});
213
- #endif
214
- {{/case}}
215
- {{/switch}}
216
-
217
- {{#switch ../gzip}}
218
- {{#case "true"}}
219
- response.setContent(datagzip_{{this.dataname}}, {{this.lengthGzip}});
220
- {{/case}}
221
- {{#case "false"}}
222
- response.setContent(data_{{this.dataname}}, {{this.length}});
223
- {{/case}}
224
- {{#case "compiler"}}
225
- #ifdef {{../definePrefix}}_ENABLE_GZIP
226
- response.setContent(datagzip_{{this.dataname}}, {{this.lengthGzip}});
227
- #else
228
- response.setContent(data_{{this.dataname}}, {{this.length}});
229
- #endif
230
- {{/case}}
231
- {{/switch}}
232
-
233
- {{../definePrefix}}_onFileServed("{{../basePath}}/{{this.filename}}", 200);
234
- return response.send();
235
- });
236
- {{#if this.isDefault}}{{#if ../basePath}}
237
- //
238
- // {{this.filename}} (base path route)
239
- server->on("{{../basePath}}", HTTP_GET, [](PsychicRequest * request) {
240
-
241
- {{#switch ../etag}}
242
- {{#case "true"}}
243
- if (request->hasHeader("If-None-Match") && request->header("If-None-Match").equals(etag_{{this.dataname}})) {
244
- PsychicResponse response304(request);
245
- response304.setCode(304);
246
- {{../definePrefix}}_onFileServed("{{../basePath}}", 304);
247
- return response304.send();
248
- }
249
- {{/case}}
250
- {{#case "compiler"}}
251
- #ifdef {{../definePrefix}}_ENABLE_ETAG
252
- if (request->hasHeader("If-None-Match") && request->header("If-None-Match").equals(etag_{{this.dataname}})) {
253
- PsychicResponse response304(request);
254
- response304.setCode(304);
255
- {{../definePrefix}}_onFileServed("{{../basePath}}", 304);
256
- return response304.send();
257
- }
258
- #endif
259
- {{/case}}
260
- {{/switch}}
261
-
262
- PsychicResponse response(request);
263
- response.setContentType("{{this.mime}}");
264
-
265
- {{#switch ../gzip}}
266
- {{#case "true"}}
267
- {{#if this.isGzip}}
268
- response.addHeader("Content-Encoding", "gzip");
269
- {{/if}}
270
- {{/case}}
271
- {{#case "compiler"}}
272
- {{#if this.isGzip}}
273
- #ifdef {{../definePrefix}}_ENABLE_GZIP
274
- response.addHeader("Content-Encoding", "gzip");
275
- #endif
276
- {{/if}}
277
- {{/case}}
278
- {{/switch}}
279
-
280
- {{#switch ../etag}}
281
- {{#case "true"}}
282
- {{#../cacheTime}}
283
- response.addHeader("Cache-Control", "max-age={{value}}");
284
- {{/../cacheTime}}
285
- {{^../cacheTime}}
286
- response.addHeader("Cache-Control", "no-cache");
287
- {{/../cacheTime}}
288
- response.addHeader("ETag", etag_{{this.dataname}});
289
- {{/case}}
290
- {{#case "compiler"}}
291
- #ifdef {{../definePrefix}}_ENABLE_ETAG
292
- {{#../cacheTime}}
293
- response.addHeader("Cache-Control", "max-age={{value}}");
294
- {{/../cacheTime}}
295
- {{^../cacheTime}}
296
- response.addHeader("Cache-Control", "no-cache");
297
- {{/../cacheTime}}
298
- response.addHeader("ETag", etag_{{this.dataname}});
299
- #endif
300
- {{/case}}
301
- {{/switch}}
302
-
303
- {{#switch ../gzip}}
304
- {{#case "true"}}
305
- response.setContent(datagzip_{{this.dataname}}, {{this.lengthGzip}});
306
- {{/case}}
307
- {{#case "false"}}
308
- response.setContent(data_{{this.dataname}}, {{this.length}});
309
- {{/case}}
310
- {{#case "compiler"}}
311
- #ifdef {{../definePrefix}}_ENABLE_GZIP
312
- response.setContent(datagzip_{{this.dataname}}, {{this.lengthGzip}});
313
- #else
314
- response.setContent(data_{{this.dataname}}, {{this.length}});
315
- #endif
316
- {{/case}}
317
- {{/switch}}
318
-
319
- {{../definePrefix}}_onFileServed("{{../basePath}}", 200);
320
- return response.send();
321
- });
322
- {{/if}}{{/if}}
323
-
324
- {{/each}}
325
- }`;
326
- const psychic2Template = `
327
- //engine: PsychicHttpServerV2
328
- //config: {{{config}}}
329
123
  {{#if created }}
330
124
  //created: {{now}}
331
125
  {{/if}}
@@ -704,21 +498,29 @@ const getTemplate = (engine) => {
704
498
  switch (engine) {
705
499
  case 'psychic':
706
500
  return psychicTemplate;
707
- case 'psychic2':
708
- return psychic2Template;
501
+ case 'async':
502
+ return asyncTemplate;
709
503
  case 'espidf':
710
504
  return cppCodeEspIdf_1.espidfTemplate;
711
505
  default:
712
- return asyncTemplate;
506
+ throw new Error(`Unknown engine: ${engine}`);
713
507
  }
714
508
  };
509
+ const bufferToByteString = (buffer) => {
510
+ if (buffer.length === 0)
511
+ return '';
512
+ let result = buffer[0].toString(10);
513
+ for (let index = 1; index < buffer.length; index++)
514
+ result += ',' + buffer[index].toString(10);
515
+ return result;
516
+ };
715
517
  const transformSourceToTemplateData = (s, etag) => ({
716
518
  ...s,
717
519
  length: s.content.length,
718
- bytes: [...s.content].map((v) => `${v.toString(10)}`).join(','),
520
+ bytes: bufferToByteString(s.content),
719
521
  lengthGzip: s.contentGzip.length,
720
- bytesGzip: [...s.contentGzip].map((v) => `${v.toString(10)}`).join(','),
721
- isDefault: s.filename.startsWith('index.htm'),
522
+ bytesGzip: bufferToByteString(s.contentGzip),
523
+ isDefault: s.filename === 'index.html' || s.filename === 'index.htm',
722
524
  gzipSizeForManifest: s.isGzip ? s.contentGzip.length : 0,
723
525
  etagForManifest: etag === 'false' ? 'NULL' : `etag_${s.dataname}`
724
526
  });
@@ -728,7 +530,7 @@ const postProcessCppCode = (code) => code
728
530
  .filter(Boolean)
729
531
  .map((line) => (line === '//' ? '' : line))
730
532
  .join('\n')
731
- .replace(/\\n{2}/, '\n');
533
+ .replace(/\n{2,}/g, '\n');
732
534
  const createHandlebarsHelpers = () => {
733
535
  let switchValue;
734
536
  return {
@@ -752,7 +554,10 @@ const getCppCode = (sources, filesByExtension) => {
752
554
  const template = (0, handlebars_1.compile)(getTemplate(commandLine_1.cmdLine.engine));
753
555
  const templateData = {
754
556
  config: (0, commandLine_1.formatConfiguration)(commandLine_1.cmdLine),
755
- now: `${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()}`,
557
+ now: (() => {
558
+ const d = new Date();
559
+ return `${d.toLocaleDateString()} ${d.toLocaleTimeString()}`;
560
+ })(),
756
561
  fileCount: sources.length.toString(),
757
562
  fileSize: sources.reduce((previous, current) => previous + current.content.length, 0).toString(),
758
563
  fileGzipSize: sources.reduce((previous, current) => previous + current.contentGzip.length, 0).toString(),
@@ -1 +1 @@
1
- export declare const espidfTemplate = "\n//engine: espidf\n//config: {{{config}}}\n{{#if created }}\n//created: {{now}}\n{{/if}}\n//\n\n{{#switch etag}}\n{{#case \"true\"}}\n#ifdef {{definePrefix}}_ENABLE_ETAG\n#warning {{definePrefix}}_ENABLE_ETAG has no effect because it is permanently switched ON\n#endif\n{{/case}}\n{{#case \"false\"}}\n#ifdef {{definePrefix}}_ENABLE_ETAG\n#warning {{definePrefix}}_ENABLE_ETAG has no effect because it is permanently switched OFF\n#endif\n{{/case}}\n{{/switch}}\n\n{{#switch gzip}}\n{{#case \"true\"}}\n#ifdef {{definePrefix}}_ENABLE_GZIP\n#warning {{definePrefix}}_ENABLE_GZIP has no effect because it is permanently switched ON\n#endif\n{{/case}}\n{{#case \"false\"}}\n#ifdef {{definePrefix}}_ENABLE_GZIP\n#warning {{definePrefix}}_ENABLE_GZIP has no effect because it is permanently switched OFF\n#endif\n{{/case}}\n{{/switch}}\n\n//\n{{#if version }}\n#define {{definePrefix}}_VERSION \"{{version}}\"\n{{/if}}\n#define {{definePrefix}}_COUNT {{fileCount}}\n#define {{definePrefix}}_SIZE {{fileSize}}\n#define {{definePrefix}}_SIZE_GZIP {{fileGzipSize}}\n\n//\n{{#each sources}}\n#define {{../definePrefix}}_FILE_{{this.datanameUpperCase}}\n{{/each}}\n\n//\n{{#each filesByExtension}}\n#define {{../definePrefix}}_{{this.extension}}_FILES {{this.count}}\n{{/each}}\n\n#include <stdint.h>\n#include <string.h>\n#include <stdlib.h>\n#include <esp_err.h>\n#include <esp_http_server.h>\n\n//\n{{#switch gzip}}\n{{#case \"true\"}}\n {{#each sources}}\nconst char datagzip_{{this.dataname}}[{{this.lengthGzip}}] = { {{this.bytesGzip}} };\n {{/each}}\n{{/case}}\n{{#case \"false\"}}\n {{#each sources}}\nconst char data_{{this.dataname}}[{{this.length}}] = { {{this.bytes}} };\n {{/each}}\n{{/case}}\n{{#case \"compiler\"}}\n#ifdef {{definePrefix}}_ENABLE_GZIP\n {{#each sources}}\nconst char datagzip_{{this.dataname}}[{{this.lengthGzip}}] = { {{this.bytesGzip}} };\n {{/each}}\n#else\n {{#each sources}}\nconst char data_{{this.dataname}}[{{this.length}}] = { {{this.bytes}} };\n {{/each}}\n#endif \n{{/case}}\n{{/switch}}\n\n//\n{{#switch etag}}\n{{#case \"true\"}}\n {{#each sources}}\nstatic const char etag_{{this.dataname}}[] = \"{{this.sha256}}\";\n {{/each}}\n{{/case}}\n{{#case \"false\"}}\n{{/case}}\n{{#case \"compiler\"}}\n#ifdef {{definePrefix}}_ENABLE_ETAG\n {{#each sources}}\nstatic const char etag_{{this.dataname}}[] = \"{{this.sha256}}\";\n {{/each}}\n#endif\n{{/case}}\n{{/switch}}\n\n// File manifest struct (C-compatible typedef)\ntypedef struct {\n const char* path;\n uint32_t size;\n uint32_t gzipSize;\n const char* etag;\n const char* contentType;\n} {{definePrefix}}_FileInfo;\n\n// File manifest array\nstatic const {{definePrefix}}_FileInfo {{definePrefix}}_FILES[] = {\n{{#each sources}}\n { \"{{../basePath}}/{{this.filename}}\", {{this.length}}, {{this.gzipSizeForManifest}}, {{this.etagForManifest}}, \"{{this.mime}}\" },\n{{/each}}\n};\nstatic const size_t {{definePrefix}}_FILE_COUNT = sizeof({{definePrefix}}_FILES) / sizeof({{definePrefix}}_FILES[0]);\n\n// File served hook - override with your own implementation\n__attribute__((weak)) void {{definePrefix}}_onFileServed(const char* path, int statusCode) {}\n\n{{#each sources}}\n\nstatic esp_err_t file_handler_{{this.datanameUpperCase}} (httpd_req_t *req)\n{\n{{#switch ../etag}}\n{{#case \"true\"}}\n size_t hdr_len = httpd_req_get_hdr_value_len(req, \"If-None-Match\");\n if (hdr_len > 0) {\n char* hdr_value = malloc(hdr_len + 1);\n if (httpd_req_get_hdr_value_str(req, \"If-None-Match\", hdr_value, hdr_len + 1) == ESP_OK) {\n if (strcmp(hdr_value, etag_{{this.dataname}}) == 0) {\n free(hdr_value);\n httpd_resp_set_status(req, \"304 Not Modified\");\n {{../definePrefix}}_onFileServed(\"{{../basePath}}/{{this.filename}}\", 304);\n httpd_resp_send(req, NULL, 0);\n return ESP_OK;\n }\n }\n free(hdr_value);\n }\n{{/case}}\n{{#case \"compiler\"}}\n #ifdef {{../definePrefix}}_ENABLE_ETAG\n size_t hdr_len = httpd_req_get_hdr_value_len(req, \"If-None-Match\");\n if (hdr_len > 0) {\n char* hdr_value = malloc(hdr_len + 1);\n if (httpd_req_get_hdr_value_str(req, \"If-None-Match\", hdr_value, hdr_len + 1) == ESP_OK) {\n if (strcmp(hdr_value, etag_{{this.dataname}}) == 0) {\n free(hdr_value);\n httpd_resp_set_status(req, \"304 Not Modified\");\n {{../definePrefix}}_onFileServed(\"{{../basePath}}/{{this.filename}}\", 304);\n httpd_resp_send(req, NULL, 0);\n return ESP_OK;\n }\n }\n free(hdr_value);\n }\n #endif\n{{/case}}\n{{/switch}}\n httpd_resp_set_type(req, \"{{this.mime}}\");\n{{#switch ../gzip}}\n{{#case \"true\"}}\n{{#if this.isGzip}}\n httpd_resp_set_hdr(req, \"Content-Encoding\", \"gzip\");\n{{/if}}\n{{/case}}\n{{#case \"compiler\"}}\n {{#if this.isGzip}}\n #ifdef {{../definePrefix}}_ENABLE_GZIP\n httpd_resp_set_hdr(req, \"Content-Encoding\", \"gzip\");\n #endif \n {{/if}}\n{{/case}}\n{{/switch}}\n\n{{#switch ../etag}}\n{{#case \"true\"}}\n{{#../cacheTime}}\n httpd_resp_set_hdr(req, \"Cache-Control\", \"max-age={{value}}\");\n{{/../cacheTime}}\n{{^../cacheTime}}\n httpd_resp_set_hdr(req, \"Cache-Control\", \"no-cache\");\n{{/../cacheTime}}\n httpd_resp_set_hdr(req, \"ETag\", etag_{{this.dataname}});\n{{/case}}\n{{#case \"compiler\"}}\n #ifdef {{../definePrefix}}_ENABLE_ETAG\n{{#../cacheTime}}\n httpd_resp_set_hdr(req, \"Cache-Control\", \"max-age={{value}}\");\n{{/../cacheTime}}\n{{^../cacheTime}}\n httpd_resp_set_hdr(req, \"Cache-Control\", \"no-cache\");\n{{/../cacheTime}}\n httpd_resp_set_hdr(req, \"ETag\", etag_{{this.dataname}});\n #endif \n{{/case}}\n{{/switch}}\n\n{{#switch ../gzip}}\n{{#case \"true\"}}\n {{../definePrefix}}_onFileServed(\"{{../basePath}}/{{this.filename}}\", 200);\n httpd_resp_send(req, datagzip_{{this.dataname}}, {{this.lengthGzip}});\n{{/case}}\n{{#case \"false\"}}\n {{../definePrefix}}_onFileServed(\"{{../basePath}}/{{this.filename}}\", 200);\n httpd_resp_send(req, data_{{this.dataname}}, {{this.length}});\n{{/case}}\n{{#case \"compiler\"}}\n {{../definePrefix}}_onFileServed(\"{{../basePath}}/{{this.filename}}\", 200);\n #ifdef {{../definePrefix}}_ENABLE_GZIP\n httpd_resp_send(req, datagzip_{{this.dataname}}, {{this.lengthGzip}});\n #else\n httpd_resp_send(req, data_{{this.dataname}}, {{this.length}});\n #endif\n{{/case}}\n{{/switch}}\n return ESP_OK;\n}\n\n{{#if this.isDefault}}\nstatic const httpd_uri_t route_def_{{this.datanameUpperCase}} = {\n .uri = \"{{#if ../basePath}}{{../basePath}}{{else}}/{{/if}}\",\n .method = HTTP_GET,\n .handler = file_handler_{{this.datanameUpperCase}},\n};\n{{/if}}\n\nstatic const httpd_uri_t route_{{this.datanameUpperCase}} = {\n .uri = \"{{../basePath}}/{{this.filename}}\",\n .method = HTTP_GET,\n .handler = file_handler_{{this.datanameUpperCase}},\n};\n\n{{/each}}\n\n\n\nstatic inline void {{methodName}}(httpd_handle_t server) {\n{{#each sources}}\n{{#if this.isDefault}}\n httpd_register_uri_handler(server, &route_def_{{this.datanameUpperCase}});\n{{/if}}\n httpd_register_uri_handler(server, &route_{{this.datanameUpperCase}});\n{{/each}}\n\n}";
1
+ export declare const espidfTemplate = "\n//engine: espidf\n//config: {{{config}}}\n{{#if created }}\n//created: {{now}}\n{{/if}}\n//\n\n{{#switch etag}}\n{{#case \"true\"}}\n#ifdef {{definePrefix}}_ENABLE_ETAG\n#warning {{definePrefix}}_ENABLE_ETAG has no effect because it is permanently switched ON\n#endif\n{{/case}}\n{{#case \"false\"}}\n#ifdef {{definePrefix}}_ENABLE_ETAG\n#warning {{definePrefix}}_ENABLE_ETAG has no effect because it is permanently switched OFF\n#endif\n{{/case}}\n{{/switch}}\n\n{{#switch gzip}}\n{{#case \"true\"}}\n#ifdef {{definePrefix}}_ENABLE_GZIP\n#warning {{definePrefix}}_ENABLE_GZIP has no effect because it is permanently switched ON\n#endif\n{{/case}}\n{{#case \"false\"}}\n#ifdef {{definePrefix}}_ENABLE_GZIP\n#warning {{definePrefix}}_ENABLE_GZIP has no effect because it is permanently switched OFF\n#endif\n{{/case}}\n{{/switch}}\n\n//\n{{#if version }}\n#define {{definePrefix}}_VERSION \"{{version}}\"\n{{/if}}\n#define {{definePrefix}}_COUNT {{fileCount}}\n#define {{definePrefix}}_SIZE {{fileSize}}\n#define {{definePrefix}}_SIZE_GZIP {{fileGzipSize}}\n\n//\n{{#each sources}}\n#define {{../definePrefix}}_FILE_{{this.datanameUpperCase}}\n{{/each}}\n\n//\n{{#each filesByExtension}}\n#define {{../definePrefix}}_{{this.extension}}_FILES {{this.count}}\n{{/each}}\n\n#include <stdint.h>\n#include <string.h>\n#include <stdlib.h>\n#include <esp_err.h>\n#include <esp_http_server.h>\n\n//\n{{#switch gzip}}\n{{#case \"true\"}}\n {{#each sources}}\nconst unsigned char datagzip_{{this.dataname}}[{{this.lengthGzip}}] = { {{this.bytesGzip}} };\n {{/each}}\n{{/case}}\n{{#case \"false\"}}\n {{#each sources}}\nconst unsigned char data_{{this.dataname}}[{{this.length}}] = { {{this.bytes}} };\n {{/each}}\n{{/case}}\n{{#case \"compiler\"}}\n#ifdef {{definePrefix}}_ENABLE_GZIP\n {{#each sources}}\nconst unsigned char datagzip_{{this.dataname}}[{{this.lengthGzip}}] = { {{this.bytesGzip}} };\n {{/each}}\n#else\n {{#each sources}}\nconst unsigned char data_{{this.dataname}}[{{this.length}}] = { {{this.bytes}} };\n {{/each}}\n#endif \n{{/case}}\n{{/switch}}\n\n//\n{{#switch etag}}\n{{#case \"true\"}}\n {{#each sources}}\nstatic const char etag_{{this.dataname}}[] = \"{{this.sha256}}\";\n {{/each}}\n{{/case}}\n{{#case \"false\"}}\n{{/case}}\n{{#case \"compiler\"}}\n#ifdef {{definePrefix}}_ENABLE_ETAG\n {{#each sources}}\nstatic const char etag_{{this.dataname}}[] = \"{{this.sha256}}\";\n {{/each}}\n#endif\n{{/case}}\n{{/switch}}\n\n// File manifest struct (C-compatible typedef)\ntypedef struct {\n const char* path;\n uint32_t size;\n uint32_t gzipSize;\n const char* etag;\n const char* contentType;\n} {{definePrefix}}_FileInfo;\n\n// File manifest array\nstatic const {{definePrefix}}_FileInfo {{definePrefix}}_FILES[] = {\n{{#each sources}}\n { \"{{../basePath}}/{{this.filename}}\", {{this.length}}, {{this.gzipSizeForManifest}}, {{this.etagForManifest}}, \"{{this.mime}}\" },\n{{/each}}\n};\nstatic const size_t {{definePrefix}}_FILE_COUNT = sizeof({{definePrefix}}_FILES) / sizeof({{definePrefix}}_FILES[0]);\n\n// File served hook - override with your own implementation\n__attribute__((weak)) void {{definePrefix}}_onFileServed(const char* path, int statusCode) {}\n\n{{#each sources}}\n\nstatic esp_err_t file_handler_{{this.datanameUpperCase}} (httpd_req_t *req)\n{\n{{#switch ../etag}}\n{{#case \"true\"}}\n size_t hdr_len = httpd_req_get_hdr_value_len(req, \"If-None-Match\");\n if (hdr_len > 0) {\n char* hdr_value = malloc(hdr_len + 1);\n if (hdr_value == NULL) { httpd_resp_send_500(req); return ESP_FAIL; }\n if (httpd_req_get_hdr_value_str(req, \"If-None-Match\", hdr_value, hdr_len + 1) == ESP_OK) {\n if (strcmp(hdr_value, etag_{{this.dataname}}) == 0) {\n free(hdr_value);\n httpd_resp_set_status(req, \"304 Not Modified\");\n {{../definePrefix}}_onFileServed(\"{{../basePath}}/{{this.filename}}\", 304);\n httpd_resp_send(req, NULL, 0);\n return ESP_OK;\n }\n }\n free(hdr_value);\n }\n{{/case}}\n{{#case \"compiler\"}}\n #ifdef {{../definePrefix}}_ENABLE_ETAG\n size_t hdr_len = httpd_req_get_hdr_value_len(req, \"If-None-Match\");\n if (hdr_len > 0) {\n char* hdr_value = malloc(hdr_len + 1);\n if (hdr_value == NULL) { httpd_resp_send_500(req); return ESP_FAIL; }\n if (httpd_req_get_hdr_value_str(req, \"If-None-Match\", hdr_value, hdr_len + 1) == ESP_OK) {\n if (strcmp(hdr_value, etag_{{this.dataname}}) == 0) {\n free(hdr_value);\n httpd_resp_set_status(req, \"304 Not Modified\");\n {{../definePrefix}}_onFileServed(\"{{../basePath}}/{{this.filename}}\", 304);\n httpd_resp_send(req, NULL, 0);\n return ESP_OK;\n }\n }\n free(hdr_value);\n }\n #endif\n{{/case}}\n{{/switch}}\n httpd_resp_set_type(req, \"{{this.mime}}\");\n{{#switch ../gzip}}\n{{#case \"true\"}}\n{{#if this.isGzip}}\n httpd_resp_set_hdr(req, \"Content-Encoding\", \"gzip\");\n{{/if}}\n{{/case}}\n{{#case \"compiler\"}}\n {{#if this.isGzip}}\n #ifdef {{../definePrefix}}_ENABLE_GZIP\n httpd_resp_set_hdr(req, \"Content-Encoding\", \"gzip\");\n #endif \n {{/if}}\n{{/case}}\n{{/switch}}\n\n{{#switch ../etag}}\n{{#case \"true\"}}\n{{#../cacheTime}}\n httpd_resp_set_hdr(req, \"Cache-Control\", \"max-age={{value}}\");\n{{/../cacheTime}}\n{{^../cacheTime}}\n httpd_resp_set_hdr(req, \"Cache-Control\", \"no-cache\");\n{{/../cacheTime}}\n httpd_resp_set_hdr(req, \"ETag\", etag_{{this.dataname}});\n{{/case}}\n{{#case \"compiler\"}}\n #ifdef {{../definePrefix}}_ENABLE_ETAG\n{{#../cacheTime}}\n httpd_resp_set_hdr(req, \"Cache-Control\", \"max-age={{value}}\");\n{{/../cacheTime}}\n{{^../cacheTime}}\n httpd_resp_set_hdr(req, \"Cache-Control\", \"no-cache\");\n{{/../cacheTime}}\n httpd_resp_set_hdr(req, \"ETag\", etag_{{this.dataname}});\n #endif \n{{/case}}\n{{/switch}}\n\n{{#switch ../gzip}}\n{{#case \"true\"}}\n {{../definePrefix}}_onFileServed(\"{{../basePath}}/{{this.filename}}\", 200);\n httpd_resp_send(req, (const char *)datagzip_{{this.dataname}}, {{this.lengthGzip}});\n{{/case}}\n{{#case \"false\"}}\n {{../definePrefix}}_onFileServed(\"{{../basePath}}/{{this.filename}}\", 200);\n httpd_resp_send(req, (const char *)data_{{this.dataname}}, {{this.length}});\n{{/case}}\n{{#case \"compiler\"}}\n {{../definePrefix}}_onFileServed(\"{{../basePath}}/{{this.filename}}\", 200);\n #ifdef {{../definePrefix}}_ENABLE_GZIP\n httpd_resp_send(req, (const char *)datagzip_{{this.dataname}}, {{this.lengthGzip}});\n #else\n httpd_resp_send(req, (const char *)data_{{this.dataname}}, {{this.length}});\n #endif\n{{/case}}\n{{/switch}}\n return ESP_OK;\n}\n\n{{#if this.isDefault}}\nstatic const httpd_uri_t route_def_{{this.datanameUpperCase}} = {\n .uri = \"{{#if ../basePath}}{{../basePath}}{{else}}/{{/if}}\",\n .method = HTTP_GET,\n .handler = file_handler_{{this.datanameUpperCase}},\n};\n{{/if}}\n\nstatic const httpd_uri_t route_{{this.datanameUpperCase}} = {\n .uri = \"{{../basePath}}/{{this.filename}}\",\n .method = HTTP_GET,\n .handler = file_handler_{{this.datanameUpperCase}},\n};\n\n{{/each}}\n\n\n\nstatic inline void {{methodName}}(httpd_handle_t server) {\n{{#each sources}}\n{{#if this.isDefault}}\n httpd_register_uri_handler(server, &route_def_{{this.datanameUpperCase}});\n{{/if}}\n httpd_register_uri_handler(server, &route_{{this.datanameUpperCase}});\n{{/each}}\n\n}";
@@ -63,22 +63,22 @@ exports.espidfTemplate = `
63
63
  {{#switch gzip}}
64
64
  {{#case "true"}}
65
65
  {{#each sources}}
66
- const char datagzip_{{this.dataname}}[{{this.lengthGzip}}] = { {{this.bytesGzip}} };
66
+ const unsigned char datagzip_{{this.dataname}}[{{this.lengthGzip}}] = { {{this.bytesGzip}} };
67
67
  {{/each}}
68
68
  {{/case}}
69
69
  {{#case "false"}}
70
70
  {{#each sources}}
71
- const char data_{{this.dataname}}[{{this.length}}] = { {{this.bytes}} };
71
+ const unsigned char data_{{this.dataname}}[{{this.length}}] = { {{this.bytes}} };
72
72
  {{/each}}
73
73
  {{/case}}
74
74
  {{#case "compiler"}}
75
75
  #ifdef {{definePrefix}}_ENABLE_GZIP
76
76
  {{#each sources}}
77
- const char datagzip_{{this.dataname}}[{{this.lengthGzip}}] = { {{this.bytesGzip}} };
77
+ const unsigned char datagzip_{{this.dataname}}[{{this.lengthGzip}}] = { {{this.bytesGzip}} };
78
78
  {{/each}}
79
79
  #else
80
80
  {{#each sources}}
81
- const char data_{{this.dataname}}[{{this.length}}] = { {{this.bytes}} };
81
+ const unsigned char data_{{this.dataname}}[{{this.length}}] = { {{this.bytes}} };
82
82
  {{/each}}
83
83
  #endif
84
84
  {{/case}}
@@ -131,6 +131,7 @@ static esp_err_t file_handler_{{this.datanameUpperCase}} (httpd_req_t *req)
131
131
  size_t hdr_len = httpd_req_get_hdr_value_len(req, "If-None-Match");
132
132
  if (hdr_len > 0) {
133
133
  char* hdr_value = malloc(hdr_len + 1);
134
+ if (hdr_value == NULL) { httpd_resp_send_500(req); return ESP_FAIL; }
134
135
  if (httpd_req_get_hdr_value_str(req, "If-None-Match", hdr_value, hdr_len + 1) == ESP_OK) {
135
136
  if (strcmp(hdr_value, etag_{{this.dataname}}) == 0) {
136
137
  free(hdr_value);
@@ -148,6 +149,7 @@ static esp_err_t file_handler_{{this.datanameUpperCase}} (httpd_req_t *req)
148
149
  size_t hdr_len = httpd_req_get_hdr_value_len(req, "If-None-Match");
149
150
  if (hdr_len > 0) {
150
151
  char* hdr_value = malloc(hdr_len + 1);
152
+ if (hdr_value == NULL) { httpd_resp_send_500(req); return ESP_FAIL; }
151
153
  if (httpd_req_get_hdr_value_str(req, "If-None-Match", hdr_value, hdr_len + 1) == ESP_OK) {
152
154
  if (strcmp(hdr_value, etag_{{this.dataname}}) == 0) {
153
155
  free(hdr_value);
@@ -204,18 +206,18 @@ static esp_err_t file_handler_{{this.datanameUpperCase}} (httpd_req_t *req)
204
206
  {{#switch ../gzip}}
205
207
  {{#case "true"}}
206
208
  {{../definePrefix}}_onFileServed("{{../basePath}}/{{this.filename}}", 200);
207
- httpd_resp_send(req, datagzip_{{this.dataname}}, {{this.lengthGzip}});
209
+ httpd_resp_send(req, (const char *)datagzip_{{this.dataname}}, {{this.lengthGzip}});
208
210
  {{/case}}
209
211
  {{#case "false"}}
210
212
  {{../definePrefix}}_onFileServed("{{../basePath}}/{{this.filename}}", 200);
211
- httpd_resp_send(req, data_{{this.dataname}}, {{this.length}});
213
+ httpd_resp_send(req, (const char *)data_{{this.dataname}}, {{this.length}});
212
214
  {{/case}}
213
215
  {{#case "compiler"}}
214
216
  {{../definePrefix}}_onFileServed("{{../basePath}}/{{this.filename}}", 200);
215
217
  #ifdef {{../definePrefix}}_ENABLE_GZIP
216
- httpd_resp_send(req, datagzip_{{this.dataname}}, {{this.lengthGzip}});
218
+ httpd_resp_send(req, (const char *)datagzip_{{this.dataname}}, {{this.lengthGzip}});
217
219
  #else
218
- httpd_resp_send(req, data_{{this.dataname}}, {{this.length}});
220
+ httpd_resp_send(req, (const char *)data_{{this.dataname}}, {{this.length}});
219
221
  #endif
220
222
  {{/case}}
221
223
  {{/switch}}
@@ -2,4 +2,4 @@ export declare function getMissingIndexError(engine: string): string;
2
2
  export declare function getInvalidEngineError(attempted: string): string;
3
3
  export declare function getSourcepathNotFoundError(sourcepath: string, reason: 'not_found' | 'not_directory'): string;
4
4
  export declare function getSizeBudgetExceededError(type: 'size' | 'gzipSize', limit: number, actual: number): string;
5
- export declare function getMaxUriHandlersHint(engine: string, routeCount: number): string;
5
+ export declare function getMaxUriHandlersHint(engine: string, routeCount: number, espmethod?: string): string;
@@ -13,7 +13,6 @@ const consoleColor_1 = require("./consoleColor");
13
13
  function getEngineName(engine) {
14
14
  const names = {
15
15
  psychic: 'PsychicHttpServer',
16
- psychic2: 'PsychicHttpServer V2',
17
16
  async: 'ESPAsyncWebServer',
18
17
  espidf: 'ESP-IDF'
19
18
  };
@@ -24,9 +23,6 @@ function getMissingIndexError(engine) {
24
23
  psychic: ` 1. Add an index.html file to your source directory
25
24
  2. The file will automatically be set as the default route ("/")
26
25
  3. PsychicHttpServer uses: server->defaultEndpoint = ...`,
27
- psychic2: ` 1. Add an index.html file to your source directory
28
- 2. The file will automatically be set as the default route ("/")
29
- 3. PsychicHttpServer V2 uses: server->defaultEndpoint = ...`,
30
26
  async: ` 1. Add an index.html file to your source directory
31
27
  2. The file will automatically create a "/" route handler
32
28
  3. ESPAsyncWebServer uses: server.on("/", HTTP_GET, ...)`,
@@ -55,7 +51,6 @@ function getInvalidEngineError(attempted) {
55
51
 
56
52
  Valid engines are:
57
53
  ${(0, consoleColor_1.cyanLog)('• psychic')} - PsychicHttpServer (ESP32 only, fastest performance)
58
- ${(0, consoleColor_1.cyanLog)('• psychic2')} - PsychicHttpServer V2 (ESP32 only, modern API)
59
54
  ${(0, consoleColor_1.cyanLog)('• async')} - ESPAsyncWebServer (ESP32/ESP8266 compatible)
60
55
  ${(0, consoleColor_1.cyanLog)('• espidf')} - Native ESP-IDF web server (ESP32 only, no Arduino)
61
56
 
@@ -136,22 +131,18 @@ CI integration:
136
131
  This non-zero exit code allows build pipelines to fail early when
137
132
  the frontend exceeds allocated flash space.`);
138
133
  }
139
- function getMaxUriHandlersHint(engine, routeCount) {
134
+ function getMaxUriHandlersHint(engine, routeCount, espmethod = 'initSvelteStaticFiles') {
140
135
  const recommended = routeCount + 5;
141
136
  const hints = {
142
137
  psychic: `PsychicHttpServer server;
143
138
  server.config.max_uri_handlers = ${recommended}; // Default is 8, you need at least ${routeCount}
144
- initSvelteStaticFiles(&server);
145
- server.listen(80);`,
146
- psychic2: `PsychicHttpServer server;
147
- server.config.max_uri_handlers = ${recommended}; // Default is 8, you need at least ${routeCount}
148
- initSvelteStaticFiles(&server);
139
+ ${espmethod}(&server);
149
140
  server.listen(80);`,
150
141
  espidf: `httpd_config_t config = HTTPD_DEFAULT_CONFIG();
151
142
  config.max_uri_handlers = ${recommended}; // Default is 8, you need at least ${routeCount}
152
143
  httpd_handle_t server = NULL;
153
144
  httpd_start(&server, &config);
154
- initSvelteStaticFiles(server);`
145
+ ${espmethod}(server);`
155
146
  };
156
147
  const hint = hints[engine];
157
148
  if (!hint)
package/dist/file.d.ts CHANGED
@@ -1 +1,5 @@
1
- export declare const getFiles: () => Map<string, Buffer>;
1
+ export type FileData = {
2
+ content: Buffer;
3
+ hash: string;
4
+ };
5
+ export declare const getFiles: () => Map<string, FileData>;
package/dist/file.js CHANGED
@@ -14,13 +14,12 @@ const consoleColor_1 = require("./consoleColor");
14
14
  const errorMessages_1 = require("./errorMessages");
15
15
  const findSimilarFiles = (files) => {
16
16
  const contentComparer = new Map();
17
- for (const [filename, content] of files.entries()) {
18
- const hash = (0, node_crypto_1.createHash)('sha256').update(content).digest('hex');
19
- const existingFiles = contentComparer.get(hash);
17
+ for (const [filename, fileData] of files.entries()) {
18
+ const existingFiles = contentComparer.get(fileData.hash);
20
19
  if (existingFiles)
21
20
  existingFiles.push(filename);
22
21
  else
23
- contentComparer.set(hash, [filename]);
22
+ contentComparer.set(fileData.hash, [filename]);
24
23
  }
25
24
  const result = [];
26
25
  for (const filenames of contentComparer.values())
@@ -30,7 +29,7 @@ const findSimilarFiles = (files) => {
30
29
  };
31
30
  const shouldSkipFile = (filename, allFilenames) => {
32
31
  const extension = node_path_1.default.extname(filename);
33
- const compressedExtensions = ['.gz', '.brottli', '.br'];
32
+ const compressedExtensions = ['.gz', '.brotli', '.br'];
34
33
  if (compressedExtensions.includes(extension)) {
35
34
  const original = filename.slice(0, -1 * extension.length);
36
35
  if (allFilenames.includes(original)) {
@@ -72,10 +71,14 @@ const getFiles = () => {
72
71
  console.log((0, consoleColor_1.cyanLog)(`... and ${excludedFiles.length - displayLimit} more`));
73
72
  console.log();
74
73
  }
74
+ else if (excludePatterns.length > 0)
75
+ console.log((0, consoleColor_1.yellowLog)(`Warning: --exclude patterns matched no files: ${excludePatterns.join(', ')}`));
75
76
  const result = new Map();
76
77
  for (const filename of filenames) {
77
78
  const filePath = node_path_1.default.join(commandLine_1.cmdLine.sourcepath, filename);
78
- result.set(filename, (0, node_fs_1.readFileSync)(filePath, { flag: 'r' }));
79
+ const content = (0, node_fs_1.readFileSync)(filePath, { flag: 'r' });
80
+ const hash = (0, node_crypto_1.createHash)('sha256').update(content).digest('hex');
81
+ result.set(filename, { content, hash });
79
82
  }
80
83
  const duplicates = findSimilarFiles(result);
81
84
  for (const sameFiles of duplicates)
package/dist/index.d.ts CHANGED
@@ -1,7 +1,9 @@
1
- import { CppCodeSource } from './cppCode';
1
+ import { CppCodeSource, ExtensionGroups } from './cppCode';
2
2
  declare const shouldUseGzip: (originalSize: number, compressedSize: number) => boolean;
3
3
  declare const calculateCompressionRatio: (originalSize: number, compressedSize: number) => number;
4
4
  declare const formatCompressionLog: (filename: string, padding: string, originalSize: number, compressedSize: number, useGzip: boolean) => string;
5
+ declare const formatSize: (bytes: number) => string;
5
6
  declare const createSourceEntry: (filename: string, dataname: string, content: Buffer, contentGzip: Buffer, mimeType: string, sha256: string, isGzip: boolean) => CppCodeSource;
6
- declare const updateExtensionGroup: (extension: string) => void;
7
- export { calculateCompressionRatio, createSourceEntry, formatCompressionLog, shouldUseGzip, updateExtensionGroup };
7
+ declare const updateExtensionGroup: (filesByExtension: ExtensionGroups, extension: string) => void;
8
+ export declare function main(): void;
9
+ export { calculateCompressionRatio, createSourceEntry, formatCompressionLog, formatSize, shouldUseGzip, updateExtensionGroup };
package/dist/index.js CHANGED
@@ -3,8 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.updateExtensionGroup = exports.shouldUseGzip = exports.formatCompressionLog = exports.createSourceEntry = exports.calculateCompressionRatio = void 0;
7
- const node_crypto_1 = require("node:crypto");
6
+ exports.updateExtensionGroup = exports.shouldUseGzip = exports.formatSize = exports.formatCompressionLog = exports.createSourceEntry = exports.calculateCompressionRatio = void 0;
7
+ exports.main = main;
8
8
  const node_fs_1 = require("node:fs");
9
9
  const node_path_1 = __importDefault(require("node:path"));
10
10
  const node_zlib_1 = require("node:zlib");
@@ -16,13 +16,6 @@ const errorMessages_1 = require("./errorMessages");
16
16
  const file_1 = require("./file");
17
17
  const GZIP_MIN_SIZE = 1024;
18
18
  const GZIP_MIN_REDUCTION_RATIO = 0.85;
19
- const summary = {
20
- filecount: 0,
21
- size: 0,
22
- gzipsize: 0
23
- };
24
- const sources = [];
25
- const filesByExtension = [];
26
19
  const shouldUseGzip = (originalSize, compressedSize) => originalSize > GZIP_MIN_SIZE && compressedSize < originalSize * GZIP_MIN_REDUCTION_RATIO;
27
20
  exports.shouldUseGzip = shouldUseGzip;
28
21
  const calculateCompressionRatio = (originalSize, compressedSize) => Math.round((compressedSize / originalSize) * 100);
@@ -36,6 +29,12 @@ const formatCompressionLog = (filename, padding, originalSize, compressedSize, u
36
29
  return (0, consoleColor_1.yellowLog)(` [${filename}] ${padding} x gzip unused ${tooSmall}${sizeInfo}`);
37
30
  };
38
31
  exports.formatCompressionLog = formatCompressionLog;
32
+ const formatSize = (bytes) => {
33
+ if (bytes < 1024)
34
+ return `${bytes}B`;
35
+ return `${Math.round(bytes / 1024)}kB`;
36
+ };
37
+ exports.formatSize = formatSize;
39
38
  const createSourceEntry = (filename, dataname, content, contentGzip, mimeType, sha256, isGzip) => ({
40
39
  filename,
41
40
  dataname,
@@ -47,7 +46,7 @@ const createSourceEntry = (filename, dataname, content, contentGzip, mimeType, s
47
46
  sha256
48
47
  });
49
48
  exports.createSourceEntry = createSourceEntry;
50
- const updateExtensionGroup = (extension) => {
49
+ const updateExtensionGroup = (filesByExtension, extension) => {
51
50
  const group = filesByExtension.find((fe) => fe.extension === extension);
52
51
  if (group)
53
52
  group.count += 1;
@@ -55,47 +54,66 @@ const updateExtensionGroup = (extension) => {
55
54
  filesByExtension.push({ extension, count: 1 });
56
55
  };
57
56
  exports.updateExtensionGroup = updateExtensionGroup;
58
- console.log('Collecting source files');
59
- const files = (0, file_1.getFiles)();
60
- if (files.size === 0) {
61
- console.error(`Directory ${commandLine_1.cmdLine.sourcepath} is empty`);
62
- process.exit(1);
63
- }
64
- console.log();
65
- console.log('Translation to header file');
66
- const longestFilename = [...files.keys()].reduce((p, c) => Math.max(c.length, p), 0);
67
- for (const [originalFilename, content] of files) {
68
- const mimeType = (0, mime_types_1.lookup)(originalFilename) || 'text/plain';
69
- summary.filecount++;
70
- const filename = originalFilename.replace(/\\/g, '/');
71
- const dataname = filename.replace(/[!&()+./@{}~-]/g, '_');
72
- let extension = node_path_1.default.extname(filename).toUpperCase();
73
- if (extension.startsWith('.'))
74
- extension = extension.slice(1);
75
- updateExtensionGroup(extension);
76
- const sha256 = (0, node_crypto_1.createHash)('sha256').update(content).digest('hex');
77
- summary.size += content.length;
78
- const zipContent = (0, node_zlib_1.gzipSync)(content, { level: 9 });
79
- summary.gzipsize += zipContent.length;
80
- const useGzip = shouldUseGzip(content.length, zipContent.length);
81
- sources.push(createSourceEntry(filename, dataname, content, zipContent, mimeType, sha256, useGzip));
82
- const padding = ' '.repeat(longestFilename - originalFilename.length);
83
- console.log(formatCompressionLog(originalFilename, padding, content.length, zipContent.length, useGzip));
84
- }
85
- console.log('');
86
- filesByExtension.sort((left, right) => left.extension.localeCompare(right.extension));
87
- if (commandLine_1.cmdLine.maxSize !== undefined && summary.size > commandLine_1.cmdLine.maxSize) {
88
- console.error((0, errorMessages_1.getSizeBudgetExceededError)('size', commandLine_1.cmdLine.maxSize, summary.size));
89
- process.exit(1);
90
- }
91
- if (commandLine_1.cmdLine.maxGzipSize !== undefined && summary.gzipsize > commandLine_1.cmdLine.maxGzipSize) {
92
- console.error((0, errorMessages_1.getSizeBudgetExceededError)('gzipSize', commandLine_1.cmdLine.maxGzipSize, summary.gzipsize));
93
- process.exit(1);
57
+ function main() {
58
+ const summary = {
59
+ filecount: 0,
60
+ size: 0,
61
+ gzipsize: 0
62
+ };
63
+ const sources = [];
64
+ const filesByExtension = [];
65
+ console.log('Collecting source files');
66
+ const files = (0, file_1.getFiles)();
67
+ if (files.size === 0) {
68
+ console.error(`Directory ${commandLine_1.cmdLine.sourcepath} is empty`);
69
+ process.exit(1);
70
+ }
71
+ console.log();
72
+ console.log('Translation to header file');
73
+ const longestFilename = [...files.keys()].reduce((p, c) => Math.max(c.length, p), 0);
74
+ for (const [originalFilename, fileData] of files) {
75
+ const { content, hash: sha256 } = fileData;
76
+ const mimeType = (0, mime_types_1.lookup)(originalFilename) || 'text/plain';
77
+ if (!(0, mime_types_1.lookup)(originalFilename))
78
+ console.log((0, consoleColor_1.yellowLog)(` [${originalFilename}] unknown MIME type for extension '${node_path_1.default.extname(originalFilename)}', using text/plain`));
79
+ summary.filecount++;
80
+ const filename = originalFilename.replace(/\\/g, '/');
81
+ let dataname = filename.replace(/\W/g, '_');
82
+ if (/^\d/.test(dataname))
83
+ dataname = '_' + dataname;
84
+ let extension = node_path_1.default.extname(filename).toUpperCase();
85
+ if (extension.startsWith('.'))
86
+ extension = extension.slice(1);
87
+ updateExtensionGroup(filesByExtension, extension);
88
+ summary.size += content.length;
89
+ const zipContent = (0, node_zlib_1.gzipSync)(content, { level: 9 });
90
+ const useGzip = shouldUseGzip(content.length, zipContent.length);
91
+ summary.gzipsize += useGzip ? zipContent.length : content.length;
92
+ sources.push(createSourceEntry(filename, dataname, content, zipContent, mimeType, sha256, useGzip));
93
+ const padding = ' '.repeat(longestFilename - originalFilename.length);
94
+ console.log(formatCompressionLog(originalFilename, padding, content.length, zipContent.length, useGzip));
95
+ }
96
+ console.log('');
97
+ filesByExtension.sort((left, right) => left.extension.localeCompare(right.extension));
98
+ if (commandLine_1.cmdLine.maxSize !== undefined && summary.size > commandLine_1.cmdLine.maxSize) {
99
+ console.error((0, errorMessages_1.getSizeBudgetExceededError)('size', commandLine_1.cmdLine.maxSize, summary.size));
100
+ process.exit(1);
101
+ }
102
+ if (commandLine_1.cmdLine.maxGzipSize !== undefined && summary.gzipsize > commandLine_1.cmdLine.maxGzipSize) {
103
+ console.error((0, errorMessages_1.getSizeBudgetExceededError)('gzipSize', commandLine_1.cmdLine.maxGzipSize, summary.gzipsize));
104
+ process.exit(1);
105
+ }
106
+ if (commandLine_1.cmdLine.dryRun) {
107
+ console.log(`[DRY RUN] ${summary.filecount} files, ${formatSize(summary.size)} original size, ${formatSize(summary.gzipsize)} gzip size`);
108
+ console.log(`[DRY RUN] Would write to ${commandLine_1.cmdLine.outputfile}`);
109
+ return;
110
+ }
111
+ const cppFile = (0, cppCode_1.getCppCode)(sources, filesByExtension);
112
+ (0, node_fs_1.mkdirSync)(node_path_1.default.normalize(node_path_1.default.dirname(commandLine_1.cmdLine.outputfile)), { recursive: true });
113
+ (0, node_fs_1.writeFileSync)(commandLine_1.cmdLine.outputfile, cppFile, { flush: true, encoding: 'utf8' });
114
+ console.log(`${summary.filecount} files, ${formatSize(summary.size)} original size, ${formatSize(summary.gzipsize)} gzip size`);
115
+ console.log(`${commandLine_1.cmdLine.outputfile} ${formatSize(cppFile.length)} size`);
116
+ if (commandLine_1.cmdLine.engine === 'psychic' || commandLine_1.cmdLine.engine === 'espidf')
117
+ console.log('\n' + (0, errorMessages_1.getMaxUriHandlersHint)(commandLine_1.cmdLine.engine, sources.length, commandLine_1.cmdLine.espmethod));
94
118
  }
95
- const cppFile = (0, cppCode_1.getCppCode)(sources, filesByExtension);
96
- (0, node_fs_1.mkdirSync)(node_path_1.default.normalize(node_path_1.default.dirname(commandLine_1.cmdLine.outputfile)), { recursive: true });
97
- (0, node_fs_1.writeFileSync)(commandLine_1.cmdLine.outputfile, cppFile, { flush: true, encoding: 'utf8' });
98
- console.log(`${summary.filecount} files, ${Math.round(summary.size / 1024)}kB original size, ${Math.round(summary.gzipsize / 1024)}kB gzip size`);
99
- console.log(`${commandLine_1.cmdLine.outputfile} ${Math.round(cppFile.length / 1024)}kB size`);
100
- if (commandLine_1.cmdLine.engine === 'psychic' || commandLine_1.cmdLine.engine === 'psychic2' || commandLine_1.cmdLine.engine === 'espidf')
101
- console.log('\n' + (0, errorMessages_1.getMaxUriHandlersHint)(commandLine_1.cmdLine.engine, sources.length));
119
+ main();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelteesp32",
3
- "version": "1.16.1",
3
+ "version": "2.0.1",
4
4
  "description": "Convert Svelte (or any frontend) JS application to serve it from ESP32 webserver (PsychicHttp)",
5
5
  "author": "BCsabaEngine",
6
6
  "license": "ISC",
@@ -30,7 +30,6 @@
30
30
  "scripts": {
31
31
  "dev:async": "nodemon src/index.ts -- -e async -s ./demo/svelte/dist -o ./demo/esp32/include/svelteesp32.h --etag=true --gzip=true --cachetime=86400 --version=v$npm_package_version",
32
32
  "dev:psychic": "nodemon src/index.ts -- -e psychic -s ./demo/svelte/dist -o ./demo/esp32/include/svelteesp32.h --etag=false --gzip=false --version=v$npm_package_version",
33
- "dev:psychic2": "nodemon src/index.ts -- -e psychic2 -s ./demo/svelte/dist -o ./demo/esp32/include/svelteesp32.h --etag=false --gzip=false --version=v$npm_package_version",
34
33
  "test": "vitest run",
35
34
  "test:watch": "vitest",
36
35
  "test:coverage": "vitest run --coverage",
@@ -56,20 +55,21 @@
56
55
  "esp8266",
57
56
  "webserver",
58
57
  "psychichttpserver",
59
- "psychichttpserverV2",
60
58
  "espasyncwebserver"
61
59
  ],
62
60
  "devDependencies": {
61
+ "@eslint/eslintrc": "^3.3.3",
62
+ "@eslint/js": "^10.0.0",
63
63
  "@types/mime-types": "^3.0.1",
64
- "@types/node": "^25.1.0",
64
+ "@types/node": "^25.3.0",
65
65
  "@types/picomatch": "^4.0.2",
66
- "@typescript-eslint/eslint-plugin": "^8.54.0",
67
- "@typescript-eslint/parser": "^8.54.0",
66
+ "@typescript-eslint/eslint-plugin": "^8.56.0",
67
+ "@typescript-eslint/parser": "^8.56.0",
68
68
  "@vitest/coverage-v8": "^4.0.18",
69
- "eslint": "^9.39.2",
69
+ "eslint": "^10.0.0",
70
70
  "eslint-config-prettier": "^10.1.8",
71
71
  "eslint-plugin-simple-import-sort": "^12.1.1",
72
- "eslint-plugin-unicorn": "^62.0.0",
72
+ "eslint-plugin-unicorn": "^63.0.0",
73
73
  "memfs": "^4.56.10",
74
74
  "nodemon": "^3.1.11",
75
75
  "prettier": "^3.8.1",