svelteesp32 2.0.1 → 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
@@ -25,7 +25,7 @@
25
25
  - **Smart Caching** — Built-in SHA256 ETags deliver HTTP 304 responses, slashing bandwidth on constrained devices.
26
26
  - **CI/CD Ready** — Simple npm package that slots into any build pipeline.
27
27
  - **Zero Runtime Overhead** — Data served directly from flash. No filesystem reads, no RAM allocation.
28
- - **3 Web Server Engines** — PsychicHttpServer, ESPAsyncWebServer, and native ESP-IDF supported.
28
+ - **4 Web Server Engines** — PsychicHttpServer V2, ESPAsyncWebServer, Arduino WebServer, and native ESP-IDF supported.
29
29
 
30
30
  ---
31
31
 
@@ -79,7 +79,8 @@ void setup() {
79
79
 
80
80
  ## What's New
81
81
 
82
- - **v2.0.1** — Dependency updates (ESLint v10, eslint-plugin-unicorn 63), improved error cause chaining
82
+ - **v2.2.0** — SPA routing catch-all (`--spa`) for client-side routers on all four engines
83
+ - **v2.1.0** — New Arduino WebServer engine (`-e webserver`), dependency updates
83
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
84
85
  - **v1.16.0** — Size budget constraints (`--maxsize`, `--maxgzipsize`)
85
86
  - **v1.15.0** — `--basepath` for multiple frontends (e.g., `/admin`, `/app`)
@@ -116,6 +117,9 @@ npx svelteesp32 -e psychic -s ./dist -o ./esp32/svelteesp32.h --etag=true
116
117
  # ESPAsyncWebServer (ESP32 + ESP8266)
117
118
  npx svelteesp32 -e async -s ./dist -o ./esp32/svelteesp32.h --etag=true
118
119
 
120
+ # Arduino WebServer (ESP32, synchronous, no dependencies)
121
+ npx svelteesp32 -e webserver -s ./dist -o ./esp32/svelteesp32.h --etag=true
122
+
119
123
  # Native ESP-IDF
120
124
  npx svelteesp32 -e espidf -s ./dist -o ./esp32/svelteesp32.h --etag=true
121
125
  ```
@@ -171,6 +175,24 @@ void setup() {
171
175
  }
172
176
  ```
173
177
 
178
+ **Arduino WebServer (built-in, no dependencies)**
179
+
180
+ ```c
181
+ #include <WebServer.h>
182
+ #include "svelteesp32.h"
183
+
184
+ WebServer server(80);
185
+
186
+ void setup() {
187
+ initSvelteStaticFiles(&server);
188
+ server.begin();
189
+ }
190
+
191
+ void loop() {
192
+ server.handleClient();
193
+ }
194
+ ```
195
+
174
196
  **Native ESP-IDF**
175
197
 
176
198
  ```c
@@ -259,11 +281,12 @@ void initSvelteStaticFiles(PsychicHttpServer * server) {
259
281
 
260
282
  ## Supported Web Server Engines
261
283
 
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 |
284
+ | Engine | Flag | Best For | Platform |
285
+ | ------------------------ | -------------- | ---------------------------- | --------------- |
286
+ | **PsychicHttpServer V2** | `-e psychic` | Maximum performance | ESP32 only |
287
+ | **ESPAsyncWebServer** | `-e async` | Cross-platform compatibility | ESP32 + ESP8266 |
288
+ | **Arduino WebServer** | `-e webserver` | No dependencies, simplicity | ESP32 only |
289
+ | **Native ESP-IDF** | `-e espidf` | Pure ESP-IDF projects | ESP32 only |
267
290
 
268
291
  **Recommendation:** For ESP32-only projects, use PsychicHttpServer V2 (`-e psychic`) for the fastest, most stable experience.
269
292
 
@@ -288,7 +311,7 @@ Reduce bandwidth dramatically with HTTP 304 "Not Modified" responses. When a bro
288
311
  - **Minimal overhead** — adds ~1-3% code size for significant bandwidth savings
289
312
  - **Compiler mode** — use `--etag=compiler` and control via `-D SVELTEESP32_ENABLE_ETAG`
290
313
 
291
- All three engines support full ETag validation.
314
+ All four engines support full ETag validation.
292
315
 
293
316
  ### Browser Cache Control
294
317
 
@@ -353,6 +376,26 @@ void setup() {
353
376
 
354
377
  **Rules:** Must start with `/`, no trailing slash, no double slashes.
355
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
+
356
399
  ### C++ Build-Time Validation
357
400
 
358
401
  Catch configuration issues at compile time with generated defines:
@@ -402,25 +445,26 @@ Called for every response (200 = content served, 304 = cache hit).
402
445
 
403
446
  ## CLI Reference
404
447
 
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 | |
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 | |
424
468
 
425
469
  ---
426
470
 
@@ -440,7 +484,8 @@ Store your settings in `.svelteesp32rc.json` for zero-argument builds:
440
484
  "maxsize": "400k",
441
485
  "maxgzipsize": "150k",
442
486
  "noindexcheck": false,
443
- "dryrun": false
487
+ "dryrun": false,
488
+ "spa": false
444
489
  }
445
490
  ```
446
491
 
@@ -1,5 +1,5 @@
1
1
  interface ICopyFilesArguments {
2
- engine: 'psychic' | 'async' | 'espidf';
2
+ engine: 'psychic' | 'async' | 'espidf' | 'webserver';
3
3
  sourcepath: string;
4
4
  outputfile: string;
5
5
  espmethod: string;
@@ -15,10 +15,11 @@ 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 {
21
- engine?: 'psychic' | 'async' | 'espidf';
22
+ engine?: 'psychic' | 'async' | 'espidf' | 'webserver';
22
23
  sourcepath?: string;
23
24
  outputfile?: string;
24
25
  espmethod?: string;
@@ -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;
@@ -24,7 +24,7 @@ Configuration:
24
24
 
25
25
  Options:
26
26
  -e, --engine <value> The engine for which the include file is created
27
- (psychic|async|espidf) (default: "psychic")
27
+ (psychic|async|espidf|webserver) (default: "psychic")
28
28
  -s, --sourcepath <path> Source dist folder contains compiled web files (required)
29
29
  -o, --outputfile <path> Generated output file with path (default: "svelteesp32.h")
30
30
  --etag <value> Use ETAG header for cache (true|false|compiler) (default: "false")
@@ -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.
@@ -66,7 +68,7 @@ RC File:
66
68
  process.exit(0);
67
69
  }
68
70
  function validateEngine(value) {
69
- if (value === 'psychic' || value === 'async' || value === 'espidf')
71
+ if (value === 'psychic' || value === 'async' || value === 'espidf' || value === 'webserver')
70
72
  return value;
71
73
  console.error((0, errorMessages_1.getInvalidEngineError)(value));
72
74
  process.exit(1);
@@ -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,372 @@ 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}}
665
+ }`;
666
+ const webserverTemplate = `
667
+ //engine: Arduino WebServer
668
+ //config: {{{config}}}
669
+ {{#if created }}
670
+ //created: {{now}}
671
+ {{/if}}
672
+ //
673
+ ${commonHeaderSection}
674
+
675
+ //
676
+ #include <Arduino.h>
677
+ #include <WebServer.h>
678
+
679
+ //
680
+ ${dataArraysSection(true)}
681
+
682
+ //
683
+ ${etagArraysSection}
684
+
685
+ //
686
+ ${manifestSection}
687
+
688
+ //
689
+ ${hookSection}
690
+
691
+ //
692
+ // Chunked send helper for PROGMEM data
693
+ static inline void {{definePrefix}}_sendChunked(WebServer * server, const uint8_t * data, size_t len) {
694
+ const size_t chunkSize = 4096;
695
+ for (size_t offset = 0; offset < len; offset += chunkSize) {
696
+ size_t remaining = len - offset;
697
+ size_t toSend = remaining < chunkSize ? remaining : chunkSize;
698
+ server->sendContent_P((const char *)(data + offset), toSend);
699
+ }
700
+ }
701
+
702
+ //
703
+ // Http Handlers
704
+ void {{methodName}}(WebServer * server) {
705
+ {{#each sources}}
706
+ //
707
+ // {{this.filename}}
708
+ server->on("{{../basePath}}/{{this.filename}}", HTTP_GET, [server]() {
709
+
710
+ {{#switch ../etag}}
711
+ {{#case "true"}}
712
+ if (server->hasHeader("If-None-Match") && server->header("If-None-Match").equals(etag_{{this.dataname}})) {
713
+ server->send(304);
714
+ {{../definePrefix}}_onFileServed("{{../basePath}}/{{this.filename}}", 304);
715
+ return;
716
+ }
717
+ {{/case}}
718
+ {{#case "compiler"}}
719
+ #ifdef {{../definePrefix}}_ENABLE_ETAG
720
+ if (server->hasHeader("If-None-Match") && server->header("If-None-Match").equals(etag_{{this.dataname}})) {
721
+ server->send(304);
722
+ {{../definePrefix}}_onFileServed("{{../basePath}}/{{this.filename}}", 304);
723
+ return;
724
+ }
725
+ #endif
726
+ {{/case}}
727
+ {{/switch}}
728
+
729
+ {{#switch ../etag}}
730
+ {{#case "true"}}
731
+ {{#../cacheTime}}
732
+ server->sendHeader("Cache-Control", "max-age={{value}}");
733
+ {{/../cacheTime}}
734
+ {{^../cacheTime}}
735
+ server->sendHeader("Cache-Control", "no-cache");
736
+ {{/../cacheTime}}
737
+ server->sendHeader("ETag", etag_{{this.dataname}});
738
+ {{/case}}
739
+ {{#case "compiler"}}
740
+ #ifdef {{../definePrefix}}_ENABLE_ETAG
741
+ {{#../cacheTime}}
742
+ server->sendHeader("Cache-Control", "max-age={{value}}");
743
+ {{/../cacheTime}}
744
+ {{^../cacheTime}}
745
+ server->sendHeader("Cache-Control", "no-cache");
746
+ {{/../cacheTime}}
747
+ server->sendHeader("ETag", etag_{{this.dataname}});
748
+ #endif
749
+ {{/case}}
750
+ {{/switch}}
751
+
752
+ {{#switch ../gzip}}
753
+ {{#case "true"}}
754
+ {{#if this.isGzip}}
755
+ server->sendHeader("Content-Encoding", "gzip");
756
+ {{/if}}
757
+ server->setContentLength({{this.lengthGzip}});
758
+ server->send(200, "{{this.mime}}", "");
759
+ {{../definePrefix}}_sendChunked(server, datagzip_{{this.dataname}}, {{this.lengthGzip}});
760
+ {{/case}}
761
+ {{#case "false"}}
762
+ server->setContentLength({{this.length}});
763
+ server->send(200, "{{this.mime}}", "");
764
+ {{../definePrefix}}_sendChunked(server, data_{{this.dataname}}, {{this.length}});
765
+ {{/case}}
766
+ {{#case "compiler"}}
767
+ #ifdef {{../definePrefix}}_ENABLE_GZIP
768
+ {{#if this.isGzip}}
769
+ server->sendHeader("Content-Encoding", "gzip");
770
+ {{/if}}
771
+ server->setContentLength({{this.lengthGzip}});
772
+ server->send(200, "{{this.mime}}", "");
773
+ {{../definePrefix}}_sendChunked(server, datagzip_{{this.dataname}}, {{this.lengthGzip}});
774
+ #else
775
+ server->setContentLength({{this.length}});
776
+ server->send(200, "{{this.mime}}", "");
777
+ {{../definePrefix}}_sendChunked(server, data_{{this.dataname}}, {{this.length}});
778
+ #endif
779
+ {{/case}}
780
+ {{/switch}}
781
+
782
+ {{../definePrefix}}_onFileServed("{{../basePath}}/{{this.filename}}", 200);
783
+ });
784
+ {{#if this.isDefault}}
785
+ server->on("{{#if ../basePath}}{{../basePath}}{{else}}/{{/if}}", HTTP_GET, [server]() {
786
+
787
+ {{#switch ../etag}}
788
+ {{#case "true"}}
789
+ if (server->hasHeader("If-None-Match") && server->header("If-None-Match").equals(etag_{{this.dataname}})) {
790
+ server->send(304);
791
+ {{../definePrefix}}_onFileServed("{{#if ../basePath}}{{../basePath}}{{else}}/{{/if}}", 304);
792
+ return;
793
+ }
794
+ {{/case}}
795
+ {{#case "compiler"}}
796
+ #ifdef {{../definePrefix}}_ENABLE_ETAG
797
+ if (server->hasHeader("If-None-Match") && server->header("If-None-Match").equals(etag_{{this.dataname}})) {
798
+ server->send(304);
799
+ {{../definePrefix}}_onFileServed("{{#if ../basePath}}{{../basePath}}{{else}}/{{/if}}", 304);
800
+ return;
801
+ }
802
+ #endif
803
+ {{/case}}
804
+ {{/switch}}
805
+
806
+ {{#switch ../etag}}
807
+ {{#case "true"}}
808
+ {{#../cacheTime}}
809
+ server->sendHeader("Cache-Control", "max-age={{value}}");
810
+ {{/../cacheTime}}
811
+ {{^../cacheTime}}
812
+ server->sendHeader("Cache-Control", "no-cache");
813
+ {{/../cacheTime}}
814
+ server->sendHeader("ETag", etag_{{this.dataname}});
815
+ {{/case}}
816
+ {{#case "compiler"}}
817
+ #ifdef {{../definePrefix}}_ENABLE_ETAG
818
+ {{#../cacheTime}}
819
+ server->sendHeader("Cache-Control", "max-age={{value}}");
820
+ {{/../cacheTime}}
821
+ {{^../cacheTime}}
822
+ server->sendHeader("Cache-Control", "no-cache");
823
+ {{/../cacheTime}}
824
+ server->sendHeader("ETag", etag_{{this.dataname}});
825
+ #endif
826
+ {{/case}}
827
+ {{/switch}}
828
+
829
+ {{#switch ../gzip}}
830
+ {{#case "true"}}
831
+ {{#if this.isGzip}}
832
+ server->sendHeader("Content-Encoding", "gzip");
833
+ {{/if}}
834
+ server->setContentLength({{this.lengthGzip}});
835
+ server->send(200, "{{this.mime}}", "");
836
+ {{../definePrefix}}_sendChunked(server, datagzip_{{this.dataname}}, {{this.lengthGzip}});
837
+ {{/case}}
838
+ {{#case "false"}}
839
+ server->setContentLength({{this.length}});
840
+ server->send(200, "{{this.mime}}", "");
841
+ {{../definePrefix}}_sendChunked(server, data_{{this.dataname}}, {{this.length}});
842
+ {{/case}}
843
+ {{#case "compiler"}}
844
+ #ifdef {{../definePrefix}}_ENABLE_GZIP
845
+ {{#if this.isGzip}}
846
+ server->sendHeader("Content-Encoding", "gzip");
847
+ {{/if}}
848
+ server->setContentLength({{this.lengthGzip}});
849
+ server->send(200, "{{this.mime}}", "");
850
+ {{../definePrefix}}_sendChunked(server, datagzip_{{this.dataname}}, {{this.lengthGzip}});
851
+ #else
852
+ server->setContentLength({{this.length}});
853
+ server->send(200, "{{this.mime}}", "");
854
+ {{../definePrefix}}_sendChunked(server, data_{{this.dataname}}, {{this.length}});
855
+ #endif
856
+ {{/case}}
857
+ {{/switch}}
858
+
859
+ {{../definePrefix}}_onFileServed("{{#if ../basePath}}{{../basePath}}{{else}}/{{/if}}", 200);
860
+ });
861
+ {{/if}}
862
+
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}}
496
950
  }`;
497
951
  const getTemplate = (engine) => {
498
952
  switch (engine) {
@@ -502,6 +956,8 @@ const getTemplate = (engine) => {
502
956
  return asyncTemplate;
503
957
  case 'espidf':
504
958
  return cppCodeEspIdf_1.espidfTemplate;
959
+ case 'webserver':
960
+ return webserverTemplate;
505
961
  default:
506
962
  throw new Error(`Unknown engine: ${engine}`);
507
963
  }
@@ -552,6 +1008,8 @@ const createHandlebarsHelpers = () => {
552
1008
  };
553
1009
  const getCppCode = (sources, filesByExtension) => {
554
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;
555
1013
  const templateData = {
556
1014
  config: (0, commandLine_1.formatConfiguration)(commandLine_1.cmdLine),
557
1015
  now: (() => {
@@ -561,7 +1019,7 @@ const getCppCode = (sources, filesByExtension) => {
561
1019
  fileCount: sources.length.toString(),
562
1020
  fileSize: sources.reduce((previous, current) => previous + current.content.length, 0).toString(),
563
1021
  fileGzipSize: sources.reduce((previous, current) => previous + current.contentGzip.length, 0).toString(),
564
- sources: sources.map((s) => transformSourceToTemplateData(s, commandLine_1.cmdLine.etag)),
1022
+ sources: transformedSources,
565
1023
  filesByExtension,
566
1024
  etag: commandLine_1.cmdLine.etag,
567
1025
  gzip: commandLine_1.cmdLine.gzip,
@@ -570,7 +1028,9 @@ const getCppCode = (sources, filesByExtension) => {
570
1028
  methodName: commandLine_1.cmdLine.espmethod,
571
1029
  cacheTime: commandLine_1.cmdLine.cachetime ? { value: commandLine_1.cmdLine.cachetime } : undefined,
572
1030
  definePrefix: commandLine_1.cmdLine.define,
573
- basePath: commandLine_1.cmdLine.basePath
1031
+ basePath: commandLine_1.cmdLine.basePath,
1032
+ spa: !!commandLine_1.cmdLine.spa,
1033
+ spaSource
574
1034
  };
575
1035
  const rawCode = template(templateData, { helpers: createHandlebarsHelpers() });
576
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
  }`;
@@ -14,7 +14,8 @@ function getEngineName(engine) {
14
14
  const names = {
15
15
  psychic: 'PsychicHttpServer',
16
16
  async: 'ESPAsyncWebServer',
17
- espidf: 'ESP-IDF'
17
+ espidf: 'ESP-IDF',
18
+ webserver: 'Arduino WebServer'
18
19
  };
19
20
  return names[engine] ?? engine;
20
21
  }
@@ -28,7 +29,10 @@ function getMissingIndexError(engine) {
28
29
  3. ESPAsyncWebServer uses: server.on("/", HTTP_GET, ...)`,
29
30
  espidf: ` 1. Add an index.html file to your source directory
30
31
  2. The file will register both "/" and "/index.html" routes
31
- 3. ESP-IDF uses: httpd_register_uri_handler(server, &route_def_...)`
32
+ 3. ESP-IDF uses: httpd_register_uri_handler(server, &route_def_...)`,
33
+ webserver: ` 1. Add an index.html file to your source directory
34
+ 2. The file will automatically create a "/" route handler
35
+ 3. Arduino WebServer uses: server.on("/", HTTP_GET, handler)`
32
36
  };
33
37
  const hint = hints[engine] ?? hints['psychic'];
34
38
  return ((0, consoleColor_1.redLog)('[ERROR] No index.html or index.htm found in source files') +
@@ -53,6 +57,7 @@ Valid engines are:
53
57
  ${(0, consoleColor_1.cyanLog)('• psychic')} - PsychicHttpServer (ESP32 only, fastest performance)
54
58
  ${(0, consoleColor_1.cyanLog)('• async')} - ESPAsyncWebServer (ESP32/ESP8266 compatible)
55
59
  ${(0, consoleColor_1.cyanLog)('• espidf')} - Native ESP-IDF web server (ESP32 only, no Arduino)
60
+ ${(0, consoleColor_1.cyanLog)('• webserver')} - Arduino WebServer (ESP32, synchronous, built-in)
56
61
 
57
62
  How to fix:
58
63
  npx svelteesp32 --engine=psychic --sourcepath=./dist
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.0.1",
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",
@@ -30,6 +30,7 @@
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:webserver": "nodemon src/index.ts -- -e webserver -s ./demo/svelte/dist -o ./demo/esp32/include/svelteesp32.h --etag=true --gzip=true --cachetime=86400 --version=v$npm_package_version",
33
34
  "test": "vitest run",
34
35
  "test:watch": "vitest",
35
36
  "test:coverage": "vitest run --coverage",
@@ -58,20 +59,20 @@
58
59
  "espasyncwebserver"
59
60
  ],
60
61
  "devDependencies": {
61
- "@eslint/eslintrc": "^3.3.3",
62
- "@eslint/js": "^10.0.0",
62
+ "@eslint/eslintrc": "^3.3.5",
63
+ "@eslint/js": "^10.0.1",
63
64
  "@types/mime-types": "^3.0.1",
64
- "@types/node": "^25.3.0",
65
+ "@types/node": "^25.4.0",
65
66
  "@types/picomatch": "^4.0.2",
66
- "@typescript-eslint/eslint-plugin": "^8.56.0",
67
- "@typescript-eslint/parser": "^8.56.0",
67
+ "@typescript-eslint/eslint-plugin": "^8.57.0",
68
+ "@typescript-eslint/parser": "^8.57.0",
68
69
  "@vitest/coverage-v8": "^4.0.18",
69
- "eslint": "^10.0.0",
70
+ "eslint": "^10.0.3",
70
71
  "eslint-config-prettier": "^10.1.8",
71
72
  "eslint-plugin-simple-import-sort": "^12.1.1",
72
73
  "eslint-plugin-unicorn": "^63.0.0",
73
- "memfs": "^4.56.10",
74
- "nodemon": "^3.1.11",
74
+ "memfs": "^4.56.11",
75
+ "nodemon": "^3.1.14",
75
76
  "prettier": "^3.8.1",
76
77
  "ts-node": "^10.9.2",
77
78
  "tsx": "^4.21.0",