svelteesp32 2.0.0 → 2.1.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,6 +79,8 @@ void setup() {
79
79
 
80
80
  ## What's New
81
81
 
82
+ - **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
82
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
83
85
  - **v1.16.0** — Size budget constraints (`--maxsize`, `--maxgzipsize`)
84
86
  - **v1.15.0** — `--basepath` for multiple frontends (e.g., `/admin`, `/app`)
@@ -115,6 +117,9 @@ npx svelteesp32 -e psychic -s ./dist -o ./esp32/svelteesp32.h --etag=true
115
117
  # ESPAsyncWebServer (ESP32 + ESP8266)
116
118
  npx svelteesp32 -e async -s ./dist -o ./esp32/svelteesp32.h --etag=true
117
119
 
120
+ # Arduino WebServer (ESP32, synchronous, no dependencies)
121
+ npx svelteesp32 -e webserver -s ./dist -o ./esp32/svelteesp32.h --etag=true
122
+
118
123
  # Native ESP-IDF
119
124
  npx svelteesp32 -e espidf -s ./dist -o ./esp32/svelteesp32.h --etag=true
120
125
  ```
@@ -170,6 +175,24 @@ void setup() {
170
175
  }
171
176
  ```
172
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
+
173
196
  **Native ESP-IDF**
174
197
 
175
198
  ```c
@@ -258,11 +281,12 @@ void initSvelteStaticFiles(PsychicHttpServer * server) {
258
281
 
259
282
  ## Supported Web Server Engines
260
283
 
261
- | Engine | Flag | Best For | Platform |
262
- | ------------------------ | ------------ | ---------------------------- | --------------- |
263
- | **PsychicHttpServer V2** | `-e psychic` | Maximum performance | ESP32 only |
264
- | **ESPAsyncWebServer** | `-e async` | Cross-platform compatibility | ESP32 + ESP8266 |
265
- | **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 |
266
290
 
267
291
  **Recommendation:** For ESP32-only projects, use PsychicHttpServer V2 (`-e psychic`) for the fastest, most stable experience.
268
292
 
@@ -287,7 +311,7 @@ Reduce bandwidth dramatically with HTTP 304 "Not Modified" responses. When a bro
287
311
  - **Minimal overhead** — adds ~1-3% code size for significant bandwidth savings
288
312
  - **Compiler mode** — use `--etag=compiler` and control via `-D SVELTEESP32_ENABLE_ETAG`
289
313
 
290
- All three engines support full ETag validation.
314
+ All four engines support full ETag validation.
291
315
 
292
316
  ### Browser Cache Control
293
317
 
@@ -401,25 +425,25 @@ Called for every response (200 = content served, 304 = cache hit).
401
425
 
402
426
  ## CLI Reference
403
427
 
404
- | Option | Description | Default |
405
- | ---------------- | ------------------------------------------------ | ----------------------- |
406
- | `-s` | Source folder with compiled web files | (required) |
407
- | `-e` | Web server engine (psychic/async/espidf) | `psychic` |
408
- | `-o` | Output header file path | `svelteesp32.h` |
409
- | `--etag` | ETag caching (true/false/compiler) | `false` |
410
- | `--gzip` | Gzip compression (true/false/compiler) | `true` |
411
- | `--exclude` | Exclude files by glob pattern | System files |
412
- | `--basepath` | URL prefix for all routes | (none) |
413
- | `--maxsize` | Max total uncompressed size (e.g., `400k`, `1m`) | (none) |
414
- | `--maxgzipsize` | Max total gzip size (e.g., `150k`, `500k`) | (none) |
415
- | `--cachetime` | Cache-Control max-age in seconds | `0` |
416
- | `--version` | Version string in header | (none) |
417
- | `--define` | C++ define prefix | `SVELTEESP32` |
418
- | `--espmethod` | Init function name | `initSvelteStaticFiles` |
419
- | `--config` | Custom RC file path | `.svelteesp32rc.json` |
420
- | `--dryrun` | Show summary without writing output | `false` |
421
- | `--noindexcheck` | Skip index.html validation | `false` |
422
- | `-h` | Show help | |
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 | |
423
447
 
424
448
  ---
425
449
 
@@ -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;
@@ -18,7 +18,7 @@ interface ICopyFilesArguments {
18
18
  help?: boolean;
19
19
  }
20
20
  interface IRcFileConfig {
21
- engine?: 'psychic' | 'async' | 'espidf';
21
+ engine?: 'psychic' | 'async' | 'espidf' | 'webserver';
22
22
  sourcepath?: string;
23
23
  outputfile?: string;
24
24
  espmethod?: string;
@@ -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")
@@ -66,7 +66,7 @@ RC File:
66
66
  process.exit(0);
67
67
  }
68
68
  function validateEngine(value) {
69
- if (value === 'psychic' || value === 'async' || value === 'espidf')
69
+ if (value === 'psychic' || value === 'async' || value === 'espidf' || value === 'webserver')
70
70
  return value;
71
71
  console.error((0, errorMessages_1.getInvalidEngineError)(value));
72
72
  process.exit(1);
@@ -151,7 +151,9 @@ function parsePackageJson(packageJsonPath) {
151
151
  return JSON.parse(content);
152
152
  }
153
153
  catch (error) {
154
- throw new Error(`Failed to parse package.json at ${packageJsonPath}: ${error.message}`);
154
+ throw new Error(`Failed to parse package.json at ${packageJsonPath}: ${error.message}`, {
155
+ cause: error
156
+ });
155
157
  }
156
158
  }
157
159
  function getNpmPackageVariable(packageJson, variableName) {
@@ -242,7 +244,7 @@ function loadRcFile(rcPath) {
242
244
  }
243
245
  catch (error) {
244
246
  if (error instanceof SyntaxError)
245
- throw new Error(`Invalid JSON in RC file ${rcPath}: ${error.message}`);
247
+ throw new Error(`Invalid JSON in RC file ${rcPath}: ${error.message}`, { cause: error });
246
248
  throw error;
247
249
  }
248
250
  }
package/dist/cppCode.js CHANGED
@@ -492,6 +492,205 @@ void {{methodName}}(AsyncWebServer * server) {
492
492
  });
493
493
  {{/if}}
494
494
 
495
+ {{/each}}
496
+ }`;
497
+ const webserverTemplate = `
498
+ //engine: Arduino WebServer
499
+ //config: {{{config}}}
500
+ {{#if created }}
501
+ //created: {{now}}
502
+ {{/if}}
503
+ //
504
+ ${commonHeaderSection}
505
+
506
+ //
507
+ #include <Arduino.h>
508
+ #include <WebServer.h>
509
+
510
+ //
511
+ ${dataArraysSection(true)}
512
+
513
+ //
514
+ ${etagArraysSection}
515
+
516
+ //
517
+ ${manifestSection}
518
+
519
+ //
520
+ ${hookSection}
521
+
522
+ //
523
+ // Chunked send helper for PROGMEM data
524
+ static inline void {{definePrefix}}_sendChunked(WebServer * server, const uint8_t * data, size_t len) {
525
+ const size_t chunkSize = 4096;
526
+ for (size_t offset = 0; offset < len; offset += chunkSize) {
527
+ size_t remaining = len - offset;
528
+ size_t toSend = remaining < chunkSize ? remaining : chunkSize;
529
+ server->sendContent_P((const char *)(data + offset), toSend);
530
+ }
531
+ }
532
+
533
+ //
534
+ // Http Handlers
535
+ void {{methodName}}(WebServer * server) {
536
+ {{#each sources}}
537
+ //
538
+ // {{this.filename}}
539
+ server->on("{{../basePath}}/{{this.filename}}", HTTP_GET, [server]() {
540
+
541
+ {{#switch ../etag}}
542
+ {{#case "true"}}
543
+ if (server->hasHeader("If-None-Match") && server->header("If-None-Match").equals(etag_{{this.dataname}})) {
544
+ server->send(304);
545
+ {{../definePrefix}}_onFileServed("{{../basePath}}/{{this.filename}}", 304);
546
+ return;
547
+ }
548
+ {{/case}}
549
+ {{#case "compiler"}}
550
+ #ifdef {{../definePrefix}}_ENABLE_ETAG
551
+ if (server->hasHeader("If-None-Match") && server->header("If-None-Match").equals(etag_{{this.dataname}})) {
552
+ server->send(304);
553
+ {{../definePrefix}}_onFileServed("{{../basePath}}/{{this.filename}}", 304);
554
+ return;
555
+ }
556
+ #endif
557
+ {{/case}}
558
+ {{/switch}}
559
+
560
+ {{#switch ../etag}}
561
+ {{#case "true"}}
562
+ {{#../cacheTime}}
563
+ server->sendHeader("Cache-Control", "max-age={{value}}");
564
+ {{/../cacheTime}}
565
+ {{^../cacheTime}}
566
+ server->sendHeader("Cache-Control", "no-cache");
567
+ {{/../cacheTime}}
568
+ server->sendHeader("ETag", etag_{{this.dataname}});
569
+ {{/case}}
570
+ {{#case "compiler"}}
571
+ #ifdef {{../definePrefix}}_ENABLE_ETAG
572
+ {{#../cacheTime}}
573
+ server->sendHeader("Cache-Control", "max-age={{value}}");
574
+ {{/../cacheTime}}
575
+ {{^../cacheTime}}
576
+ server->sendHeader("Cache-Control", "no-cache");
577
+ {{/../cacheTime}}
578
+ server->sendHeader("ETag", etag_{{this.dataname}});
579
+ #endif
580
+ {{/case}}
581
+ {{/switch}}
582
+
583
+ {{#switch ../gzip}}
584
+ {{#case "true"}}
585
+ {{#if this.isGzip}}
586
+ server->sendHeader("Content-Encoding", "gzip");
587
+ {{/if}}
588
+ server->setContentLength({{this.lengthGzip}});
589
+ server->send(200, "{{this.mime}}", "");
590
+ {{../definePrefix}}_sendChunked(server, datagzip_{{this.dataname}}, {{this.lengthGzip}});
591
+ {{/case}}
592
+ {{#case "false"}}
593
+ server->setContentLength({{this.length}});
594
+ server->send(200, "{{this.mime}}", "");
595
+ {{../definePrefix}}_sendChunked(server, data_{{this.dataname}}, {{this.length}});
596
+ {{/case}}
597
+ {{#case "compiler"}}
598
+ #ifdef {{../definePrefix}}_ENABLE_GZIP
599
+ {{#if this.isGzip}}
600
+ server->sendHeader("Content-Encoding", "gzip");
601
+ {{/if}}
602
+ server->setContentLength({{this.lengthGzip}});
603
+ server->send(200, "{{this.mime}}", "");
604
+ {{../definePrefix}}_sendChunked(server, datagzip_{{this.dataname}}, {{this.lengthGzip}});
605
+ #else
606
+ server->setContentLength({{this.length}});
607
+ server->send(200, "{{this.mime}}", "");
608
+ {{../definePrefix}}_sendChunked(server, data_{{this.dataname}}, {{this.length}});
609
+ #endif
610
+ {{/case}}
611
+ {{/switch}}
612
+
613
+ {{../definePrefix}}_onFileServed("{{../basePath}}/{{this.filename}}", 200);
614
+ });
615
+ {{#if this.isDefault}}
616
+ server->on("{{#if ../basePath}}{{../basePath}}{{else}}/{{/if}}", HTTP_GET, [server]() {
617
+
618
+ {{#switch ../etag}}
619
+ {{#case "true"}}
620
+ if (server->hasHeader("If-None-Match") && server->header("If-None-Match").equals(etag_{{this.dataname}})) {
621
+ server->send(304);
622
+ {{../definePrefix}}_onFileServed("{{#if ../basePath}}{{../basePath}}{{else}}/{{/if}}", 304);
623
+ return;
624
+ }
625
+ {{/case}}
626
+ {{#case "compiler"}}
627
+ #ifdef {{../definePrefix}}_ENABLE_ETAG
628
+ if (server->hasHeader("If-None-Match") && server->header("If-None-Match").equals(etag_{{this.dataname}})) {
629
+ server->send(304);
630
+ {{../definePrefix}}_onFileServed("{{#if ../basePath}}{{../basePath}}{{else}}/{{/if}}", 304);
631
+ return;
632
+ }
633
+ #endif
634
+ {{/case}}
635
+ {{/switch}}
636
+
637
+ {{#switch ../etag}}
638
+ {{#case "true"}}
639
+ {{#../cacheTime}}
640
+ server->sendHeader("Cache-Control", "max-age={{value}}");
641
+ {{/../cacheTime}}
642
+ {{^../cacheTime}}
643
+ server->sendHeader("Cache-Control", "no-cache");
644
+ {{/../cacheTime}}
645
+ server->sendHeader("ETag", etag_{{this.dataname}});
646
+ {{/case}}
647
+ {{#case "compiler"}}
648
+ #ifdef {{../definePrefix}}_ENABLE_ETAG
649
+ {{#../cacheTime}}
650
+ server->sendHeader("Cache-Control", "max-age={{value}}");
651
+ {{/../cacheTime}}
652
+ {{^../cacheTime}}
653
+ server->sendHeader("Cache-Control", "no-cache");
654
+ {{/../cacheTime}}
655
+ server->sendHeader("ETag", etag_{{this.dataname}});
656
+ #endif
657
+ {{/case}}
658
+ {{/switch}}
659
+
660
+ {{#switch ../gzip}}
661
+ {{#case "true"}}
662
+ {{#if this.isGzip}}
663
+ server->sendHeader("Content-Encoding", "gzip");
664
+ {{/if}}
665
+ server->setContentLength({{this.lengthGzip}});
666
+ server->send(200, "{{this.mime}}", "");
667
+ {{../definePrefix}}_sendChunked(server, datagzip_{{this.dataname}}, {{this.lengthGzip}});
668
+ {{/case}}
669
+ {{#case "false"}}
670
+ server->setContentLength({{this.length}});
671
+ server->send(200, "{{this.mime}}", "");
672
+ {{../definePrefix}}_sendChunked(server, data_{{this.dataname}}, {{this.length}});
673
+ {{/case}}
674
+ {{#case "compiler"}}
675
+ #ifdef {{../definePrefix}}_ENABLE_GZIP
676
+ {{#if this.isGzip}}
677
+ server->sendHeader("Content-Encoding", "gzip");
678
+ {{/if}}
679
+ server->setContentLength({{this.lengthGzip}});
680
+ server->send(200, "{{this.mime}}", "");
681
+ {{../definePrefix}}_sendChunked(server, datagzip_{{this.dataname}}, {{this.lengthGzip}});
682
+ #else
683
+ server->setContentLength({{this.length}});
684
+ server->send(200, "{{this.mime}}", "");
685
+ {{../definePrefix}}_sendChunked(server, data_{{this.dataname}}, {{this.length}});
686
+ #endif
687
+ {{/case}}
688
+ {{/switch}}
689
+
690
+ {{../definePrefix}}_onFileServed("{{#if ../basePath}}{{../basePath}}{{else}}/{{/if}}", 200);
691
+ });
692
+ {{/if}}
693
+
495
694
  {{/each}}
496
695
  }`;
497
696
  const getTemplate = (engine) => {
@@ -502,6 +701,8 @@ const getTemplate = (engine) => {
502
701
  return asyncTemplate;
503
702
  case 'espidf':
504
703
  return cppCodeEspIdf_1.espidfTemplate;
704
+ case 'webserver':
705
+ return webserverTemplate;
505
706
  default:
506
707
  throw new Error(`Unknown engine: ${engine}`);
507
708
  }
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelteesp32",
3
- "version": "2.0.0",
3
+ "version": "2.1.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,18 +59,20 @@
58
59
  "espasyncwebserver"
59
60
  ],
60
61
  "devDependencies": {
62
+ "@eslint/eslintrc": "^3.3.4",
63
+ "@eslint/js": "^10.0.1",
61
64
  "@types/mime-types": "^3.0.1",
62
- "@types/node": "^25.2.2",
65
+ "@types/node": "^25.3.2",
63
66
  "@types/picomatch": "^4.0.2",
64
- "@typescript-eslint/eslint-plugin": "^8.54.0",
65
- "@typescript-eslint/parser": "^8.54.0",
67
+ "@typescript-eslint/eslint-plugin": "^8.56.1",
68
+ "@typescript-eslint/parser": "^8.56.1",
66
69
  "@vitest/coverage-v8": "^4.0.18",
67
- "eslint": "^9.39.2",
70
+ "eslint": "^10.0.2",
68
71
  "eslint-config-prettier": "^10.1.8",
69
72
  "eslint-plugin-simple-import-sort": "^12.1.1",
70
- "eslint-plugin-unicorn": "^62.0.0",
73
+ "eslint-plugin-unicorn": "^63.0.0",
71
74
  "memfs": "^4.56.10",
72
- "nodemon": "^3.1.11",
75
+ "nodemon": "^3.1.14",
73
76
  "prettier": "^3.8.1",
74
77
  "ts-node": "^10.9.2",
75
78
  "tsx": "^4.21.0",