svelteesp32 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -79,8 +79,8 @@ void setup() {
79
79
 
80
80
  ## What's New
81
81
 
82
+ - **v2.2.0** — SPA routing catch-all (`--spa`) for client-side routers on all four engines
82
83
  - **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
84
  - **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
85
  - **v1.16.0** — Size budget constraints (`--maxsize`, `--maxgzipsize`)
86
86
  - **v1.15.0** — `--basepath` for multiple frontends (e.g., `/admin`, `/app`)
@@ -376,6 +376,26 @@ void setup() {
376
376
 
377
377
  **Rules:** Must start with `/`, no trailing slash, no double slashes.
378
378
 
379
+ ### SPA Routing (Client-Side Routers)
380
+
381
+ 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`:
382
+
383
+ ```bash
384
+ npx svelteesp32 -e async -s ./dist -o ./output.h --spa
385
+ npx svelteesp32 -e psychic -s ./dist -o ./output.h --spa --basepath=/app
386
+ ```
387
+
388
+ What gets generated per engine:
389
+
390
+ | Engine | Catch-all mechanism |
391
+ | ----------- | ---------------------------------------------------------------------------------------------------------------------------- |
392
+ | `psychic` | `server->on("/*", ...)` (no basePath) already handled by `defaultEndpoint`; `server->on("/app/*", ...)` when basePath is set |
393
+ | `async` | `server->onNotFound(...)` with optional basePath prefix check |
394
+ | `webserver` | `server->onNotFound(...)` with optional basePath prefix check |
395
+ | `espidf` | `httpd_register_err_handler(HTTPD_404_NOT_FOUND, ...)` |
396
+
397
+ **Note:** `--spa` requires `index.html` or `index.htm` in the source directory — a warning is printed if it is missing.
398
+
379
399
  ### C++ Build-Time Validation
380
400
 
381
401
  Catch configuration issues at compile time with generated defines:
@@ -425,25 +445,26 @@ Called for every response (200 = content served, 304 = cache hit).
425
445
 
426
446
  ## CLI Reference
427
447
 
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 | |
448
+ | Option | Description | Default |
449
+ | ---------------- | --------------------------------------------------- | ----------------------- |
450
+ | `-s` | Source folder with compiled web files | (required) |
451
+ | `-e` | Web server engine (psychic/async/espidf/webserver) | `psychic` |
452
+ | `-o` | Output header file path | `svelteesp32.h` |
453
+ | `--etag` | ETag caching (true/false/compiler) | `false` |
454
+ | `--gzip` | Gzip compression (true/false/compiler) | `true` |
455
+ | `--exclude` | Exclude files by glob pattern | System files |
456
+ | `--basepath` | URL prefix for all routes | (none) |
457
+ | `--maxsize` | Max total uncompressed size (e.g., `400k`, `1m`) | (none) |
458
+ | `--maxgzipsize` | Max total gzip size (e.g., `150k`, `500k`) | (none) |
459
+ | `--cachetime` | Cache-Control max-age in seconds | `0` |
460
+ | `--version` | Version string in header | (none) |
461
+ | `--define` | C++ define prefix | `SVELTEESP32` |
462
+ | `--espmethod` | Init function name | `initSvelteStaticFiles` |
463
+ | `--config` | Custom RC file path | `.svelteesp32rc.json` |
464
+ | `--dryrun` | Show summary without writing output | `false` |
465
+ | `--spa` | Serve index.html for unmatched routes (SPA routing) | `false` |
466
+ | `--noindexcheck` | Skip index.html validation | `false` |
467
+ | `-h` | Show help | |
447
468
 
448
469
  ---
449
470
 
@@ -463,7 +484,8 @@ Store your settings in `.svelteesp32rc.json` for zero-argument builds:
463
484
  "maxsize": "400k",
464
485
  "maxgzipsize": "150k",
465
486
  "noindexcheck": false,
466
- "dryrun": false
487
+ "dryrun": false,
488
+ "spa": false
467
489
  }
468
490
  ```
469
491
 
@@ -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.js CHANGED
@@ -68,6 +68,8 @@ function main() {
68
68
  console.error(`Directory ${commandLine_1.cmdLine.sourcepath} is empty`);
69
69
  process.exit(1);
70
70
  }
71
+ if (commandLine_1.cmdLine.spa && ![...files.keys()].some((f) => f === 'index.html' || f === 'index.htm'))
72
+ 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
73
  console.log();
72
74
  console.log('Translation to header file');
73
75
  const longestFilename = [...files.keys()].reduce((p, c) => Math.max(c.length, p), 0);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelteesp32",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
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,19 +59,19 @@
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.4.0",
66
66
  "@types/picomatch": "^4.0.2",
67
- "@typescript-eslint/eslint-plugin": "^8.56.1",
68
- "@typescript-eslint/parser": "^8.56.1",
67
+ "@typescript-eslint/eslint-plugin": "^8.57.0",
68
+ "@typescript-eslint/parser": "^8.57.0",
69
69
  "@vitest/coverage-v8": "^4.0.18",
70
- "eslint": "^10.0.2",
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",