svelteesp32 2.3.0 → 2.3.2

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
@@ -75,13 +75,20 @@ void setup() {
75
75
 
76
76
  **That's it.** Your entire web app is now embedded and ready to serve.
77
77
 
78
+ > **Just want production-safe defaults?**
79
+ >
80
+ > ```bash
81
+ > npx svelteesp32 -e psychic -s ./dist -o ./esp32/svelteesp32.h \
82
+ > --etag=true --gzip=true --cachetime-html=0 --cachetime-assets=31536000
83
+ > ```
84
+ >
85
+ > ETags for instant 304s, gzip for smaller transfers, `no-cache` for HTML so updates are always picked up, and 1-year caching for content-hashed JS/CSS assets.
86
+
78
87
  ---
79
88
 
80
89
  ## What's New
81
90
 
82
91
  - **v2.3.0** — `--cachetime-html` and `--cachetime-assets` for per-type cache control (e.g. `no-cache` for HTML, 1-year for content-hashed JS/CSS)
83
- - **v2.2.2** — `static const` data/ETag arrays prevent multi-TU linker collisions; `SVELTEESP32_MAX_URI_HANDLERS` define added for psychic engine; default exclude patterns removed (now explicit-only)
84
- - **v2.2.1** — Enhanced `--dryrun` output: engine/ETag/gzip/SPA summary header + aligned route table with MIME types, sizes, and tags (`[default]`, `[no gzip]`, `[SPA catch-all]`)
85
92
  - **v2.2.0** — SPA routing catch-all (`--spa`) for client-side routers on all four engines
86
93
  - **v2.1.0** — New Arduino WebServer engine (`-e webserver`), dependency updates
87
94
  - **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
@@ -316,6 +323,8 @@ Reduce bandwidth dramatically with HTTP 304 "Not Modified" responses. When a bro
316
323
 
317
324
  All four engines support full ETag validation.
318
325
 
326
+ > **Browser compatibility note:** ETags and `Cache-Control: max-age` are universally supported in all modern browsers. Very old clients (IE6/7, early Android 2.x WebViews) may ignore `must-revalidate` or mishandle 304 responses. If you target these environments, set `--etag=false` and `--cachetime=0` to force full downloads on every request.
327
+
319
328
  ### Browser Cache Control
320
329
 
321
330
  Fine-tune how browsers cache your content:
@@ -540,6 +549,134 @@ npx svelteesp32 --config=.svelteesp32rc.prod.json
540
549
 
541
550
  CLI arguments always override RC file values.
542
551
 
552
+ > **Security note:** When svelteesp32 auto-loads an RC file from the current directory it prints a warning. If you cloned a project that includes `.svelteesp32rc.json`, review it before running. `outputfile` in RC files must be a relative path — use `--output` on the CLI for absolute paths.
553
+
554
+ ---
555
+
556
+ ## Common Recipes
557
+
558
+ ### Recipe A — Arduino IDE + Built-in WebServer
559
+
560
+ No PlatformIO required. Build your frontend, run svelteesp32 manually, then compile in Arduino IDE.
561
+
562
+ ```bash
563
+ # 1. Build your frontend
564
+ npm run build
565
+
566
+ # 2. Generate the header
567
+ npx svelteesp32 -e webserver -s ./dist -o ./MyProject/svelteesp32.h \
568
+ --gzip=true --etag=false
569
+ ```
570
+
571
+ > **Note:** The Arduino `WebServer` library does not support ETag validation, so `--etag=false` is the correct setting here. Remember to call `server.handleClient()` in your `loop()`.
572
+
573
+ ```c
574
+ #include <WebServer.h>
575
+ #include "svelteesp32.h"
576
+
577
+ WebServer server(80);
578
+
579
+ void setup() {
580
+ WiFi.begin("ssid", "password");
581
+ while (WiFi.status() != WL_CONNECTED) delay(500);
582
+ initSvelteStaticFiles(&server);
583
+ server.begin();
584
+ }
585
+
586
+ void loop() {
587
+ server.handleClient();
588
+ }
589
+ ```
590
+
591
+ ---
592
+
593
+ ### Recipe B — PlatformIO + Vite/SvelteKit, Auto-Generated Header on `pio run`
594
+
595
+ Run svelteesp32 automatically as a PlatformIO pre-build step so your header is always up to date.
596
+
597
+ **`platformio.ini`**
598
+
599
+ ```ini
600
+ [env:esp32dev]
601
+ platform = espressif32
602
+ board = esp32dev
603
+ framework = arduino
604
+ lib_deps =
605
+ ESP Async WebServer
606
+ extra_scripts = pre:scripts/build_frontend.py
607
+ ```
608
+
609
+ **`scripts/build_frontend.py`**
610
+
611
+ ```python
612
+ Import("env")
613
+ import subprocess
614
+
615
+ def build_frontend(source, target, env):
616
+ print("Building frontend...")
617
+ subprocess.run(["npm", "run", "build"], cwd="frontend", check=True)
618
+ subprocess.run([
619
+ "npx", "svelteesp32",
620
+ "-e", "async",
621
+ "-s", "frontend/dist",
622
+ "-o", "src/svelteesp32.h",
623
+ "--etag=true", "--gzip=true",
624
+ "--cachetime-html=0", "--cachetime-assets=31536000"
625
+ ], check=True)
626
+
627
+ env.AddPreAction("buildprog", build_frontend)
628
+ ```
629
+
630
+ This ensures the C++ header is regenerated from the latest frontend build every time you run `pio run`.
631
+
632
+ ---
633
+
634
+ ### Recipe C — ESP-IDF CMake, 1-Year Cached Content-Hashed Assets
635
+
636
+ Integrate svelteesp32 into a CMake build so the header is regenerated automatically.
637
+
638
+ **`CMakeLists.txt` (component or top-level)**
639
+
640
+ ```cmake
641
+ add_custom_command(
642
+ OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/main/svelteesp32.h
643
+ COMMAND npm run build
644
+ COMMAND npx svelteesp32
645
+ -e espidf
646
+ -s ${CMAKE_CURRENT_SOURCE_DIR}/frontend/dist
647
+ -o ${CMAKE_CURRENT_SOURCE_DIR}/main/svelteesp32.h
648
+ --etag=true --gzip=true
649
+ --cachetime-html=0 --cachetime-assets=31536000
650
+ WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
651
+ COMMENT "Generating svelteesp32.h from frontend build"
652
+ VERBATIM
653
+ )
654
+
655
+ add_custom_target(frontend_header
656
+ DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/main/svelteesp32.h
657
+ )
658
+
659
+ # Make your main component depend on the generated header
660
+ add_dependencies(${COMPONENT_LIB} frontend_header)
661
+ ```
662
+
663
+ **`main/app_main.c`**
664
+
665
+ ```c
666
+ #include <esp_http_server.h>
667
+ #include "svelteesp32.h"
668
+
669
+ void app_main(void) {
670
+ // ... WiFi init ...
671
+ httpd_config_t config = HTTPD_DEFAULT_CONFIG();
672
+ httpd_handle_t server = NULL;
673
+ httpd_start(&server, &config);
674
+ initSvelteStaticFiles(server);
675
+ }
676
+ ```
677
+
678
+ The `--cachetime-html=0 --cachetime-assets=31536000` combination gives you `no-cache` for `index.html` (so browser always checks for updates) and a 1-year `max-age` for content-hashed JS/CSS bundles.
679
+
543
680
  ---
544
681
 
545
682
  ## FAQ
@@ -94,6 +94,15 @@ function validateBasePath(value) {
94
94
  throw new Error(`basePath must not end with /: ${value}`);
95
95
  if (value.includes('//'))
96
96
  throw new Error(`basePath must not contain //: ${value}`);
97
+ if (value.includes('"'))
98
+ throw new Error(`basePath must not contain double quotes: ${value}`);
99
+ if (value.includes('\\'))
100
+ throw new Error(`basePath must not contain backslashes: ${value}`);
101
+ return value;
102
+ }
103
+ function validateVersion(value) {
104
+ if (!/^[\w+.-]+$/.test(value))
105
+ throw new Error(`version must only contain word characters, dots, hyphens, and plus signs: ${value}`);
97
106
  return value;
98
107
  }
99
108
  function parseSize(value, name) {
@@ -326,6 +335,8 @@ function validateRcConfig(config, rcPath) {
326
335
  throw new TypeError(`Invalid dryrun in RC file: ${configObject['dryrun']} (must be boolean)`);
327
336
  if (configObject['spa'] !== undefined && typeof configObject['spa'] !== 'boolean')
328
337
  throw new TypeError(`Invalid spa in RC file: ${configObject['spa']} (must be boolean)`);
338
+ if (configObject['outputfile'] !== undefined && node_path_1.default.isAbsolute(configObject['outputfile']))
339
+ throw new Error(`'outputfile' in RC file must be a relative path (use --output CLI flag for absolute paths): ${configObject['outputfile']}`);
329
340
  return configObject;
330
341
  }
331
342
  function parseArguments() {
@@ -346,8 +357,11 @@ function parseArguments() {
346
357
  }
347
358
  const rcPath = findRcFile(customConfigPath);
348
359
  const rcConfig = rcPath ? loadRcFile(rcPath) : {};
349
- if (rcPath)
360
+ if (rcPath) {
350
361
  console.log((0, consoleColor_1.cyanLog)(`[SvelteESP32] Using config from: ${rcPath}`));
362
+ if (!customConfigPath && node_path_1.default.dirname(rcPath) === process.cwd())
363
+ console.warn((0, consoleColor_1.yellowLog)(`Warning: Loading config from current directory — verify this file is trusted before proceeding.`));
364
+ }
351
365
  const result = {
352
366
  engine: 'psychic',
353
367
  outputfile: 'svelteesp32.h',
@@ -420,7 +434,7 @@ function parseArguments() {
420
434
  result.gzip = validateTriState(value, 'gzip');
421
435
  break;
422
436
  case 'version':
423
- result.version = value;
437
+ result.version = validateVersion(value);
424
438
  break;
425
439
  case 'espmethod':
426
440
  result.espmethod = validateCppIdentifier(value, 'espmethod');
@@ -576,7 +590,7 @@ function formatConfiguration(cmdLine) {
576
590
  parts.push(`spa=${cmdLine.spa}`);
577
591
  if (cmdLine.exclude.length > 0)
578
592
  parts.push(`exclude=[${cmdLine.exclude.join(', ')}]`);
579
- return parts.join(' ');
593
+ return parts.join(' ').replace(/[\n\r]/g, ' ');
580
594
  }
581
595
  exports.cmdLine = parseArguments();
582
596
  if (!(0, node_fs_1.existsSync)(exports.cmdLine.sourcepath)) {
package/dist/file.js CHANGED
@@ -51,7 +51,12 @@ const isExcluded = (filename, excludePatterns) => {
51
51
  return isMatch(normalizedFilename);
52
52
  };
53
53
  const getFiles = () => {
54
- const allFilenames = (0, tinyglobby_1.globSync)('**/*', { cwd: commandLine_1.cmdLine.sourcepath, onlyFiles: true, dot: false });
54
+ const allFilenames = (0, tinyglobby_1.globSync)('**/*', {
55
+ cwd: commandLine_1.cmdLine.sourcepath,
56
+ onlyFiles: true,
57
+ dot: false,
58
+ followSymbolicLinks: false
59
+ });
55
60
  const withoutCompressed = allFilenames.filter((filename) => !shouldSkipFile(filename, allFilenames));
56
61
  const excludePatterns = commandLine_1.cmdLine.exclude || [];
57
62
  const excludedFiles = [];
@@ -73,9 +78,13 @@ const getFiles = () => {
73
78
  }
74
79
  else if (excludePatterns.length > 0)
75
80
  console.log((0, consoleColor_1.yellowLog)(`Warning: --exclude patterns matched no files: ${excludePatterns.join(', ')}`));
81
+ const MAX_FILE_SIZE = 50 * 1024 * 1024;
76
82
  const result = new Map();
77
83
  for (const filename of filenames) {
78
84
  const filePath = node_path_1.default.join(commandLine_1.cmdLine.sourcepath, filename);
85
+ const fileSize = (0, node_fs_1.statSync)(filePath).size;
86
+ if (fileSize > MAX_FILE_SIZE)
87
+ throw new Error(`File too large (${Math.round(fileSize / 1024)}kB): ${filename}. Maximum single-file size is 50MB.`);
79
88
  const content = (0, node_fs_1.readFileSync)(filePath, { flag: 'r' });
80
89
  const hash = (0, node_crypto_1.createHash)('sha256').update(content).digest('hex');
81
90
  result.set(filename, { content, hash });
package/dist/index.js CHANGED
@@ -115,9 +115,10 @@ function main() {
115
115
  const longestFilename = [...files.keys()].reduce((p, c) => Math.max(c.length, p), 0);
116
116
  for (const [originalFilename, fileData] of files) {
117
117
  const { content, hash: sha256 } = fileData;
118
- const mimeType = (0, mime_types_1.lookup)(originalFilename) || 'text/plain';
119
- if (!(0, mime_types_1.lookup)(originalFilename))
118
+ const rawMime = (0, mime_types_1.lookup)(originalFilename);
119
+ if (!rawMime)
120
120
  console.log((0, consoleColor_1.yellowLog)(` [${originalFilename}] unknown MIME type for extension '${node_path_1.default.extname(originalFilename)}', using text/plain`));
121
+ const mimeType = rawMime || 'text/plain';
121
122
  summary.filecount++;
122
123
  const filename = originalFilename.replace(/\\/g, '/');
123
124
  let dataname = filename.replace(/\W/g, '_');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelteesp32",
3
- "version": "2.3.0",
3
+ "version": "2.3.2",
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",
@@ -64,25 +64,25 @@
64
64
  "@types/mime-types": "^3.0.1",
65
65
  "@types/node": "^25.5.0",
66
66
  "@types/picomatch": "^4.0.2",
67
- "@typescript-eslint/eslint-plugin": "^8.57.0",
68
- "@typescript-eslint/parser": "^8.57.0",
69
- "@vitest/coverage-v8": "^4.1.0",
70
- "eslint": "^10.0.3",
67
+ "@typescript-eslint/eslint-plugin": "^8.57.2",
68
+ "@typescript-eslint/parser": "^8.57.2",
69
+ "@vitest/coverage-v8": "^4.1.2",
70
+ "eslint": "^10.1.0",
71
71
  "eslint-config-prettier": "^10.1.8",
72
72
  "eslint-plugin-simple-import-sort": "^12.1.1",
73
- "eslint-plugin-unicorn": "^63.0.0",
74
- "memfs": "^4.56.11",
73
+ "eslint-plugin-unicorn": "^64.0.0",
74
+ "memfs": "^4.57.1",
75
75
  "nodemon": "^3.1.14",
76
76
  "prettier": "^3.8.1",
77
77
  "ts-node": "^10.9.2",
78
78
  "tsx": "^4.21.0",
79
79
  "typescript": "^5.9.3",
80
- "vitest": "^4.1.0"
80
+ "vitest": "^4.1.2"
81
81
  },
82
82
  "dependencies": {
83
- "handlebars": "^4.7.8",
83
+ "handlebars": "^4.7.9",
84
84
  "mime-types": "^3.0.2",
85
- "picomatch": "^4.0.3",
85
+ "picomatch": "^4.0.4",
86
86
  "tinyglobby": "^0.2.15"
87
87
  }
88
88
  }