svelteesp32 2.1.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -79,8 +79,9 @@ void setup() {
79
79
 
80
80
  ## What's New
81
81
 
82
+ - **v2.2.1** — Enhanced `--dryrun` output: engine/ETag/gzip/SPA summary header + aligned route table with MIME types, sizes, and tags (`[default]`, `[no gzip]`, `[SPA catch-all]`)
83
+ - **v2.2.0** — SPA routing catch-all (`--spa`) for client-side routers on all four engines
82
84
  - **v2.1.0** — New Arduino WebServer engine (`-e webserver`), dependency updates
83
- - **v2.0.1** — Dependency updates (ESLint v10, eslint-plugin-unicorn 63), improved error cause chaining
84
85
  - **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
85
86
  - **v1.16.0** — Size budget constraints (`--maxsize`, `--maxgzipsize`)
86
87
  - **v1.15.0** — `--basepath` for multiple frontends (e.g., `/admin`, `/app`)
@@ -376,6 +377,26 @@ void setup() {
376
377
 
377
378
  **Rules:** Must start with `/`, no trailing slash, no double slashes.
378
379
 
380
+ ### SPA Routing (Client-Side Routers)
381
+
382
+ Modern JS frameworks use client-side routing. Without a catch-all, refreshing `/settings` on your ESP32 returns nothing. Add `--spa` to make all unmatched GET requests fall through to `index.html`:
383
+
384
+ ```bash
385
+ npx svelteesp32 -e async -s ./dist -o ./output.h --spa
386
+ npx svelteesp32 -e psychic -s ./dist -o ./output.h --spa --basepath=/app
387
+ ```
388
+
389
+ What gets generated per engine:
390
+
391
+ | Engine | Catch-all mechanism |
392
+ | ----------- | ---------------------------------------------------------------------------------------------------------------------------- |
393
+ | `psychic` | `server->on("/*", ...)` (no basePath) already handled by `defaultEndpoint`; `server->on("/app/*", ...)` when basePath is set |
394
+ | `async` | `server->onNotFound(...)` with optional basePath prefix check |
395
+ | `webserver` | `server->onNotFound(...)` with optional basePath prefix check |
396
+ | `espidf` | `httpd_register_err_handler(HTTPD_404_NOT_FOUND, ...)` |
397
+
398
+ **Note:** `--spa` requires `index.html` or `index.htm` in the source directory — a warning is printed if it is missing.
399
+
379
400
  ### C++ Build-Time Validation
380
401
 
381
402
  Catch configuration issues at compile time with generated defines:
@@ -425,25 +446,26 @@ Called for every response (200 = content served, 304 = cache hit).
425
446
 
426
447
  ## CLI Reference
427
448
 
428
- | Option | Description | Default |
429
- | ---------------- | -------------------------------------------------- | ----------------------- |
430
- | `-s` | Source folder with compiled web files | (required) |
431
- | `-e` | Web server engine (psychic/async/espidf/webserver) | `psychic` |
432
- | `-o` | Output header file path | `svelteesp32.h` |
433
- | `--etag` | ETag caching (true/false/compiler) | `false` |
434
- | `--gzip` | Gzip compression (true/false/compiler) | `true` |
435
- | `--exclude` | Exclude files by glob pattern | System files |
436
- | `--basepath` | URL prefix for all routes | (none) |
437
- | `--maxsize` | Max total uncompressed size (e.g., `400k`, `1m`) | (none) |
438
- | `--maxgzipsize` | Max total gzip size (e.g., `150k`, `500k`) | (none) |
439
- | `--cachetime` | Cache-Control max-age in seconds | `0` |
440
- | `--version` | Version string in header | (none) |
441
- | `--define` | C++ define prefix | `SVELTEESP32` |
442
- | `--espmethod` | Init function name | `initSvelteStaticFiles` |
443
- | `--config` | Custom RC file path | `.svelteesp32rc.json` |
444
- | `--dryrun` | Show summary without writing output | `false` |
445
- | `--noindexcheck` | Skip index.html validation | `false` |
446
- | `-h` | Show help | |
449
+ | Option | Description | Default |
450
+ | ---------------- | --------------------------------------------------- | ----------------------- |
451
+ | `-s` | Source folder with compiled web files | (required) |
452
+ | `-e` | Web server engine (psychic/async/espidf/webserver) | `psychic` |
453
+ | `-o` | Output header file path | `svelteesp32.h` |
454
+ | `--etag` | ETag caching (true/false/compiler) | `false` |
455
+ | `--gzip` | Gzip compression (true/false/compiler) | `true` |
456
+ | `--exclude` | Exclude files by glob pattern | System files |
457
+ | `--basepath` | URL prefix for all routes | (none) |
458
+ | `--maxsize` | Max total uncompressed size (e.g., `400k`, `1m`) | (none) |
459
+ | `--maxgzipsize` | Max total gzip size (e.g., `150k`, `500k`) | (none) |
460
+ | `--cachetime` | Cache-Control max-age in seconds | `0` |
461
+ | `--version` | Version string in header | (none) |
462
+ | `--define` | C++ define prefix | `SVELTEESP32` |
463
+ | `--espmethod` | Init function name | `initSvelteStaticFiles` |
464
+ | `--config` | Custom RC file path | `.svelteesp32rc.json` |
465
+ | `--dryrun` | Show route table + summary without writing output | `false` |
466
+ | `--spa` | Serve index.html for unmatched routes (SPA routing) | `false` |
467
+ | `--noindexcheck` | Skip index.html validation | `false` |
468
+ | `-h` | Show help | |
447
469
 
448
470
  ---
449
471
 
@@ -463,7 +485,8 @@ Store your settings in `.svelteesp32rc.json` for zero-argument builds:
463
485
  "maxsize": "400k",
464
486
  "maxgzipsize": "150k",
465
487
  "noindexcheck": false,
466
- "dryrun": false
488
+ "dryrun": false,
489
+ "spa": false
467
490
  }
468
491
  ```
469
492
 
@@ -15,6 +15,7 @@ interface ICopyFilesArguments {
15
15
  maxGzipSize?: number;
16
16
  noIndexCheck?: boolean;
17
17
  dryRun?: boolean;
18
+ spa?: boolean;
18
19
  help?: boolean;
19
20
  }
20
21
  interface IRcFileConfig {
@@ -34,6 +35,7 @@ interface IRcFileConfig {
34
35
  maxgzipsize?: number | string;
35
36
  noindexcheck?: boolean;
36
37
  dryrun?: boolean;
38
+ spa?: boolean;
37
39
  }
38
40
  declare function validateCppIdentifier(value: string, name: string): string;
39
41
  declare function parseSize(value: string, name: string): number;
@@ -40,6 +40,7 @@ Options:
40
40
  --maxsize <size> Maximum total uncompressed size (e.g., 400k, 1.5m, 409600)
41
41
  --maxgzipsize <size> Maximum total gzip size (e.g., 150k, 1m, 153600)
42
42
  --dryrun Show summary without writing the output file (default: false)
43
+ --spa Serve index.html for unmatched routes (SPA routing) (default: false)
43
44
  -h, --help Shows this help
44
45
 
45
46
  RC File:
@@ -58,7 +59,8 @@ RC File:
58
59
  "basepath": "/ui",
59
60
  "maxsize": "400k",
60
61
  "maxgzipsize": "150k",
61
- "noindexcheck": false
62
+ "noindexcheck": false,
63
+ "spa": false
62
64
  }
63
65
 
64
66
  CLI arguments override RC file values.
@@ -268,7 +270,8 @@ function validateRcConfig(config, rcPath) {
268
270
  'maxsize',
269
271
  'maxgzipsize',
270
272
  'noindexcheck',
271
- 'dryrun'
273
+ 'dryrun',
274
+ 'spa'
272
275
  ]);
273
276
  for (const key of Object.keys(configObject))
274
277
  if (!validKeys.has(key))
@@ -324,6 +327,8 @@ function validateRcConfig(config, rcPath) {
324
327
  throw new TypeError(`Invalid noindexcheck in RC file: ${configObject['noindexcheck']} (must be boolean)`);
325
328
  if (configObject['dryrun'] !== undefined && typeof configObject['dryrun'] !== 'boolean')
326
329
  throw new TypeError(`Invalid dryrun in RC file: ${configObject['dryrun']} (must be boolean)`);
330
+ if (configObject['spa'] !== undefined && typeof configObject['spa'] !== 'boolean')
331
+ throw new TypeError(`Invalid spa in RC file: ${configObject['spa']} (must be boolean)`);
327
332
  return configObject;
328
333
  }
329
334
  function parseArguments() {
@@ -389,6 +394,8 @@ function parseArguments() {
389
394
  result.noIndexCheck = rcConfig.noindexcheck;
390
395
  if (rcConfig.dryrun !== undefined)
391
396
  result.dryRun = rcConfig.dryrun;
397
+ if (rcConfig.spa !== undefined)
398
+ result.spa = rcConfig.spa;
392
399
  if (rcConfig.exclude && rcConfig.exclude.length > 0)
393
400
  result.exclude = [...rcConfig.exclude];
394
401
  const cliExclude = [];
@@ -475,6 +482,10 @@ function parseArguments() {
475
482
  result.dryRun = true;
476
483
  continue;
477
484
  }
485
+ if (argument === '--spa') {
486
+ result.spa = true;
487
+ continue;
488
+ }
478
489
  if (argument.startsWith('-') && !argument.startsWith('--')) {
479
490
  const flag = argument.slice(1);
480
491
  const nextArgument = arguments_[index + 1];
@@ -540,6 +551,8 @@ function formatConfiguration(cmdLine) {
540
551
  parts.push(`maxSize=${cmdLine.maxSize}`);
541
552
  if (cmdLine.maxGzipSize !== undefined)
542
553
  parts.push(`maxGzipSize=${cmdLine.maxGzipSize}`);
554
+ if (cmdLine.spa)
555
+ parts.push(`spa=${cmdLine.spa}`);
543
556
  if (cmdLine.exclude.length > 0)
544
557
  parts.push(`exclude=[${cmdLine.exclude.join(', ')}]`);
545
558
  return parts.join(' ');
package/dist/cppCode.js CHANGED
@@ -315,6 +315,94 @@ void {{methodName}}(PsychicHttpServer * server) {
315
315
  {{/if}}{{/if}}
316
316
 
317
317
  {{/each}}
318
+ {{#if spa}}
319
+ {{#with spaSource}}
320
+ {{#if ../basePath}}
321
+ //
322
+ // SPA catch-all: unmatched routes serve {{this.filename}}
323
+ server->on("{{../basePath}}/*", HTTP_GET, [](PsychicRequest * request, PsychicResponse * response) {
324
+
325
+ {{#switch ../etag}}
326
+ {{#case "true"}}
327
+ if (request->hasHeader("If-None-Match") && request->header("If-None-Match").equals(etag_{{this.dataname}})) {
328
+ response->setCode(304);
329
+ {{../definePrefix}}_onFileServed("{{../basePath}}/{{this.filename}}", 304);
330
+ return response->send();
331
+ }
332
+ {{/case}}
333
+ {{#case "compiler"}}
334
+ #ifdef {{../definePrefix}}_ENABLE_ETAG
335
+ if (request->hasHeader("If-None-Match") && request->header("If-None-Match").equals(etag_{{this.dataname}})) {
336
+ response->setCode(304);
337
+ {{../definePrefix}}_onFileServed("{{../basePath}}/{{this.filename}}", 304);
338
+ return response->send();
339
+ }
340
+ #endif
341
+ {{/case}}
342
+ {{/switch}}
343
+
344
+ response->setContentType("{{this.mime}}");
345
+
346
+ {{#switch ../gzip}}
347
+ {{#case "true"}}
348
+ {{#if this.isGzip}}
349
+ response->addHeader("Content-Encoding", "gzip");
350
+ {{/if}}
351
+ {{/case}}
352
+ {{#case "compiler"}}
353
+ {{#if this.isGzip}}
354
+ #ifdef {{../definePrefix}}_ENABLE_GZIP
355
+ response->addHeader("Content-Encoding", "gzip");
356
+ #endif
357
+ {{/if}}
358
+ {{/case}}
359
+ {{/switch}}
360
+
361
+ {{#switch ../etag}}
362
+ {{#case "true"}}
363
+ {{#../cacheTime}}
364
+ response->addHeader("Cache-Control", "max-age={{value}}");
365
+ {{/../cacheTime}}
366
+ {{^../cacheTime}}
367
+ response->addHeader("Cache-Control", "no-cache");
368
+ {{/../cacheTime}}
369
+ response->addHeader("ETag", etag_{{this.dataname}});
370
+ {{/case}}
371
+ {{#case "compiler"}}
372
+ #ifdef {{../definePrefix}}_ENABLE_ETAG
373
+ {{#../cacheTime}}
374
+ response->addHeader("Cache-Control", "max-age={{value}}");
375
+ {{/../cacheTime}}
376
+ {{^../cacheTime}}
377
+ response->addHeader("Cache-Control", "no-cache");
378
+ {{/../cacheTime}}
379
+ response->addHeader("ETag", etag_{{this.dataname}});
380
+ #endif
381
+ {{/case}}
382
+ {{/switch}}
383
+
384
+ {{#switch ../gzip}}
385
+ {{#case "true"}}
386
+ response->setContent(datagzip_{{this.dataname}}, {{this.lengthGzip}});
387
+ {{/case}}
388
+ {{#case "false"}}
389
+ response->setContent(data_{{this.dataname}}, {{this.length}});
390
+ {{/case}}
391
+ {{#case "compiler"}}
392
+ #ifdef {{../definePrefix}}_ENABLE_GZIP
393
+ response->setContent(datagzip_{{this.dataname}}, {{this.lengthGzip}});
394
+ #else
395
+ response->setContent(data_{{this.dataname}}, {{this.length}});
396
+ #endif
397
+ {{/case}}
398
+ {{/switch}}
399
+
400
+ {{../definePrefix}}_onFileServed("{{../basePath}}/{{this.filename}}", 200);
401
+ return response->send();
402
+ });
403
+ {{/if}}
404
+ {{/with}}
405
+ {{/if}}
318
406
  }`;
319
407
  const asyncTemplate = `
320
408
  //engine: ESPAsyncWebServer
@@ -493,6 +581,87 @@ void {{methodName}}(AsyncWebServer * server) {
493
581
  {{/if}}
494
582
 
495
583
  {{/each}}
584
+ {{#if spa}}
585
+ {{#with spaSource}}
586
+ //
587
+ // SPA catch-all: unmatched routes serve {{this.filename}}
588
+ server->onNotFound([](AsyncWebServerRequest * request) {
589
+ if (request->method() != HTTP_GET) { request->send(404); return; }
590
+ {{#if ../basePath}}
591
+ if (!request->url().startsWith("{{../basePath}}/") && request->url() != "{{../basePath}}") { request->send(404); return; }
592
+ {{/if}}
593
+
594
+ {{#switch ../etag}}
595
+ {{#case "true"}}
596
+ const AsyncWebHeader* h = request->getHeader("If-None-Match");
597
+ if (h && h->value().equals(etag_{{this.dataname}})) {
598
+ {{../definePrefix}}_onFileServed("{{../basePath}}/{{this.filename}}", 304);
599
+ request->send(304);
600
+ return;
601
+ }
602
+ {{/case}}
603
+ {{#case "compiler"}}
604
+ #ifdef {{../definePrefix}}_ENABLE_ETAG
605
+ const AsyncWebHeader* h = request->getHeader("If-None-Match");
606
+ if (h && h->value().equals(etag_{{this.dataname}})) {
607
+ {{../definePrefix}}_onFileServed("{{../basePath}}/{{this.filename}}", 304);
608
+ request->send(304);
609
+ return;
610
+ }
611
+ #endif
612
+ {{/case}}
613
+ {{/switch}}
614
+
615
+ {{#switch ../gzip}}
616
+ {{#case "true"}}
617
+ AsyncWebServerResponse *response = request->beginResponse(200, "{{this.mime}}", datagzip_{{this.dataname}}, {{this.lengthGzip}});
618
+ {{#if this.isGzip}}
619
+ response->addHeader("Content-Encoding", "gzip");
620
+ {{/if}}
621
+ {{/case}}
622
+ {{#case "false"}}
623
+ AsyncWebServerResponse *response = request->beginResponse(200, "{{this.mime}}", data_{{this.dataname}}, {{this.length}});
624
+ {{/case}}
625
+ {{#case "compiler"}}
626
+ #ifdef {{../definePrefix}}_ENABLE_GZIP
627
+ AsyncWebServerResponse *response = request->beginResponse(200, "{{this.mime}}", datagzip_{{this.dataname}}, {{this.lengthGzip}});
628
+ {{#if this.isGzip}}
629
+ response->addHeader("Content-Encoding", "gzip");
630
+ {{/if}}
631
+ #else
632
+ AsyncWebServerResponse *response = request->beginResponse(200, "{{this.mime}}", data_{{this.dataname}}, {{this.length}});
633
+ #endif
634
+ {{/case}}
635
+ {{/switch}}
636
+
637
+ {{#switch ../etag}}
638
+ {{#case "true"}}
639
+ {{#../cacheTime}}
640
+ response->addHeader("Cache-Control", "max-age={{value}}");
641
+ {{/../cacheTime}}
642
+ {{^../cacheTime}}
643
+ response->addHeader("Cache-Control", "no-cache");
644
+ {{/../cacheTime}}
645
+ response->addHeader("ETag", etag_{{this.dataname}});
646
+ {{/case}}
647
+ {{#case "compiler"}}
648
+ #ifdef {{../definePrefix}}_ENABLE_ETAG
649
+ {{#../cacheTime}}
650
+ response->addHeader("Cache-Control", "max-age={{value}}");
651
+ {{/../cacheTime}}
652
+ {{^../cacheTime}}
653
+ response->addHeader("Cache-Control", "no-cache");
654
+ {{/../cacheTime}}
655
+ response->addHeader("ETag", etag_{{this.dataname}});
656
+ #endif
657
+ {{/case}}
658
+ {{/switch}}
659
+
660
+ {{../definePrefix}}_onFileServed("{{../basePath}}/{{this.filename}}", 200);
661
+ request->send(response);
662
+ });
663
+ {{/with}}
664
+ {{/if}}
496
665
  }`;
497
666
  const webserverTemplate = `
498
667
  //engine: Arduino WebServer
@@ -692,6 +861,92 @@ void {{methodName}}(WebServer * server) {
692
861
  {{/if}}
693
862
 
694
863
  {{/each}}
864
+ {{#if spa}}
865
+ {{#with spaSource}}
866
+ //
867
+ // SPA catch-all: unmatched routes serve {{this.filename}}
868
+ server->onNotFound([server]() {
869
+ if (server->method() != HTTP_GET) { server->send(404, "text/plain", "Not found"); return; }
870
+ {{#if ../basePath}}
871
+ if (!server->uri().startsWith("{{../basePath}}/") && server->uri() != "{{../basePath}}") { server->send(404, "text/plain", "Not found"); return; }
872
+ {{/if}}
873
+
874
+ {{#switch ../etag}}
875
+ {{#case "true"}}
876
+ if (server->hasHeader("If-None-Match") && server->header("If-None-Match").equals(etag_{{this.dataname}})) {
877
+ server->send(304);
878
+ {{../definePrefix}}_onFileServed("{{../basePath}}/{{this.filename}}", 304);
879
+ return;
880
+ }
881
+ {{/case}}
882
+ {{#case "compiler"}}
883
+ #ifdef {{../definePrefix}}_ENABLE_ETAG
884
+ if (server->hasHeader("If-None-Match") && server->header("If-None-Match").equals(etag_{{this.dataname}})) {
885
+ server->send(304);
886
+ {{../definePrefix}}_onFileServed("{{../basePath}}/{{this.filename}}", 304);
887
+ return;
888
+ }
889
+ #endif
890
+ {{/case}}
891
+ {{/switch}}
892
+
893
+ {{#switch ../etag}}
894
+ {{#case "true"}}
895
+ {{#../cacheTime}}
896
+ server->sendHeader("Cache-Control", "max-age={{value}}");
897
+ {{/../cacheTime}}
898
+ {{^../cacheTime}}
899
+ server->sendHeader("Cache-Control", "no-cache");
900
+ {{/../cacheTime}}
901
+ server->sendHeader("ETag", etag_{{this.dataname}});
902
+ {{/case}}
903
+ {{#case "compiler"}}
904
+ #ifdef {{../definePrefix}}_ENABLE_ETAG
905
+ {{#../cacheTime}}
906
+ server->sendHeader("Cache-Control", "max-age={{value}}");
907
+ {{/../cacheTime}}
908
+ {{^../cacheTime}}
909
+ server->sendHeader("Cache-Control", "no-cache");
910
+ {{/../cacheTime}}
911
+ server->sendHeader("ETag", etag_{{this.dataname}});
912
+ #endif
913
+ {{/case}}
914
+ {{/switch}}
915
+
916
+ {{#switch ../gzip}}
917
+ {{#case "true"}}
918
+ {{#if this.isGzip}}
919
+ server->sendHeader("Content-Encoding", "gzip");
920
+ {{/if}}
921
+ server->setContentLength({{this.lengthGzip}});
922
+ server->send(200, "{{this.mime}}", "");
923
+ {{../definePrefix}}_sendChunked(server, datagzip_{{this.dataname}}, {{this.lengthGzip}});
924
+ {{/case}}
925
+ {{#case "false"}}
926
+ server->setContentLength({{this.length}});
927
+ server->send(200, "{{this.mime}}", "");
928
+ {{../definePrefix}}_sendChunked(server, data_{{this.dataname}}, {{this.length}});
929
+ {{/case}}
930
+ {{#case "compiler"}}
931
+ #ifdef {{../definePrefix}}_ENABLE_GZIP
932
+ {{#if this.isGzip}}
933
+ server->sendHeader("Content-Encoding", "gzip");
934
+ {{/if}}
935
+ server->setContentLength({{this.lengthGzip}});
936
+ server->send(200, "{{this.mime}}", "");
937
+ {{../definePrefix}}_sendChunked(server, datagzip_{{this.dataname}}, {{this.lengthGzip}});
938
+ #else
939
+ server->setContentLength({{this.length}});
940
+ server->send(200, "{{this.mime}}", "");
941
+ {{../definePrefix}}_sendChunked(server, data_{{this.dataname}}, {{this.length}});
942
+ #endif
943
+ {{/case}}
944
+ {{/switch}}
945
+
946
+ {{../definePrefix}}_onFileServed("{{../basePath}}/{{this.filename}}", 200);
947
+ });
948
+ {{/with}}
949
+ {{/if}}
695
950
  }`;
696
951
  const getTemplate = (engine) => {
697
952
  switch (engine) {
@@ -753,6 +1008,8 @@ const createHandlebarsHelpers = () => {
753
1008
  };
754
1009
  const getCppCode = (sources, filesByExtension) => {
755
1010
  const template = (0, handlebars_1.compile)(getTemplate(commandLine_1.cmdLine.engine));
1011
+ const transformedSources = sources.map((s) => transformSourceToTemplateData(s, commandLine_1.cmdLine.etag));
1012
+ const spaSource = commandLine_1.cmdLine.spa ? transformedSources.find((s) => s.isDefault) : undefined;
756
1013
  const templateData = {
757
1014
  config: (0, commandLine_1.formatConfiguration)(commandLine_1.cmdLine),
758
1015
  now: (() => {
@@ -762,7 +1019,7 @@ const getCppCode = (sources, filesByExtension) => {
762
1019
  fileCount: sources.length.toString(),
763
1020
  fileSize: sources.reduce((previous, current) => previous + current.content.length, 0).toString(),
764
1021
  fileGzipSize: sources.reduce((previous, current) => previous + current.contentGzip.length, 0).toString(),
765
- sources: sources.map((s) => transformSourceToTemplateData(s, commandLine_1.cmdLine.etag)),
1022
+ sources: transformedSources,
766
1023
  filesByExtension,
767
1024
  etag: commandLine_1.cmdLine.etag,
768
1025
  gzip: commandLine_1.cmdLine.gzip,
@@ -771,7 +1028,9 @@ const getCppCode = (sources, filesByExtension) => {
771
1028
  methodName: commandLine_1.cmdLine.espmethod,
772
1029
  cacheTime: commandLine_1.cmdLine.cachetime ? { value: commandLine_1.cmdLine.cachetime } : undefined,
773
1030
  definePrefix: commandLine_1.cmdLine.define,
774
- basePath: commandLine_1.cmdLine.basePath
1031
+ basePath: commandLine_1.cmdLine.basePath,
1032
+ spa: !!commandLine_1.cmdLine.spa,
1033
+ spaSource
775
1034
  };
776
1035
  const rawCode = template(templateData, { helpers: createHandlebarsHelpers() });
777
1036
  return postProcessCppCode(rawCode);
@@ -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 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}";
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{{#if spa}}\n{{#with spaSource}}\nstatic esp_err_t spa_handler_{{this.datanameUpperCase}}(httpd_req_t *req, httpd_err_code_t err) {\n{{#if ../basePath}}\n const char* prefix = \"{{../basePath}}/\";\n if (strncmp(req->uri, prefix, strlen(prefix)) != 0 && strcmp(req->uri, \"{{../basePath}}\") != 0) {\n httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, \"Not found\");\n return ESP_FAIL;\n }\n{{/if}}\n return file_handler_{{this.datanameUpperCase}}(req);\n}\n{{/with}}\n{{/if}}\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{{#if spa}}\n{{#with spaSource}}\n httpd_register_err_handler(server, HTTPD_404_NOT_FOUND, spa_handler_{{this.datanameUpperCase}});\n{{/with}}\n{{/if}}\n\n}";
@@ -240,6 +240,20 @@ static const httpd_uri_t route_{{this.datanameUpperCase}} = {
240
240
 
241
241
  {{/each}}
242
242
 
243
+ {{#if spa}}
244
+ {{#with spaSource}}
245
+ static esp_err_t spa_handler_{{this.datanameUpperCase}}(httpd_req_t *req, httpd_err_code_t err) {
246
+ {{#if ../basePath}}
247
+ const char* prefix = "{{../basePath}}/";
248
+ if (strncmp(req->uri, prefix, strlen(prefix)) != 0 && strcmp(req->uri, "{{../basePath}}") != 0) {
249
+ httpd_resp_send_err(req, HTTPD_404_NOT_FOUND, "Not found");
250
+ return ESP_FAIL;
251
+ }
252
+ {{/if}}
253
+ return file_handler_{{this.datanameUpperCase}}(req);
254
+ }
255
+ {{/with}}
256
+ {{/if}}
243
257
 
244
258
 
245
259
  static inline void {{methodName}}(httpd_handle_t server) {
@@ -249,5 +263,10 @@ static inline void {{methodName}}(httpd_handle_t server) {
249
263
  {{/if}}
250
264
  httpd_register_uri_handler(server, &route_{{this.datanameUpperCase}});
251
265
  {{/each}}
266
+ {{#if spa}}
267
+ {{#with spaSource}}
268
+ httpd_register_err_handler(server, HTTPD_404_NOT_FOUND, spa_handler_{{this.datanameUpperCase}});
269
+ {{/with}}
270
+ {{/if}}
252
271
 
253
272
  }`;
package/dist/index.d.ts CHANGED
@@ -1,9 +1,10 @@
1
- import { CppCodeSource, ExtensionGroups } from './cppCode';
1
+ import { CppCodeSource, CppCodeSources, 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
5
  declare const formatSize: (bytes: number) => string;
6
6
  declare const createSourceEntry: (filename: string, dataname: string, content: Buffer, contentGzip: Buffer, mimeType: string, sha256: string, isGzip: boolean) => CppCodeSource;
7
7
  declare const updateExtensionGroup: (filesByExtension: ExtensionGroups, extension: string) => void;
8
+ declare const formatDryRunRoutes: (sources: CppCodeSources, engine: "psychic" | "async" | "espidf" | "webserver", basePath: string, spa: boolean) => string;
8
9
  export declare function main(): void;
9
- export { calculateCompressionRatio, createSourceEntry, formatCompressionLog, formatSize, shouldUseGzip, updateExtensionGroup };
10
+ export { calculateCompressionRatio, createSourceEntry, formatCompressionLog, formatDryRunRoutes, formatSize, shouldUseGzip, updateExtensionGroup };
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ 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.formatSize = exports.formatCompressionLog = exports.createSourceEntry = exports.calculateCompressionRatio = void 0;
6
+ exports.updateExtensionGroup = exports.shouldUseGzip = exports.formatSize = exports.formatDryRunRoutes = exports.formatCompressionLog = exports.createSourceEntry = exports.calculateCompressionRatio = void 0;
7
7
  exports.main = main;
8
8
  const node_fs_1 = require("node:fs");
9
9
  const node_path_1 = __importDefault(require("node:path"));
@@ -54,6 +54,46 @@ const updateExtensionGroup = (filesByExtension, extension) => {
54
54
  filesByExtension.push({ extension, count: 1 });
55
55
  };
56
56
  exports.updateExtensionGroup = updateExtensionGroup;
57
+ const sizeCellFor = (s) => {
58
+ const orig = formatSize(s.content.length);
59
+ if (s.isGzip)
60
+ return `${orig} → ${formatSize(s.contentGzip.length)}`;
61
+ return orig;
62
+ };
63
+ const formatDryRunRoutes = (sources, engine, basePath, spa) => {
64
+ if (sources.length === 0)
65
+ return ' (no files)';
66
+ const defaultSource = sources.find((s) => s.filename === 'index.html' || s.filename === 'index.htm');
67
+ const rows = [];
68
+ if (defaultSource)
69
+ rows.push({
70
+ url: basePath || '/',
71
+ mime: defaultSource.mime,
72
+ sizeCell: sizeCellFor(defaultSource),
73
+ tag: '[default]'
74
+ });
75
+ for (const source of sources)
76
+ rows.push({
77
+ url: `${basePath}/${source.filename}`,
78
+ mime: source.mime,
79
+ sizeCell: sizeCellFor(source),
80
+ tag: source.isGzip ? '' : '[no gzip]'
81
+ });
82
+ if (spa && defaultSource) {
83
+ const spaUrl = engine === 'psychic' && basePath ? `${basePath}/*` : '(SPA catch-all)';
84
+ rows.push({ url: spaUrl, mime: defaultSource.mime, sizeCell: '', tag: '[SPA catch-all → index.html]' });
85
+ }
86
+ const urlWidth = Math.max(...rows.map((r) => r.url.length));
87
+ const mimeWidth = Math.max(...rows.map((r) => r.mime.length));
88
+ const sizeWidth = Math.max(...rows.map((r) => r.sizeCell.length));
89
+ return rows
90
+ .map((r) => {
91
+ const tagPart = r.tag ? ` ${r.tag}` : '';
92
+ return ` GET ${r.url.padEnd(urlWidth)} ${r.mime.padEnd(mimeWidth)} ${r.sizeCell.padEnd(sizeWidth)}${tagPart}`.trimEnd();
93
+ })
94
+ .join('\n');
95
+ };
96
+ exports.formatDryRunRoutes = formatDryRunRoutes;
57
97
  function main() {
58
98
  const summary = {
59
99
  filecount: 0,
@@ -68,6 +108,8 @@ function main() {
68
108
  console.error(`Directory ${commandLine_1.cmdLine.sourcepath} is empty`);
69
109
  process.exit(1);
70
110
  }
111
+ if (commandLine_1.cmdLine.spa && ![...files.keys()].some((f) => f === 'index.html' || f === 'index.htm'))
112
+ console.warn((0, consoleColor_1.yellowLog)('[SvelteESP32] Warning: --spa is set but no index.html/index.htm found; catch-all will not be generated.'));
71
113
  console.log();
72
114
  console.log('Translation to header file');
73
115
  const longestFilename = [...files.keys()].reduce((p, c) => Math.max(c.length, p), 0);
@@ -104,8 +146,13 @@ function main() {
104
146
  process.exit(1);
105
147
  }
106
148
  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}`);
149
+ const baseLabel = commandLine_1.cmdLine.basePath || '(none)';
150
+ const spaLabel = commandLine_1.cmdLine.spa ? 'yes' : 'no';
151
+ console.log(`[DRY RUN] Engine: ${commandLine_1.cmdLine.engine} | ETag: ${commandLine_1.cmdLine.etag} | Gzip: ${commandLine_1.cmdLine.gzip} | Base: ${baseLabel} | SPA: ${spaLabel}`);
152
+ console.log(`[DRY RUN] ${summary.filecount} files, ${formatSize(summary.size)} → ${formatSize(summary.gzipsize)} gzip | would write to ${commandLine_1.cmdLine.outputfile}`);
153
+ console.log('');
154
+ console.log('[DRY RUN] Routes:');
155
+ console.log(formatDryRunRoutes(sources, commandLine_1.cmdLine.engine, commandLine_1.cmdLine.basePath, commandLine_1.cmdLine.spa ?? false));
109
156
  return;
110
157
  }
111
158
  const cppFile = (0, cppCode_1.getCppCode)(sources, filesByExtension);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelteesp32",
3
- "version": "2.1.0",
3
+ "version": "2.2.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",
@@ -59,25 +59,25 @@
59
59
  "espasyncwebserver"
60
60
  ],
61
61
  "devDependencies": {
62
- "@eslint/eslintrc": "^3.3.4",
62
+ "@eslint/eslintrc": "^3.3.5",
63
63
  "@eslint/js": "^10.0.1",
64
64
  "@types/mime-types": "^3.0.1",
65
- "@types/node": "^25.3.2",
65
+ "@types/node": "^25.5.0",
66
66
  "@types/picomatch": "^4.0.2",
67
- "@typescript-eslint/eslint-plugin": "^8.56.1",
68
- "@typescript-eslint/parser": "^8.56.1",
69
- "@vitest/coverage-v8": "^4.0.18",
70
- "eslint": "^10.0.2",
67
+ "@typescript-eslint/eslint-plugin": "^8.57.0",
68
+ "@typescript-eslint/parser": "^8.57.0",
69
+ "@vitest/coverage-v8": "^4.1.0",
70
+ "eslint": "^10.0.3",
71
71
  "eslint-config-prettier": "^10.1.8",
72
72
  "eslint-plugin-simple-import-sort": "^12.1.1",
73
73
  "eslint-plugin-unicorn": "^63.0.0",
74
- "memfs": "^4.56.10",
74
+ "memfs": "^4.56.11",
75
75
  "nodemon": "^3.1.14",
76
76
  "prettier": "^3.8.1",
77
77
  "ts-node": "^10.9.2",
78
78
  "tsx": "^4.21.0",
79
79
  "typescript": "^5.9.3",
80
- "vitest": "^4.0.18"
80
+ "vitest": "^4.1.0"
81
81
  },
82
82
  "dependencies": {
83
83
  "handlebars": "^4.7.8",