svelteesp32 2.2.0 → 2.2.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
@@ -79,6 +79,8 @@ void setup() {
79
79
 
80
80
  ## What's New
81
81
 
82
+ - **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)
83
+ - **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]`)
82
84
  - **v2.2.0** — SPA routing catch-all (`--spa`) for client-side routers on all four engines
83
85
  - **v2.1.0** — New Arduino WebServer engine (`-e webserver`), dependency updates
84
86
  - **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
@@ -290,7 +292,7 @@ void initSvelteStaticFiles(PsychicHttpServer * server) {
290
292
 
291
293
  **Recommendation:** For ESP32-only projects, use PsychicHttpServer V2 (`-e psychic`) for the fastest, most stable experience.
292
294
 
293
- **Note:** For PsychicHttp, configure `server.config.max_uri_handlers` to match your file count.
295
+ **Note:** For PsychicHttp, configure `server.config.max_uri_handlers`. The generated header provides `SVELTEESP32_MAX_URI_HANDLERS` (file count + 5 safety margin) for use directly in your sketch.
294
296
 
295
297
  ---
296
298
 
@@ -342,7 +344,7 @@ npx svelteesp32 -s ./dist -o ./output.h --exclude="*.map"
342
344
  npx svelteesp32 -s ./dist -o ./output.h --exclude="*.map,*.md,test/**/*"
343
345
  ```
344
346
 
345
- **Default exclusions:** `.DS_Store`, `Thumbs.db`, `.git`, `.svn`, `*.swp`, `*~`, `.gitignore`, `.gitattributes`
347
+ No patterns are excluded by default specify everything you need explicitly.
346
348
 
347
349
  Build output shows exactly what's excluded:
348
350
 
@@ -461,7 +463,7 @@ Called for every response (200 = content served, 304 = cache hit).
461
463
  | `--define` | C++ define prefix | `SVELTEESP32` |
462
464
  | `--espmethod` | Init function name | `initSvelteStaticFiles` |
463
465
  | `--config` | Custom RC file path | `.svelteesp32rc.json` |
464
- | `--dryrun` | Show summary without writing output | `false` |
466
+ | `--dryrun` | Show route table + summary without writing output | `false` |
465
467
  | `--spa` | Serve index.html for unmatched routes (SPA routing) | `false` |
466
468
  | `--noindexcheck` | Skip index.html validation | `false` |
467
469
  | `-h` | Show help | |
@@ -113,16 +113,6 @@ function parseSize(value, name) {
113
113
  throw new Error(`${name} must be a positive integer: ${value}`);
114
114
  return bytes;
115
115
  }
116
- const DEFAULT_EXCLUDE_PATTERNS = [
117
- '.DS_Store',
118
- 'Thumbs.db',
119
- '.git',
120
- '.svn',
121
- '*.swp',
122
- '*~',
123
- '.gitignore',
124
- '.gitattributes'
125
- ];
126
116
  function findRcFile(customConfigPath) {
127
117
  if (customConfigPath) {
128
118
  if ((0, node_fs_1.existsSync)(customConfigPath))
@@ -186,8 +176,7 @@ function hasNpmVariables(config) {
186
176
  checkStringForNpmVariable(config.define) ||
187
177
  checkStringForNpmVariable(config.version) ||
188
178
  checkStringForNpmVariable(config.basepath) ||
189
- (Array.isArray(config.exclude) && config.exclude.some((pattern) => checkStringForNpmVariable(pattern))) ||
190
- false);
179
+ (Array.isArray(config.exclude) && config.exclude.some((pattern) => checkStringForNpmVariable(pattern))));
191
180
  }
192
181
  function interpolateNpmVariables(config, rcFilePath) {
193
182
  if (!hasNpmVariables(config))
@@ -250,6 +239,20 @@ function loadRcFile(rcPath) {
250
239
  throw error;
251
240
  }
252
241
  }
242
+ function validateSizeOption(configObject, key) {
243
+ const value = configObject[key];
244
+ if (value === undefined)
245
+ return;
246
+ if (typeof value === 'string')
247
+ try {
248
+ configObject[key] = parseSize(value, key);
249
+ }
250
+ catch {
251
+ throw new TypeError(`Invalid ${key} in RC file: ${value} (must be a positive number with optional k/m suffix)`);
252
+ }
253
+ else if (typeof value !== 'number' || Number.isNaN(value) || value <= 0)
254
+ throw new TypeError(`Invalid ${key} in RC file: ${value} (must be a positive number)`);
255
+ }
253
256
  function validateRcConfig(config, rcPath) {
254
257
  if (typeof config !== 'object' || config === null)
255
258
  throw new Error(`RC file ${rcPath} must contain a JSON object`);
@@ -299,30 +302,8 @@ function validateRcConfig(config, rcPath) {
299
302
  if (typeof pattern !== 'string')
300
303
  throw new TypeError('All exclude patterns must be strings');
301
304
  }
302
- if (configObject['maxsize'] !== undefined) {
303
- const value = configObject['maxsize'];
304
- if (typeof value === 'string')
305
- try {
306
- configObject['maxsize'] = parseSize(value, 'maxsize');
307
- }
308
- catch {
309
- throw new TypeError(`Invalid maxsize in RC file: ${value} (must be a positive number with optional k/m suffix)`);
310
- }
311
- else if (typeof value !== 'number' || Number.isNaN(value) || value <= 0)
312
- throw new TypeError(`Invalid maxsize in RC file: ${value} (must be a positive number)`);
313
- }
314
- if (configObject['maxgzipsize'] !== undefined) {
315
- const value = configObject['maxgzipsize'];
316
- if (typeof value === 'string')
317
- try {
318
- configObject['maxgzipsize'] = parseSize(value, 'maxgzipsize');
319
- }
320
- catch {
321
- throw new TypeError(`Invalid maxgzipsize in RC file: ${value} (must be a positive number with optional k/m suffix)`);
322
- }
323
- else if (typeof value !== 'number' || Number.isNaN(value) || value <= 0)
324
- throw new TypeError(`Invalid maxgzipsize in RC file: ${value} (must be a positive number)`);
325
- }
305
+ validateSizeOption(configObject, 'maxsize');
306
+ validateSizeOption(configObject, 'maxgzipsize');
326
307
  if (configObject['noindexcheck'] !== undefined && typeof configObject['noindexcheck'] !== 'boolean')
327
308
  throw new TypeError(`Invalid noindexcheck in RC file: ${configObject['noindexcheck']} (must be boolean)`);
328
309
  if (configObject['dryrun'] !== undefined && typeof configObject['dryrun'] !== 'boolean')
@@ -361,7 +342,7 @@ function parseArguments() {
361
342
  espmethod: 'initSvelteStaticFiles',
362
343
  define: 'SVELTEESP32',
363
344
  cachetime: 0,
364
- exclude: [...DEFAULT_EXCLUDE_PATTERNS],
345
+ exclude: [],
365
346
  basePath: ''
366
347
  };
367
348
  if (rcConfig.engine)
package/dist/cppCode.js CHANGED
@@ -38,6 +38,9 @@ const commonHeaderSection = `
38
38
  #define {{definePrefix}}_COUNT {{fileCount}}
39
39
  #define {{definePrefix}}_SIZE {{fileSize}}
40
40
  #define {{definePrefix}}_SIZE_GZIP {{fileGzipSize}}
41
+ {{#if isPsychic}}
42
+ #define {{definePrefix}}_MAX_URI_HANDLERS {{maxUriHandlers}}
43
+ {{/if}}
41
44
 
42
45
  //
43
46
  {{#each sources}}
@@ -55,22 +58,22 @@ const dataArraysSection = (progmem = false) => {
55
58
  {{#switch gzip}}
56
59
  {{#case "true"}}
57
60
  {{#each sources}}
58
- const uint8_t datagzip_{{this.dataname}}[{{this.lengthGzip}}]${memDirective} = { {{this.bytesGzip}} };
61
+ static const uint8_t datagzip_{{this.dataname}}[{{this.lengthGzip}}]${memDirective} = { {{this.bytesGzip}} };
59
62
  {{/each}}
60
63
  {{/case}}
61
64
  {{#case "false"}}
62
65
  {{#each sources}}
63
- const uint8_t data_{{this.dataname}}[{{this.length}}]${memDirective} = { {{this.bytes}} };
66
+ static const uint8_t data_{{this.dataname}}[{{this.length}}]${memDirective} = { {{this.bytes}} };
64
67
  {{/each}}
65
68
  {{/case}}
66
69
  {{#case "compiler"}}
67
70
  #ifdef {{definePrefix}}_ENABLE_GZIP
68
71
  {{#each sources}}
69
- const uint8_t datagzip_{{this.dataname}}[{{this.lengthGzip}}]${memDirective} = { {{this.bytesGzip}} };
72
+ static const uint8_t datagzip_{{this.dataname}}[{{this.lengthGzip}}]${memDirective} = { {{this.bytesGzip}} };
70
73
  {{/each}}
71
74
  #else
72
75
  {{#each sources}}
73
- const uint8_t data_{{this.dataname}}[{{this.length}}]${memDirective} = { {{this.bytes}} };
76
+ static const uint8_t data_{{this.dataname}}[{{this.length}}]${memDirective} = { {{this.bytes}} };
74
77
  {{/each}}
75
78
  #endif
76
79
  {{/case}}
@@ -81,7 +84,7 @@ const etagArraysSection = `
81
84
  {{#switch etag}}
82
85
  {{#case "true"}}
83
86
  {{#each sources}}
84
- const char * etag_{{this.dataname}} = "{{this.sha256}}";
87
+ static const char etag_{{this.dataname}}[] = "{{this.sha256}}";
85
88
  {{/each}}
86
89
  {{/case}}
87
90
  {{#case "false"}}
@@ -89,7 +92,7 @@ const char * etag_{{this.dataname}} = "{{this.sha256}}";
89
92
  {{#case "compiler"}}
90
93
  #ifdef {{definePrefix}}_ENABLE_ETAG
91
94
  {{#each sources}}
92
- const char * etag_{{this.dataname}} = "{{this.sha256}}";
95
+ static const char etag_{{this.dataname}}[] = "{{this.sha256}}";
93
96
  {{/each}}
94
97
  #endif
95
98
  {{/case}}
@@ -106,12 +109,12 @@ struct {{definePrefix}}_FileInfo {
106
109
  };
107
110
 
108
111
  // File manifest array
109
- const {{definePrefix}}_FileInfo {{definePrefix}}_FILES[] = {
112
+ static const {{definePrefix}}_FileInfo {{definePrefix}}_FILES[] = {
110
113
  {{#each sources}}
111
114
  { "{{../basePath}}/{{this.filename}}", {{this.length}}, {{this.gzipSizeForManifest}}, {{this.etagForManifest}}, "{{this.mime}}" },
112
115
  {{/each}}
113
116
  };
114
- const size_t {{definePrefix}}_FILE_COUNT = sizeof({{definePrefix}}_FILES) / sizeof({{definePrefix}}_FILES[0]);
117
+ static const size_t {{definePrefix}}_FILE_COUNT = sizeof({{definePrefix}}_FILES) / sizeof({{definePrefix}}_FILES[0]);
115
118
  `;
116
119
  const hookSection = `
117
120
  // File served hook - override with your own implementation
@@ -1030,7 +1033,9 @@ const getCppCode = (sources, filesByExtension) => {
1030
1033
  definePrefix: commandLine_1.cmdLine.define,
1031
1034
  basePath: commandLine_1.cmdLine.basePath,
1032
1035
  spa: !!commandLine_1.cmdLine.spa,
1033
- spaSource
1036
+ spaSource,
1037
+ isPsychic: commandLine_1.cmdLine.engine === 'psychic',
1038
+ maxUriHandlers: (sources.length + 5).toString()
1034
1039
  };
1035
1040
  const rawCode = template(templateData, { helpers: createHandlebarsHelpers() });
1036
1041
  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{{#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}";
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}}\nstatic const unsigned char datagzip_{{this.dataname}}[{{this.lengthGzip}}] = { {{this.bytesGzip}} };\n {{/each}}\n{{/case}}\n{{#case \"false\"}}\n {{#each sources}}\nstatic const unsigned char data_{{this.dataname}}[{{this.length}}] = { {{this.bytes}} };\n {{/each}}\n{{/case}}\n{{#case \"compiler\"}}\n#ifdef {{definePrefix}}_ENABLE_GZIP\n {{#each sources}}\nstatic const unsigned char datagzip_{{this.dataname}}[{{this.lengthGzip}}] = { {{this.bytesGzip}} };\n {{/each}}\n#else\n {{#each sources}}\nstatic const 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}";
@@ -63,22 +63,22 @@ exports.espidfTemplate = `
63
63
  {{#switch gzip}}
64
64
  {{#case "true"}}
65
65
  {{#each sources}}
66
- const unsigned char datagzip_{{this.dataname}}[{{this.lengthGzip}}] = { {{this.bytesGzip}} };
66
+ static const unsigned char datagzip_{{this.dataname}}[{{this.lengthGzip}}] = { {{this.bytesGzip}} };
67
67
  {{/each}}
68
68
  {{/case}}
69
69
  {{#case "false"}}
70
70
  {{#each sources}}
71
- const unsigned char data_{{this.dataname}}[{{this.length}}] = { {{this.bytes}} };
71
+ static const unsigned char data_{{this.dataname}}[{{this.length}}] = { {{this.bytes}} };
72
72
  {{/each}}
73
73
  {{/case}}
74
74
  {{#case "compiler"}}
75
75
  #ifdef {{definePrefix}}_ENABLE_GZIP
76
76
  {{#each sources}}
77
- const unsigned char datagzip_{{this.dataname}}[{{this.lengthGzip}}] = { {{this.bytesGzip}} };
77
+ static const unsigned char datagzip_{{this.dataname}}[{{this.lengthGzip}}] = { {{this.bytesGzip}} };
78
78
  {{/each}}
79
79
  #else
80
80
  {{#each sources}}
81
- const unsigned char data_{{this.dataname}}[{{this.length}}] = { {{this.bytes}} };
81
+ static const unsigned char data_{{this.dataname}}[{{this.length}}] = { {{this.bytes}} };
82
82
  {{/each}}
83
83
  #endif
84
84
  {{/case}}
@@ -140,7 +140,7 @@ function getMaxUriHandlersHint(engine, routeCount, espmethod = 'initSvelteStatic
140
140
  const recommended = routeCount + 5;
141
141
  const hints = {
142
142
  psychic: `PsychicHttpServer server;
143
- server.config.max_uri_handlers = ${recommended}; // Default is 8, you need at least ${routeCount}
143
+ server.config.max_uri_handlers = SVELTEESP32_MAX_URI_HANDLERS; // already defined in the header (${recommended})
144
144
  ${espmethod}(&server);
145
145
  server.listen(80);`,
146
146
  espidf: `httpd_config_t config = HTTPD_DEFAULT_CONFIG();
package/dist/index.d.ts CHANGED
@@ -1,9 +1,10 @@
1
- import { CppCodeSource, ExtensionGroups } from './cppCode';
1
+ import { CppCodeSource, CppCodeSources, ExtensionGroups } from './cppCode';
2
2
  declare const shouldUseGzip: (originalSize: number, compressedSize: number) => boolean;
3
3
  declare const calculateCompressionRatio: (originalSize: number, compressedSize: number) => number;
4
4
  declare const formatCompressionLog: (filename: string, padding: string, originalSize: number, compressedSize: number, useGzip: boolean) => string;
5
5
  declare const formatSize: (bytes: number) => string;
6
6
  declare const createSourceEntry: (filename: string, dataname: string, content: Buffer, contentGzip: Buffer, mimeType: string, sha256: string, isGzip: boolean) => CppCodeSource;
7
7
  declare const updateExtensionGroup: (filesByExtension: ExtensionGroups, extension: string) => void;
8
+ declare const formatDryRunRoutes: (sources: CppCodeSources, engine: "psychic" | "async" | "espidf" | "webserver", basePath: string, spa: boolean) => string;
8
9
  export declare function main(): void;
9
- export { calculateCompressionRatio, createSourceEntry, formatCompressionLog, formatSize, shouldUseGzip, updateExtensionGroup };
10
+ export { calculateCompressionRatio, createSourceEntry, formatCompressionLog, formatDryRunRoutes, formatSize, shouldUseGzip, updateExtensionGroup };
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.updateExtensionGroup = exports.shouldUseGzip = exports.formatSize = exports.formatCompressionLog = exports.createSourceEntry = exports.calculateCompressionRatio = void 0;
6
+ exports.updateExtensionGroup = exports.shouldUseGzip = exports.formatSize = exports.formatDryRunRoutes = exports.formatCompressionLog = exports.createSourceEntry = exports.calculateCompressionRatio = void 0;
7
7
  exports.main = main;
8
8
  const node_fs_1 = require("node:fs");
9
9
  const node_path_1 = __importDefault(require("node:path"));
@@ -54,6 +54,46 @@ const updateExtensionGroup = (filesByExtension, extension) => {
54
54
  filesByExtension.push({ extension, count: 1 });
55
55
  };
56
56
  exports.updateExtensionGroup = updateExtensionGroup;
57
+ const sizeCellFor = (s) => {
58
+ const orig = formatSize(s.content.length);
59
+ if (s.isGzip)
60
+ return `${orig} → ${formatSize(s.contentGzip.length)}`;
61
+ return orig;
62
+ };
63
+ const formatDryRunRoutes = (sources, engine, basePath, spa) => {
64
+ if (sources.length === 0)
65
+ return ' (no files)';
66
+ const defaultSource = sources.find((s) => s.filename === 'index.html' || s.filename === 'index.htm');
67
+ const rows = [];
68
+ if (defaultSource)
69
+ rows.push({
70
+ url: basePath || '/',
71
+ mime: defaultSource.mime,
72
+ sizeCell: sizeCellFor(defaultSource),
73
+ tag: '[default]'
74
+ });
75
+ for (const source of sources)
76
+ rows.push({
77
+ url: `${basePath}/${source.filename}`,
78
+ mime: source.mime,
79
+ sizeCell: sizeCellFor(source),
80
+ tag: source.isGzip ? '' : '[no gzip]'
81
+ });
82
+ if (spa && defaultSource) {
83
+ const spaUrl = engine === 'psychic' && basePath ? `${basePath}/*` : '(SPA catch-all)';
84
+ rows.push({ url: spaUrl, mime: defaultSource.mime, sizeCell: '', tag: '[SPA catch-all → index.html]' });
85
+ }
86
+ const urlWidth = Math.max(...rows.map((r) => r.url.length));
87
+ const mimeWidth = Math.max(...rows.map((r) => r.mime.length));
88
+ const sizeWidth = Math.max(...rows.map((r) => r.sizeCell.length));
89
+ return rows
90
+ .map((r) => {
91
+ const tagPart = r.tag ? ` ${r.tag}` : '';
92
+ return ` GET ${r.url.padEnd(urlWidth)} ${r.mime.padEnd(mimeWidth)} ${r.sizeCell.padEnd(sizeWidth)}${tagPart}`.trimEnd();
93
+ })
94
+ .join('\n');
95
+ };
96
+ exports.formatDryRunRoutes = formatDryRunRoutes;
57
97
  function main() {
58
98
  const summary = {
59
99
  filecount: 0,
@@ -106,8 +146,13 @@ function main() {
106
146
  process.exit(1);
107
147
  }
108
148
  if (commandLine_1.cmdLine.dryRun) {
109
- console.log(`[DRY RUN] ${summary.filecount} files, ${formatSize(summary.size)} original size, ${formatSize(summary.gzipsize)} gzip size`);
110
- console.log(`[DRY RUN] Would write to ${commandLine_1.cmdLine.outputfile}`);
149
+ const baseLabel = commandLine_1.cmdLine.basePath || '(none)';
150
+ const spaLabel = commandLine_1.cmdLine.spa ? 'yes' : 'no';
151
+ console.log(`[DRY RUN] Engine: ${commandLine_1.cmdLine.engine} | ETag: ${commandLine_1.cmdLine.etag} | Gzip: ${commandLine_1.cmdLine.gzip} | Base: ${baseLabel} | SPA: ${spaLabel}`);
152
+ console.log(`[DRY RUN] ${summary.filecount} files, ${formatSize(summary.size)} → ${formatSize(summary.gzipsize)} gzip | would write to ${commandLine_1.cmdLine.outputfile}`);
153
+ console.log('');
154
+ console.log('[DRY RUN] Routes:');
155
+ console.log(formatDryRunRoutes(sources, commandLine_1.cmdLine.engine, commandLine_1.cmdLine.basePath, commandLine_1.cmdLine.spa ?? false));
111
156
  return;
112
157
  }
113
158
  const cppFile = (0, cppCode_1.getCppCode)(sources, filesByExtension);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelteesp32",
3
- "version": "2.2.0",
3
+ "version": "2.2.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",
@@ -62,11 +62,11 @@
62
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.4.0",
65
+ "@types/node": "^25.5.0",
66
66
  "@types/picomatch": "^4.0.2",
67
67
  "@typescript-eslint/eslint-plugin": "^8.57.0",
68
68
  "@typescript-eslint/parser": "^8.57.0",
69
- "@vitest/coverage-v8": "^4.0.18",
69
+ "@vitest/coverage-v8": "^4.1.0",
70
70
  "eslint": "^10.0.3",
71
71
  "eslint-config-prettier": "^10.1.8",
72
72
  "eslint-plugin-simple-import-sort": "^12.1.1",
@@ -77,7 +77,7 @@
77
77
  "ts-node": "^10.9.2",
78
78
  "tsx": "^4.21.0",
79
79
  "typescript": "^5.9.3",
80
- "vitest": "^4.0.18"
80
+ "vitest": "^4.1.0"
81
81
  },
82
82
  "dependencies": {
83
83
  "handlebars": "^4.7.8",