svelteesp32 1.9.1 → 1.9.3

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
@@ -18,7 +18,7 @@ This npm package provides a solution for **inserting any JS client application i
18
18
 
19
19
  > Starting with version v1.7.0, with the cachetime command line option, you can set whether the browser can cache pages
20
20
 
21
- > Starting with version v1.6.0, mime npm package is used instead of mime-types (application/javascript -> text/javascript)
21
+ > Starting with version v1.6.0, mime-types package properly handles MIME types (application/javascript -> text/javascript)
22
22
 
23
23
  > Starting with version v1.5.0, PsychicHttp v2 is also supported.
24
24
 
@@ -30,6 +30,11 @@ This npm package provides a solution for **inserting any JS client application i
30
30
 
31
31
  > Starting with version v1.1.0, the ETag header is also supported.
32
32
 
33
+ ### Requirements
34
+
35
+ - Node.js >= 20
36
+ - npm >= 9
37
+
33
38
  ### Usage
34
39
 
35
40
  **Install package** as devDependency (it is practical if the package is part of the project so that you always receive updates)
@@ -54,19 +59,25 @@ npx svelteesp32 -e async -s ../svelteapp/dist -o ../esp32project/svelteesp32.h -
54
59
  npx svelteesp32 -e espidf -s ../svelteapp/dist -o ../esp32project/svelteesp32.h --etag=true
55
60
  ```
56
61
 
57
- During the **translation process**, the processed file details are visible, and at the end, the result shows the ESP's memory allocation (gzip size)
62
+ During the **translation process**, the processed file details are visible with compression ratios, and at the end, the result shows the ESP's memory allocation (gzip size)
58
63
 
59
64
  ```
60
- [assets/index-KwubEIf-.js] ✓ gzip used (38850 -> 12547)
61
- [assets/index-Soe6cpLA.css] ✓ gzip used (32494 -> 5368)
62
- [favicon.png] x gzip unused (33249 -> 33282)
63
- [index.html] x gzip unused (too small) (472 -> 308)
64
- [roboto_regular.json] ✓ gzip used (363757 -> 93567)
65
+ [assets/index-KwubEIf-.js] ✓ gzip used (38850 -> 12547 = 32%)
66
+ [assets/index-Soe6cpLA.css] ✓ gzip used (32494 -> 5368 = 17%)
67
+ [favicon.png] x gzip unused (33249 -> 33282 = 100%)
68
+ [index.html] x gzip unused (too small) (472 -> 308 = 65%)
69
+ [roboto_regular.json] ✓ gzip used (363757 -> 93567 = 26%)
65
70
 
66
71
  5 files, 458kB original size, 142kB gzip size
67
72
  ../../../Arduino/EspSvelte/svelteesp32.h 842kB size
68
73
  ```
69
74
 
75
+ The tool automatically:
76
+
77
+ - Compresses files with gzip level 9 when beneficial (>1024 bytes and >15% reduction)
78
+ - Detects and reports duplicate files using SHA256 hashing
79
+ - Skips pre-compressed files (.gz, .br, .brottli) if the original exists
80
+
70
81
  **Include svelteesp32.h** into your Arduino or PlatformIO c++ project (copy it next to the main c++ file)
71
82
 
72
83
  ```c
@@ -111,59 +122,107 @@ void setup()
111
122
  }
112
123
  ```
113
124
 
114
- You can find a minimal buildable example platformio project in [demo/esp32](demo/esp32) folder.
125
+ or for ESP-IDF native:
126
+
127
+ ```c
128
+ ...
129
+
130
+ #include <esp_http_server.h>
131
+ #include "svelteesp32.h"
132
+
133
+ httpd_handle_t server = NULL;
134
+
135
+ ...
136
+
137
+ void app_main()
138
+ {
139
+ ...
140
+
141
+ httpd_config_t config = HTTPD_DEFAULT_CONFIG();
142
+ httpd_start(&server, &config);
143
+
144
+ initSvelteStaticFiles(server);
145
+ }
146
+ ```
147
+
148
+ You can find minimal buildable example projects in [demo/esp32](demo/esp32) (Arduino/PlatformIO) and [demo/esp32idf](demo/esp32idf) (ESP-IDF native) folders.
115
149
 
116
- The content of **generated file** (do not edit, just use)
150
+ The content of **generated file** (do not edit, just use):
117
151
 
118
152
  ```c
153
+ //engine: PsychicHttpServer
154
+ //cmdline: -e psychic -s ./dist -o ./output.h --etag=true --gzip=true
155
+ //
119
156
  #define SVELTEESP32_COUNT 5
120
- #define SVELTEESP32_SIZE 145633
157
+ #define SVELTEESP32_SIZE 468822
158
+ #define SVELTEESP32_SIZE_GZIP 145633
121
159
  #define SVELTEESP32_FILE_INDEX_HTML
122
160
  #define SVELTEESP32_HTML_FILES 1
161
+ #define SVELTEESP32_CSS_FILES 1
162
+ #define SVELTEESP32_JS_FILES 1
123
163
  ...
124
164
 
125
- const uint8_t data0[12547] = {0x1f, 0x8b, 0x8, 0x0, ...
126
- const uint8_t data1[5368] = {0x1f, 0x8b, 0x8, 0x0, 0x0, ...
127
- const char * etag0 = "387b88e345cc56ef9091...";
165
+ #include <Arduino.h>
166
+ #include <PsychicHttp.h>
167
+ #include <PsychicHttpsServer.h>
168
+
169
+ const uint8_t datagzip_assets_index_KwubEIf__js[12547] = {0x1f, 0x8b, 0x8, 0x0, ...
170
+ const uint8_t datagzip_assets_index_Soe6cpLA_css[5368] = {0x1f, 0x8b, 0x8, 0x0, 0x0, ...
171
+ const char * etag_assets_index_KwubEIf__js = "387b88e345cc56ef9091...";
172
+ const char * etag_assets_index_Soe6cpLA_css = "d4f23bc45ef67890ab12...";
173
+ ...
128
174
 
129
175
  void initSvelteStaticFiles(PsychicHttpServer * server) {
130
- server->on("/assets/index-KwubEIf-.js", HTTP_GET, [](PsychicRequest * request)
131
- {
132
- if (request->hasHeader("If-None-Match") && ...) {
176
+ server->on("/assets/index-KwubEIf-.js", HTTP_GET, [](PsychicRequest * request) {
177
+ if (request->hasHeader("If-None-Match") &&
178
+ request->header("If-None-Match").equals(etag_assets_index_KwubEIf__js)) {
133
179
  PsychicResponse response304(request);
134
180
  response304.setCode(304);
135
181
  return response304.send();
136
182
  }
137
183
 
138
184
  PsychicResponse response(request);
139
- response.setContentType("application/javascript");
185
+ response.setContentType("text/javascript");
140
186
  response.addHeader("Content-Encoding", "gzip");
141
- response.setContent(data0, sizeof(data0));
187
+ response.addHeader("Cache-Control", "no-cache");
188
+ response.addHeader("ETag", etag_assets_index_KwubEIf__js);
189
+ response.setContent(datagzip_assets_index_KwubEIf__js, 12547);
142
190
  return response.send();
143
191
  });
144
192
 
145
- server->on("/assets/index-Soe6cpLA.css", HTTP_GET, [](PsychicRequest * request)
146
- {
147
- if (request->hasHeader("If-None-Match") && ...) {
148
- ...
193
+ server->on("/assets/index-Soe6cpLA.css", HTTP_GET, [](PsychicRequest * request) {
194
+ if (request->hasHeader("If-None-Match") &&
195
+ request->header("If-None-Match").equals(etag_assets_index_Soe6cpLA_css)) {
196
+ PsychicResponse response304(request);
197
+ response304.setCode(304);
198
+ return response304.send();
149
199
  }
150
200
 
151
201
  PsychicResponse response(request);
152
202
  response.setContentType("text/css");
153
203
  response.addHeader("Content-Encoding", "gzip");
154
- response.setContent(data1, sizeof(data1));
204
+ response.addHeader("Cache-Control", "no-cache");
205
+ response.addHeader("ETag", etag_assets_index_Soe6cpLA_css);
206
+ response.setContent(datagzip_assets_index_Soe6cpLA_css, 5368);
155
207
  return response.send();
156
208
  });
157
209
 
158
- ...
210
+ // ... more routes
159
211
  }
160
212
  ```
161
213
 
162
214
  ### Engines and ESP variants
163
215
 
164
- ESPAsyncWebServer (current location https://github.com/ESP32Async/ESPAsyncWebServer) is a popular web server. When you want to generate a file for this, use the `-e async` switch.
216
+ Four web server engines are supported:
165
217
 
166
- If you **only work on ESP32**, I recommend using PsychicHttpServer, which uses the native mode ESP-IDF web server inside. This way, its operation is significantly faster and more continuous. You can access this mode with the `-e psychic` switch.
218
+ - **`-e psychic`** (default): PsychicHttpServer V1 - Fast ESP32-only server using ESP-IDF internally. Recommended for ESP32 projects.
219
+ - **`-e psychic2`**: PsychicHttpServer V2 - Updated version with improved API
220
+ - **`-e async`**: ESPAsyncWebServer (https://github.com/ESP32Async/ESPAsyncWebServer) - Popular async server supporting both ESP32 and ESP8266/ESP8285
221
+ - **`-e espidf`**: Native ESP-IDF web server - For projects using ESP-IDF framework directly (not Arduino)
222
+
223
+ If you **only work on ESP32**, we recommend using PsychicHttpServer (`-e psychic` or `-e psychic2`), which uses the native ESP-IDF web server internally for significantly faster and more stable operation.
224
+
225
+ **Note for PsychicHttpServer users:** The generated header includes a comment suggesting to configure `server.config.max_uri_handlers` to match or exceed your file count to ensure all routes are properly registered.
167
226
 
168
227
  ### Gzip
169
228
 
@@ -183,6 +242,8 @@ Since microcontroller data traffic is moderately expensive, it is an individual
183
242
 
184
243
  The use of ETag is **not enabled by default**, this can be achieved with the `--etag=true` switch.
185
244
 
245
+ All four engines (psychic, psychic2, async, espidf) fully support ETag validation with HTTP 304 Not Modified responses, reducing bandwidth usage when clients have valid cached content.
246
+
186
247
  > This setting has three states: yes, no, and compiler mode is available. In compiler mode, you can disable/enable ETag by setting the `SVELTEESP32_ENABLE_ETAG` c++ compiler directive. For example, if using platformio, just type `-D SVELTEESP32_ENABLE_ETAG`.
187
248
 
188
249
  ### Cache-control
@@ -239,19 +300,19 @@ You can use the following c++ directives at the project level if you want to con
239
300
 
240
301
  ### Command line options
241
302
 
242
- | Option | Description | default |
243
- | ------------- | ------------------------------------------------------------------------- | ----------------------- |
244
- | `-s` | **Source dist folder contains compiled web files** | |
245
- | `-e` | The engine for which the include file is created (psychic/psychic2/async) | psychic |
246
- | `-o` | Generated output file with path | `svelteesp32.h` |
247
- | `--etag` | Use ETag header for cache (true/false/compiler) | false |
248
- | `--cachetime` | Override no-cache response with a max-age=<cachetime> response | 0 |
249
- | `--gzip` | Compress content with gzip (true/false/compiler) | true |
250
- | `--created` | Include creation time | false |
251
- | `--version` | Include a version string, `--version=v$npm_package_version` | '' |
252
- | `--espmethod` | Name of generated method | `initSvelteStaticFiles` |
253
- | `--define` | Prefix of c++ defines | `SVELTEESP32` |
254
- | `-h` | Show help | |
303
+ | Option | Description | default |
304
+ | ------------- | ------------------------------------------------------------------------------------ | ----------------------- |
305
+ | `-s` | **Source dist folder contains compiled web files** | (required) |
306
+ | `-e` | The engine for which the include file is created (psychic/psychic2/async/espidf) | psychic |
307
+ | `-o` | Generated output file with path | `svelteesp32.h` |
308
+ | `--etag` | Use ETag header for cache (true/false/compiler) | false |
309
+ | `--cachetime` | Override no-cache response with a max-age=\<cachetime\> response (seconds) | 0 |
310
+ | `--gzip` | Compress content with gzip (true/false/compiler) | true |
311
+ | `--created` | Include creation time in generated header | false |
312
+ | `--version` | Include a version string in generated header, e.g. `--version=v$npm_package_version` | '' |
313
+ | `--espmethod` | Name of generated initialization method | `initSvelteStaticFiles` |
314
+ | `--define` | Prefix of c++ defines (e.g., SVELTEESP32_COUNT) | `SVELTEESP32` |
315
+ | `-h` | Show help | |
255
316
 
256
317
  ### Q&A
257
318
 
@@ -259,9 +320,9 @@ You can use the following c++ directives at the project level if you want to con
259
320
 
260
321
  - **How fast is cpp file compilation?** The cpp (.h) file can be large, but it can be compiled in a few seconds on any machine. If you don't modify your svelte/react app, it will use the already compiled cpp file (not recompile). This does not increase the speed of ESP32/ESP8266 development.
261
322
 
262
- - **Does the solution use PROGMEM?** No and yes. ESP32 no longer has PROGMEM. (Exists, but does not affect the translation). Instead, if we use a const array in the global namespace, its content will be placed in the code area, i.e. it will not be used from the heap or the stack, so the content of the files to be served will be placed next to the code. When working on ESP8266, PROGMEM will actually be used.
323
+ - **Does the solution use PROGMEM?** It depends on the engine. For PsychicHttpServer (`psychic`, `psychic2`) and ESP-IDF (`espidf`) engines, const arrays are used which are automatically placed in program memory on ESP32. For ESPAsyncWebServer (`async`), PROGMEM directive is explicitly used to support ESP8266/ESP8285. In both cases, the file data is stored in flash memory, not RAM, so your heap and stack remain available for your application.
263
324
 
264
- - **Why is the .h file so big?** The source files are always larger than the binary compiled from them. Does the size information in the header (SVELTEESP32_SIZE, SVELTEESP32_SIZE_GZIP) show the actual memory allocation.
325
+ - **Why is the .h file so big?** The source files are always larger than the binary compiled from them. The .h file contains byte arrays in text format (comma-separated decimal numbers), which takes more space than the binary data itself. The actual memory allocation is shown in the header file defines (SVELTEESP32_SIZE for uncompressed, SVELTEESP32_SIZE_GZIP for compressed).
265
326
 
266
327
  - **Is collaboration between groups supported?** Yes, the Frontend team produces the application, the use of svelteesp32 is part of the build process. Then, provided with a version number, the .h file is placed in git, which the ESP team translates into the platformio application.
267
328
 
package/dist/cppCode.js CHANGED
@@ -4,15 +4,7 @@ exports.getCppCode = void 0;
4
4
  const handlebars_1 = require("handlebars");
5
5
  const commandLine_1 = require("./commandLine");
6
6
  const cppCodeEspIdf_1 = require("./cppCodeEspIdf");
7
- const psychicTemplate = `
8
- //engine: PsychicHttpServer
9
- //cmdline: {{{commandLine}}}
10
- //You should use server.config.max_uri_handlers = {{fileCount}}; or higher value to proper handles all files
11
- {{#if created }}
12
- //created: {{now}}
13
- {{/if}}
14
- //
15
-
7
+ const commonHeaderSection = `
16
8
  {{#switch etag}}
17
9
  {{#case "true"}}
18
10
  #ifdef {{definePrefix}}_ENABLE_ETAG
@@ -56,38 +48,36 @@ const psychicTemplate = `
56
48
  {{#each filesByExtension}}
57
49
  #define {{../definePrefix}}_{{this.extension}}_FILES {{this.count}}
58
50
  {{/each}}
59
-
60
- //
61
- #include <Arduino.h>
62
- #include <PsychicHttp.h>
63
- #include <PsychicHttpsServer.h>
64
-
65
- //
51
+ `;
52
+ const dataArraysSection = (progmem = false) => {
53
+ const memDirective = progmem ? ' PROGMEM' : '';
54
+ return `
66
55
  {{#switch gzip}}
67
56
  {{#case "true"}}
68
57
  {{#each sources}}
69
- const uint8_t datagzip_{{this.dataname}}[{{this.lengthGzip}}] = { {{this.bytesGzip}} };
58
+ const uint8_t datagzip_{{this.dataname}}[{{this.lengthGzip}}]${memDirective} = { {{this.bytesGzip}} };
70
59
  {{/each}}
71
60
  {{/case}}
72
61
  {{#case "false"}}
73
62
  {{#each sources}}
74
- const uint8_t data_{{this.dataname}}[{{this.length}}] = { {{this.bytes}} };
63
+ const uint8_t data_{{this.dataname}}[{{this.length}}]${memDirective} = { {{this.bytes}} };
75
64
  {{/each}}
76
65
  {{/case}}
77
66
  {{#case "compiler"}}
78
67
  #ifdef {{definePrefix}}_ENABLE_GZIP
79
68
  {{#each sources}}
80
- const uint8_t datagzip_{{this.dataname}}[{{this.lengthGzip}}] = { {{this.bytesGzip}} };
69
+ const uint8_t datagzip_{{this.dataname}}[{{this.lengthGzip}}]${memDirective} = { {{this.bytesGzip}} };
81
70
  {{/each}}
82
71
  #else
83
72
  {{#each sources}}
84
- const uint8_t data_{{this.dataname}}[{{this.length}}] = { {{this.bytes}} };
73
+ const uint8_t data_{{this.dataname}}[{{this.length}}]${memDirective} = { {{this.bytes}} };
85
74
  {{/each}}
86
- #endif
75
+ #endif
87
76
  {{/case}}
88
77
  {{/switch}}
89
-
90
- //
78
+ `;
79
+ };
80
+ const etagArraysSection = `
91
81
  {{#switch etag}}
92
82
  {{#case "true"}}
93
83
  {{#each sources}}
@@ -101,9 +91,30 @@ const char * etag_{{this.dataname}} = "{{this.md5}}";
101
91
  {{#each sources}}
102
92
  const char * etag_{{this.dataname}} = "{{this.md5}}";
103
93
  {{/each}}
104
- #endif
94
+ #endif
105
95
  {{/case}}
106
96
  {{/switch}}
97
+ `;
98
+ const psychicTemplate = `
99
+ //engine: PsychicHttpServer
100
+ //cmdline: {{{commandLine}}}
101
+ //You should use server.config.max_uri_handlers = {{fileCount}}; or higher value to proper handles all files
102
+ {{#if created }}
103
+ //created: {{now}}
104
+ {{/if}}
105
+ //
106
+ ${commonHeaderSection}
107
+
108
+ //
109
+ #include <Arduino.h>
110
+ #include <PsychicHttp.h>
111
+ #include <PsychicHttpsServer.h>
112
+
113
+ //
114
+ ${dataArraysSection(false)}
115
+
116
+ //
117
+ ${etagArraysSection}
107
118
 
108
119
  //
109
120
  // Http Handlers
@@ -115,7 +126,7 @@ void {{methodName}}(PsychicHttpServer * server) {
115
126
 
116
127
  {{#switch ../etag}}
117
128
  {{#case "true"}}
118
- if (request->hasHeader("If-None-Match") && request->header("If-None-Match") == String(etag_{{this.dataname}})) {
129
+ if (request->hasHeader("If-None-Match") && request->header("If-None-Match").equals(etag_{{this.dataname}})) {
119
130
  PsychicResponse response304(request);
120
131
  response304.setCode(304);
121
132
  return response304.send();
@@ -123,12 +134,12 @@ void {{methodName}}(PsychicHttpServer * server) {
123
134
  {{/case}}
124
135
  {{#case "compiler"}}
125
136
  #ifdef {{../definePrefix}}_ENABLE_ETAG
126
- if (request->hasHeader("If-None-Match") && request->header("If-None-Match") == String(etag_{{this.dataname}})) {
137
+ if (request->hasHeader("If-None-Match") && request->header("If-None-Match").equals(etag_{{this.dataname}})) {
127
138
  PsychicResponse response304(request);
128
139
  response304.setCode(304);
129
140
  return response304.send();
130
141
  }
131
- #endif
142
+ #endif
132
143
  {{/case}}
133
144
  {{/switch}}
134
145
 
@@ -201,50 +212,7 @@ const psychic2Template = `
201
212
  //created: {{now}}
202
213
  {{/if}}
203
214
  //
204
-
205
- {{#switch etag}}
206
- {{#case "true"}}
207
- #ifdef {{definePrefix}}_ENABLE_ETAG
208
- #warning {{definePrefix}}_ENABLE_ETAG has no effect because it is permanently switched ON
209
- #endif
210
- {{/case}}
211
- {{#case "false"}}
212
- #ifdef {{definePrefix}}_ENABLE_ETAG
213
- #warning {{definePrefix}}_ENABLE_ETAG has no effect because it is permanently switched OFF
214
- #endif
215
- {{/case}}
216
- {{/switch}}
217
-
218
- {{#switch gzip}}
219
- {{#case "true"}}
220
- #ifdef {{definePrefix}}_ENABLE_GZIP
221
- #warning {{definePrefix}}_ENABLE_GZIP has no effect because it is permanently switched ON
222
- #endif
223
- {{/case}}
224
- {{#case "false"}}
225
- #ifdef {{definePrefix}}_ENABLE_GZIP
226
- #warning {{definePrefix}}_ENABLE_GZIP has no effect because it is permanently switched OFF
227
- #endif
228
- {{/case}}
229
- {{/switch}}
230
-
231
- //
232
- {{#if version }}
233
- #define {{definePrefix}}_VERSION "{{version}}"
234
- {{/if}}
235
- #define {{definePrefix}}_COUNT {{fileCount}}
236
- #define {{definePrefix}}_SIZE {{fileSize}}
237
- #define {{definePrefix}}_SIZE_GZIP {{fileGzipSize}}
238
-
239
- //
240
- {{#each sources}}
241
- #define {{../definePrefix}}_FILE_{{this.datanameUpperCase}}
242
- {{/each}}
243
-
244
- //
245
- {{#each filesByExtension}}
246
- #define {{../definePrefix}}_{{this.extension}}_FILES {{this.count}}
247
- {{/each}}
215
+ ${commonHeaderSection}
248
216
 
249
217
  //
250
218
  #include <Arduino.h>
@@ -252,47 +220,10 @@ const psychic2Template = `
252
220
  #include <PsychicHttpsServer.h>
253
221
 
254
222
  //
255
- {{#switch gzip}}
256
- {{#case "true"}}
257
- {{#each sources}}
258
- const uint8_t datagzip_{{this.dataname}}[{{this.lengthGzip}}] = { {{this.bytesGzip}} };
259
- {{/each}}
260
- {{/case}}
261
- {{#case "false"}}
262
- {{#each sources}}
263
- const uint8_t data_{{this.dataname}}[{{this.length}}] = { {{this.bytes}} };
264
- {{/each}}
265
- {{/case}}
266
- {{#case "compiler"}}
267
- #ifdef {{definePrefix}}_ENABLE_GZIP
268
- {{#each sources}}
269
- const uint8_t datagzip_{{this.dataname}}[{{this.lengthGzip}}] = { {{this.bytesGzip}} };
270
- {{/each}}
271
- #else
272
- {{#each sources}}
273
- const uint8_t data_{{this.dataname}}[{{this.length}}] = { {{this.bytes}} };
274
- {{/each}}
275
- #endif
276
- {{/case}}
277
- {{/switch}}
223
+ ${dataArraysSection(false)}
278
224
 
279
225
  //
280
- {{#switch etag}}
281
- {{#case "true"}}
282
- {{#each sources}}
283
- const char * etag_{{this.dataname}} = "{{this.md5}}";
284
- {{/each}}
285
- {{/case}}
286
- {{#case "false"}}
287
- {{/case}}
288
- {{#case "compiler"}}
289
- #ifdef {{definePrefix}}_ENABLE_ETAG
290
- {{#each sources}}
291
- const char * etag_{{this.dataname}} = "{{this.md5}}";
292
- {{/each}}
293
- #endif
294
- {{/case}}
295
- {{/switch}}
226
+ ${etagArraysSection}
296
227
 
297
228
  //
298
229
  // Http Handlers
@@ -304,18 +235,18 @@ void {{methodName}}(PsychicHttpServer * server) {
304
235
 
305
236
  {{#switch ../etag}}
306
237
  {{#case "true"}}
307
- if (request->hasHeader("If-None-Match") && request->header("If-None-Match") == String(etag_{{this.dataname}})) {
238
+ if (request->hasHeader("If-None-Match") && request->header("If-None-Match").equals(etag_{{this.dataname}})) {
308
239
  response->setCode(304);
309
240
  return response->send();
310
241
  }
311
242
  {{/case}}
312
243
  {{#case "compiler"}}
313
244
  #ifdef {{../definePrefix}}_ENABLE_ETAG
314
- if (request->hasHeader("If-None-Match") && request->header("If-None-Match") == String(etag_{{this.dataname}})) {
245
+ if (request->hasHeader("If-None-Match") && request->header("If-None-Match").equals(etag_{{this.dataname}})) {
315
246
  response->setCode(304);
316
247
  return response->send();
317
248
  }
318
- #endif
249
+ #endif
319
250
  {{/case}}
320
251
  {{/switch}}
321
252
 
@@ -387,120 +318,111 @@ const asyncTemplate = `
387
318
  //created: {{now}}
388
319
  {{/if}}
389
320
  //
321
+ ${commonHeaderSection}
390
322
 
391
- {{#switch etag}}
392
- {{#case "true"}}
393
- #ifdef {{definePrefix}}_ENABLE_ETAG
394
- #warning {{definePrefix}}_ENABLE_ETAG has no effect because it is permanently switched ON
395
- #endif
396
- {{/case}}
397
- {{#case "false"}}
398
- #ifdef {{definePrefix}}_ENABLE_ETAG
399
- #warning {{definePrefix}}_ENABLE_ETAG has no effect because it is permanently switched OFF
400
- #endif
401
- {{/case}}
402
- {{/switch}}
323
+ //
324
+ #include <Arduino.h>
325
+ #include <ESPAsyncWebServer.h>
403
326
 
404
- {{#switch gzip}}
405
- {{#case "true"}}
406
- #ifdef {{definePrefix}}_ENABLE_GZIP
407
- #warning {{definePrefix}}_ENABLE_GZIP has no effect because it is permanently switched ON
408
- #endif
409
- {{/case}}
410
- {{#case "false"}}
411
- #ifdef {{definePrefix}}_ENABLE_GZIP
412
- #warning {{definePrefix}}_ENABLE_GZIP has no effect because it is permanently switched OFF
413
- #endif
414
- {{/case}}
415
- {{/switch}}
327
+ //
328
+ ${dataArraysSection(true)}
416
329
 
417
330
  //
418
- {{#if version }}
419
- #define {{definePrefix}}_VERSION "{{version}}"
420
- {{/if}}
421
- #define {{definePrefix}}_COUNT {{fileCount}}
422
- #define {{definePrefix}}_SIZE {{fileSize}}
423
- #define {{definePrefix}}_SIZE_GZIP {{fileGzipSize}}
331
+ ${etagArraysSection}
424
332
 
425
333
  //
334
+ // Http Handlers
335
+ void {{methodName}}(AsyncWebServer * server) {
426
336
  {{#each sources}}
427
- #define {{../definePrefix}}_FILE_{{this.datanameUpperCase}}
428
- {{/each}}
429
-
430
337
  //
431
- {{#each filesByExtension}}
432
- #define {{../definePrefix}}_{{this.extension}}_FILES {{this.count}}
433
- {{/each}}
338
+ // {{this.filename}}
339
+ server->on("/{{this.filename}}", HTTP_GET, [](AsyncWebServerRequest * request) {
434
340
 
435
- //
436
- #include <Arduino.h>
437
- #include <ESPAsyncWebServer.h>
341
+ {{#switch ../etag}}
342
+ {{#case "true"}}
343
+ const AsyncWebHeader* h = request->getHeader("If-None-Match");
344
+ if (h && h->value().equals(etag_{{this.dataname}})) {
345
+ request->send(304);
346
+ return;
347
+ }
348
+ {{/case}}
349
+ {{#case "compiler"}}
350
+ #ifdef {{../definePrefix}}_ENABLE_ETAG
351
+ const AsyncWebHeader* h = request->getHeader("If-None-Match");
352
+ if (h && h->value().equals(etag_{{this.dataname}})) {
353
+ request->send(304);
354
+ return;
355
+ }
356
+ #endif
357
+ {{/case}}
358
+ {{/switch}}
438
359
 
439
- //
440
- {{#switch gzip}}
360
+ {{#switch ../gzip}}
441
361
  {{#case "true"}}
442
- {{#each sources}}
443
- const uint8_t datagzip_{{this.dataname}}[{{this.lengthGzip}}] PROGMEM = { {{this.bytesGzip}} };
444
- {{/each}}
362
+ AsyncWebServerResponse *response = request->beginResponse(200, "{{this.mime}}", datagzip_{{this.dataname}}, {{this.lengthGzip}});
363
+ {{#if this.isGzip}}
364
+ response->addHeader("Content-Encoding", "gzip");
365
+ {{/if}}
445
366
  {{/case}}
446
367
  {{#case "false"}}
447
- {{#each sources}}
448
- const uint8_t data_{{this.dataname}}[{{this.length}}] PROGMEM = { {{this.bytes}} };
449
- {{/each}}
368
+ AsyncWebServerResponse *response = request->beginResponse(200, "{{this.mime}}", data_{{this.dataname}}, {{this.length}});
450
369
  {{/case}}
451
370
  {{#case "compiler"}}
452
- #ifdef {{definePrefix}}_ENABLE_GZIP
453
- {{#each sources}}
454
- const uint8_t datagzip_{{this.dataname}}[{{this.lengthGzip}}] PROGMEM = { {{this.bytesGzip}} };
455
- {{/each}}
456
- #else
457
- {{#each sources}}
458
- const uint8_t data_{{this.dataname}}[{{this.length}}] PROGMEM = { {{this.bytes}} };
459
- {{/each}}
460
- #endif
371
+ #ifdef {{../definePrefix}}_ENABLE_GZIP
372
+ AsyncWebServerResponse *response = request->beginResponse(200, "{{this.mime}}", datagzip_{{this.dataname}}, {{this.lengthGzip}});
373
+ {{#if this.isGzip}}
374
+ response->addHeader("Content-Encoding", "gzip");
375
+ {{/if}}
376
+ #else
377
+ AsyncWebServerResponse *response = request->beginResponse(200, "{{this.mime}}", data_{{this.dataname}}, {{this.length}});
378
+ #endif
461
379
  {{/case}}
462
380
  {{/switch}}
463
381
 
464
- //
465
- {{#switch etag}}
382
+ {{#switch ../etag}}
466
383
  {{#case "true"}}
467
- {{#each sources}}
468
- const char * etag_{{this.dataname}} = "{{this.md5}}";
469
- {{/each}}
470
- {{/case}}
471
- {{#case "false"}}
384
+ {{#../cacheTime}}
385
+ response->addHeader("Cache-Control", "max-age={{value}}");
386
+ {{/../cacheTime}}
387
+ {{^../cacheTime}}
388
+ response->addHeader("Cache-Control", "no-cache");
389
+ {{/../cacheTime}}
390
+ response->addHeader("ETag", etag_{{this.dataname}});
472
391
  {{/case}}
473
392
  {{#case "compiler"}}
474
- #ifdef {{definePrefix}}_ENABLE_ETAG
475
- {{#each sources}}
476
- const char * etag_{{this.dataname}} = "{{this.md5}}";
477
- {{/each}}
478
- #endif
393
+ #ifdef {{../definePrefix}}_ENABLE_ETAG
394
+ {{#../cacheTime}}
395
+ response->addHeader("Cache-Control", "max-age={{value}}");
396
+ {{/../cacheTime}}
397
+ {{^../cacheTime}}
398
+ response->addHeader("Cache-Control", "no-cache");
399
+ {{/../cacheTime}}
400
+ response->addHeader("ETag", etag_{{this.dataname}});
401
+ #endif
479
402
  {{/case}}
480
403
  {{/switch}}
481
404
 
482
- //
483
- // Http Handlers
484
- void {{methodName}}(AsyncWebServer * server) {
485
- {{#each sources}}
486
- //
487
- // {{this.filename}}
488
- ArRequestHandlerFunction func_{{this.dataname}} = [](AsyncWebServerRequest * request) {
405
+ request->send(response);
406
+ });
407
+ {{#if this.isDefault}}
408
+ server->on("/", HTTP_GET, [](AsyncWebServerRequest * request) {
489
409
 
490
410
  {{#switch ../etag}}
491
411
  {{#case "true"}}
492
- if (request->hasHeader("If-None-Match") && request->getHeader("If-None-Match")->value() == String(etag_{{this.dataname}})) {
412
+ const AsyncWebHeader* h = request->getHeader("If-None-Match");
413
+ if (h && h->value().equals(etag_{{this.dataname}})) {
493
414
  request->send(304);
494
415
  return;
495
416
  }
496
417
  {{/case}}
497
418
  {{#case "compiler"}}
498
419
  #ifdef {{../definePrefix}}_ENABLE_ETAG
499
- if (request->hasHeader("If-None-Match") && request->getHeader("If-None-Match")->value() == String(etag_{{this.dataname}})) {
420
+ const AsyncWebHeader* h = request->getHeader("If-None-Match");
421
+ if (h && h->value().equals(etag_{{this.dataname}})) {
500
422
  request->send(304);
501
423
  return;
502
424
  }
503
- #endif
425
+ #endif
504
426
  {{/case}}
505
427
  {{/switch}}
506
428
 
@@ -522,7 +444,7 @@ void {{methodName}}(AsyncWebServer * server) {
522
444
  {{/if}}
523
445
  #else
524
446
  AsyncWebServerResponse *response = request->beginResponse(200, "{{this.mime}}", data_{{this.dataname}}, {{this.length}});
525
- #endif
447
+ #endif
526
448
  {{/case}}
527
449
  {{/switch}}
528
450
 
@@ -545,15 +467,12 @@ void {{methodName}}(AsyncWebServer * server) {
545
467
  response->addHeader("Cache-Control", "no-cache");
546
468
  {{/../cacheTime}}
547
469
  response->addHeader("ETag", etag_{{this.dataname}});
548
- #endif
470
+ #endif
549
471
  {{/case}}
550
472
  {{/switch}}
551
473
 
552
474
  request->send(response);
553
- };
554
- server->on("/{{this.filename}}", HTTP_GET, func_{{this.dataname}});
555
- {{#if this.isDefault}}
556
- server->on("/", HTTP_GET, func_{{this.dataname}});
475
+ });
557
476
  {{/if}}
558
477
 
559
478
  {{/each}}
@@ -570,33 +489,26 @@ const getTemplate = (engine) => {
570
489
  return asyncTemplate;
571
490
  }
572
491
  };
573
- let switchValue;
574
- const getCppCode = (sources, filesByExtension) => (0, handlebars_1.compile)(getTemplate(commandLine_1.cmdLine.engine))({
575
- commandLine: process.argv.slice(2).join(' '),
576
- now: `${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()}`,
577
- fileCount: sources.length.toString(),
578
- fileSize: sources.reduce((previous, current) => previous + current.content.length, 0).toString(),
579
- fileGzipSize: sources.reduce((previous, current) => previous + current.contentGzip.length, 0).toString(),
580
- sources: sources.map((s) => ({
581
- ...s,
582
- length: s.content.length,
583
- bytes: [...s.content].map((v) => `${v.toString(10)}`).join(','),
584
- lengthGzip: s.contentGzip.length,
585
- bytesGzip: [...s.contentGzip].map((v) => `${v.toString(10)}`).join(','),
586
- isDefault: s.filename.startsWith('index.htm')
587
- })),
588
- filesByExtension,
589
- etag: commandLine_1.cmdLine.etag,
590
- gzip: commandLine_1.cmdLine.gzip,
591
- created: commandLine_1.cmdLine.created,
592
- version: commandLine_1.cmdLine.version,
593
- methodName: commandLine_1.cmdLine.espmethod,
594
- cacheTime: commandLine_1.cmdLine.cachetime ? { value: commandLine_1.cmdLine.cachetime } : undefined,
595
- definePrefix: commandLine_1.cmdLine.define
596
- }, {
597
- helpers: {
492
+ const transformSourceToTemplateData = (s) => ({
493
+ ...s,
494
+ length: s.content.length,
495
+ bytes: [...s.content].map((v) => `${v.toString(10)}`).join(','),
496
+ lengthGzip: s.contentGzip.length,
497
+ bytesGzip: [...s.contentGzip].map((v) => `${v.toString(10)}`).join(','),
498
+ isDefault: s.filename.startsWith('index.htm')
499
+ });
500
+ const postProcessCppCode = (code) => code
501
+ .split('\n')
502
+ .map((line) => line.trimEnd())
503
+ .filter(Boolean)
504
+ .map((line) => (line === '//' ? '' : line))
505
+ .join('\n')
506
+ .replace(/\\n{2}/, '\n');
507
+ const createHandlebarsHelpers = () => {
508
+ let switchValue;
509
+ return {
598
510
  ifeq: function (a, b, options) {
599
- if (a == b)
511
+ if (a === b)
600
512
  return options.fn(this);
601
513
  return options.inverse(this);
602
514
  },
@@ -605,16 +517,31 @@ const getCppCode = (sources, filesByExtension) => (0, handlebars_1.compile)(getT
605
517
  return options.fn(this);
606
518
  },
607
519
  case: function (value, options) {
608
- if (value == switchValue)
520
+ if (value === switchValue)
609
521
  return options.fn(this);
610
522
  return options.inverse(this);
611
523
  }
612
- }
613
- })
614
- .split('\n')
615
- .map((line) => line.trimEnd())
616
- .filter(Boolean)
617
- .map((line) => (line == '//' ? '' : line))
618
- .join('\n')
619
- .replace(/\\n{2}/, '\n');
524
+ };
525
+ };
526
+ const getCppCode = (sources, filesByExtension) => {
527
+ const template = (0, handlebars_1.compile)(getTemplate(commandLine_1.cmdLine.engine));
528
+ const templateData = {
529
+ commandLine: process.argv.slice(2).join(' '),
530
+ now: `${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()}`,
531
+ fileCount: sources.length.toString(),
532
+ fileSize: sources.reduce((previous, current) => previous + current.content.length, 0).toString(),
533
+ fileGzipSize: sources.reduce((previous, current) => previous + current.contentGzip.length, 0).toString(),
534
+ sources: sources.map((s) => transformSourceToTemplateData(s)),
535
+ filesByExtension,
536
+ etag: commandLine_1.cmdLine.etag,
537
+ gzip: commandLine_1.cmdLine.gzip,
538
+ created: commandLine_1.cmdLine.created,
539
+ version: commandLine_1.cmdLine.version,
540
+ methodName: commandLine_1.cmdLine.espmethod,
541
+ cacheTime: commandLine_1.cmdLine.cachetime ? { value: commandLine_1.cmdLine.cachetime } : undefined,
542
+ definePrefix: commandLine_1.cmdLine.define
543
+ };
544
+ const rawCode = template(templateData, { helpers: createHandlebarsHelpers() });
545
+ return postProcessCppCode(rawCode);
546
+ };
620
547
  exports.getCppCode = getCppCode;
@@ -1 +1 @@
1
- export declare const espidfTemplate = "\n//engine: espidf\n//cmdline: {{{commandLine}}}\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 <esp_err.h>\n#include <esp_http_server.h>\n\n//\n{{#switch gzip}}\n{{#case \"true\"}}\n {{#each sources}}\nconst char datagzip_{{this.dataname}}[{{this.lengthGzip}}] = { {{this.bytesGzip}} };\n {{/each}}\n{{/case}}\n{{#case \"false\"}}\n {{#each sources}}\nconst char data_{{this.dataname}}[{{this.length}}] = { {{this.bytes}} };\n {{/each}}\n{{/case}}\n{{#case \"compiler\"}}\n#ifdef {{definePrefix}}_ENABLE_GZIP\n {{#each sources}}\nconst char datagzip_{{this.dataname}}[{{this.lengthGzip}}] = { {{this.bytesGzip}} };\n {{/each}}\n#else\n {{#each sources}}\nconst 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}}\nconst char * etag_{{this.dataname}} = \"{{this.md5}}\";\n {{/each}}\n{{/case}}\n{{#case \"false\"}}\n{{/case}}\n{{#case \"compiler\"}}\n#ifdef {{definePrefix}}_ENABLE_ETAG\n {{#each sources}}\nconst char * etag_{{this.dataname}} = \"{{this.md5}}\";\n {{/each}}\n#endif \n{{/case}}\n{{/switch}}\n\n{{#each sources}}\n\nstatic esp_err_t file_handler_{{this.datanameUpperCase}} (httpd_req_t *req)\n{\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 httpd_resp_send(req, datagzip_{{this.dataname}}, {{this.lengthGzip}});\n{{/case}}\n{{#case \"false\"}}\n httpd_resp_send(req, data_{{this.dataname}}, {{this.length}});\n{{/case}}\n{{#case \"compiler\"}}\n #ifdef {{../definePrefix}}_ENABLE_GZIP\n httpd_resp_send(req, datagzip_{{this.dataname}}, {{this.lengthGzip}});\n #else\n httpd_resp_send(req, 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 = \"/\",\n .method = HTTP_GET,\n .handler = file_handler_{{this.datanameUpperCase}},\n};\n{{/if}}\n\nstatic const httpd_uri_t route_{{this.datanameUpperCase}} = {\n .uri = \"/{{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//cmdline: {{{commandLine}}}\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 char datagzip_{{this.dataname}}[{{this.lengthGzip}}] = { {{this.bytesGzip}} };\n {{/each}}\n{{/case}}\n{{#case \"false\"}}\n {{#each sources}}\nconst char data_{{this.dataname}}[{{this.length}}] = { {{this.bytes}} };\n {{/each}}\n{{/case}}\n{{#case \"compiler\"}}\n#ifdef {{definePrefix}}_ENABLE_GZIP\n {{#each sources}}\nconst char datagzip_{{this.dataname}}[{{this.lengthGzip}}] = { {{this.bytesGzip}} };\n {{/each}}\n#else\n {{#each sources}}\nconst 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}}\nconst char * etag_{{this.dataname}} = \"{{this.md5}}\";\n {{/each}}\n{{/case}}\n{{#case \"false\"}}\n{{/case}}\n{{#case \"compiler\"}}\n#ifdef {{definePrefix}}_ENABLE_ETAG\n {{#each sources}}\nconst char * etag_{{this.dataname}} = \"{{this.md5}}\";\n {{/each}}\n#endif \n{{/case}}\n{{/switch}}\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 (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 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 (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 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 httpd_resp_send(req, datagzip_{{this.dataname}}, {{this.lengthGzip}});\n{{/case}}\n{{#case \"false\"}}\n httpd_resp_send(req, data_{{this.dataname}}, {{this.length}});\n{{/case}}\n{{#case \"compiler\"}}\n #ifdef {{../definePrefix}}_ENABLE_GZIP\n httpd_resp_send(req, datagzip_{{this.dataname}}, {{this.lengthGzip}});\n #else\n httpd_resp_send(req, 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 = \"/\",\n .method = HTTP_GET,\n .handler = file_handler_{{this.datanameUpperCase}},\n};\n{{/if}}\n\nstatic const httpd_uri_t route_{{this.datanameUpperCase}} = {\n .uri = \"/{{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}";
@@ -54,6 +54,8 @@ exports.espidfTemplate = `
54
54
  {{/each}}
55
55
 
56
56
  #include <stdint.h>
57
+ #include <string.h>
58
+ #include <stdlib.h>
57
59
  #include <esp_err.h>
58
60
  #include <esp_http_server.h>
59
61
 
@@ -104,6 +106,40 @@ const char * etag_{{this.dataname}} = "{{this.md5}}";
104
106
 
105
107
  static esp_err_t file_handler_{{this.datanameUpperCase}} (httpd_req_t *req)
106
108
  {
109
+ {{#switch ../etag}}
110
+ {{#case "true"}}
111
+ size_t hdr_len = httpd_req_get_hdr_value_len(req, "If-None-Match");
112
+ if (hdr_len > 0) {
113
+ char* hdr_value = malloc(hdr_len + 1);
114
+ if (httpd_req_get_hdr_value_str(req, "If-None-Match", hdr_value, hdr_len + 1) == ESP_OK) {
115
+ if (strcmp(hdr_value, etag_{{this.dataname}}) == 0) {
116
+ free(hdr_value);
117
+ httpd_resp_set_status(req, "304 Not Modified");
118
+ httpd_resp_send(req, NULL, 0);
119
+ return ESP_OK;
120
+ }
121
+ }
122
+ free(hdr_value);
123
+ }
124
+ {{/case}}
125
+ {{#case "compiler"}}
126
+ #ifdef {{../definePrefix}}_ENABLE_ETAG
127
+ size_t hdr_len = httpd_req_get_hdr_value_len(req, "If-None-Match");
128
+ if (hdr_len > 0) {
129
+ char* hdr_value = malloc(hdr_len + 1);
130
+ if (httpd_req_get_hdr_value_str(req, "If-None-Match", hdr_value, hdr_len + 1) == ESP_OK) {
131
+ if (strcmp(hdr_value, etag_{{this.dataname}}) == 0) {
132
+ free(hdr_value);
133
+ httpd_resp_set_status(req, "304 Not Modified");
134
+ httpd_resp_send(req, NULL, 0);
135
+ return ESP_OK;
136
+ }
137
+ }
138
+ free(hdr_value);
139
+ }
140
+ #endif
141
+ {{/case}}
142
+ {{/switch}}
107
143
  httpd_resp_set_type(req, "{{this.mime}}");
108
144
  {{#switch ../gzip}}
109
145
  {{#case "true"}}
package/dist/file.js CHANGED
@@ -14,35 +14,46 @@ const findSimilarFiles = (files) => {
14
14
  const contentComparer = new Map();
15
15
  for (const [filename, content] of files.entries()) {
16
16
  const hash = (0, node_crypto_1.createHash)('sha256').update(content).digest('hex');
17
- if (contentComparer.has(hash))
18
- contentComparer.get(hash).push(filename);
19
- else
17
+ const existingFiles = contentComparer.get(hash);
18
+ if (existingFiles) {
19
+ existingFiles.push(filename);
20
+ }
21
+ else {
20
22
  contentComparer.set(hash, [filename]);
23
+ }
21
24
  }
22
25
  const result = [];
23
- for (const filenames of contentComparer.values())
24
- if (filenames.length > 1)
26
+ for (const filenames of contentComparer.values()) {
27
+ if (filenames.length > 1) {
25
28
  result.push(filenames);
29
+ }
30
+ }
26
31
  return result;
27
32
  };
28
- const getFiles = () => {
29
- let filenames = (0, glob_1.globSync)('**/*', { cwd: commandLine_1.cmdLine.sourcepath, nodir: true });
30
- filenames = filenames.filter((filename) => {
31
- const extension = node_path_1.default.extname(filename);
32
- if (['.gz', '.brottli', '.br'].includes(extension)) {
33
- const original = filename.slice(0, -1 * extension.length);
34
- if (filenames.includes(original)) {
35
- console.log((0, consoleColor_1.redLog)(` ${filename} skipped because is perhaps a compressed version of ${original}`));
36
- return false;
37
- }
33
+ const shouldSkipFile = (filename, allFilenames) => {
34
+ const extension = node_path_1.default.extname(filename);
35
+ const compressedExtensions = ['.gz', '.brottli', '.br'];
36
+ if (compressedExtensions.includes(extension)) {
37
+ const original = filename.slice(0, -1 * extension.length);
38
+ if (allFilenames.includes(original)) {
39
+ console.log((0, consoleColor_1.redLog)(` ${filename} skipped because is perhaps a compressed version of ${original}`));
40
+ return true;
38
41
  }
39
- return true;
40
- });
42
+ }
43
+ return false;
44
+ };
45
+ const getFiles = () => {
46
+ const allFilenames = (0, glob_1.globSync)('**/*', { cwd: commandLine_1.cmdLine.sourcepath, nodir: true });
47
+ const filenames = allFilenames.filter((filename) => !shouldSkipFile(filename, allFilenames));
41
48
  const result = new Map();
42
- for (const filename of filenames)
43
- result.set(filename, (0, node_fs_1.readFileSync)(node_path_1.default.join(commandLine_1.cmdLine.sourcepath, filename), { flag: 'r' }));
44
- for (const sameFile of findSimilarFiles(result))
45
- console.log((0, consoleColor_1.yellowLog)(` ${sameFile.join(', ')} files look like identical`));
49
+ for (const filename of filenames) {
50
+ const filePath = node_path_1.default.join(commandLine_1.cmdLine.sourcepath, filename);
51
+ result.set(filename, (0, node_fs_1.readFileSync)(filePath, { flag: 'r' }));
52
+ }
53
+ const duplicates = findSimilarFiles(result);
54
+ for (const sameFiles of duplicates) {
55
+ console.log((0, consoleColor_1.yellowLog)(` ${sameFiles.join(', ')} files look like identical`));
56
+ }
46
57
  return result;
47
58
  };
48
59
  exports.getFiles = getFiles;
package/dist/index.js CHANGED
@@ -12,6 +12,8 @@ const commandLine_1 = require("./commandLine");
12
12
  const consoleColor_1 = require("./consoleColor");
13
13
  const cppCode_1 = require("./cppCode");
14
14
  const file_1 = require("./file");
15
+ const GZIP_MIN_SIZE = 1024;
16
+ const GZIP_MIN_REDUCTION_RATIO = 0.85;
15
17
  const summary = {
16
18
  filecount: 0,
17
19
  size: 0,
@@ -19,6 +21,36 @@ const summary = {
19
21
  };
20
22
  const sources = [];
21
23
  const filesByExtension = [];
24
+ const shouldUseGzip = (originalSize, compressedSize) => originalSize > GZIP_MIN_SIZE && compressedSize < originalSize * GZIP_MIN_REDUCTION_RATIO;
25
+ const calculateCompressionRatio = (originalSize, compressedSize) => Math.round((compressedSize / originalSize) * 100);
26
+ const formatCompressionLog = (filename, padding, originalSize, compressedSize, useGzip) => {
27
+ const ratio = calculateCompressionRatio(originalSize, compressedSize);
28
+ const sizeInfo = `(${originalSize} -> ${compressedSize} = ${ratio}%)`;
29
+ if (useGzip) {
30
+ return (0, consoleColor_1.greenLog)(` [${filename}] ${padding} ✓ gzip used ${sizeInfo}`);
31
+ }
32
+ const tooSmall = originalSize <= GZIP_MIN_SIZE ? '(too small) ' : '';
33
+ return (0, consoleColor_1.yellowLog)(` [${filename}] ${padding} x gzip unused ${tooSmall}${sizeInfo}`);
34
+ };
35
+ const createSourceEntry = (filename, dataname, content, contentGzip, mimeType, md5, isGzip) => ({
36
+ filename,
37
+ dataname,
38
+ datanameUpperCase: dataname.toUpperCase(),
39
+ content,
40
+ contentGzip: isGzip ? contentGzip : content,
41
+ isGzip,
42
+ mime: mimeType,
43
+ md5
44
+ });
45
+ const updateExtensionGroup = (extension) => {
46
+ const group = filesByExtension.find((fe) => fe.extension === extension);
47
+ if (group) {
48
+ group.count += 1;
49
+ }
50
+ else {
51
+ filesByExtension.push({ extension, count: 1 });
52
+ }
53
+ };
22
54
  console.log('Collecting source files');
23
55
  const files = (0, file_1.getFiles)();
24
56
  if (files.size === 0) {
@@ -36,42 +68,15 @@ for (const [originalFilename, content] of files) {
36
68
  let extension = node_path_1.default.extname(filename).toUpperCase();
37
69
  if (extension.startsWith('.'))
38
70
  extension = extension.slice(1);
39
- const group = filesByExtension.find((fe) => fe.extension === extension);
40
- if (group)
41
- group.count += 1;
42
- else
43
- filesByExtension.push({ extension, count: 1 });
71
+ updateExtensionGroup(extension);
44
72
  const md5 = (0, node_crypto_1.createHash)('md5').update(content).digest('hex');
45
73
  summary.size += content.length;
46
74
  const zipContent = (0, node_zlib_1.gzipSync)(content, { level: 9 });
47
75
  summary.gzipsize += zipContent.length;
48
- const zipRatio = Math.round((zipContent.length / content.length) * 100);
49
- if (content.length > 1024 && zipContent.length < content.length * 0.85) {
50
- sources.push({
51
- filename,
52
- dataname,
53
- datanameUpperCase: dataname.toUpperCase(),
54
- content,
55
- contentGzip: zipContent,
56
- isGzip: true,
57
- mime: mimeType,
58
- md5
59
- });
60
- console.log((0, consoleColor_1.greenLog)(` [${originalFilename}] ${' '.repeat(longestFilename - originalFilename.length)} ✓ gzip used (${content.length} -> ${zipContent.length} = ${zipRatio}%)`));
61
- }
62
- else {
63
- sources.push({
64
- filename,
65
- dataname,
66
- datanameUpperCase: dataname.toUpperCase(),
67
- content,
68
- contentGzip: content,
69
- isGzip: false,
70
- mime: mimeType,
71
- md5
72
- });
73
- console.log((0, consoleColor_1.yellowLog)(` [${originalFilename}] ${' '.repeat(longestFilename - originalFilename.length)} x gzip unused ${content.length <= 1024 ? `(too small) ` : ''}(${content.length} -> ${zipContent.length} = ${zipRatio}%)`));
74
- }
76
+ const useGzip = shouldUseGzip(content.length, zipContent.length);
77
+ sources.push(createSourceEntry(filename, dataname, content, zipContent, mimeType, md5, useGzip));
78
+ const padding = ' '.repeat(longestFilename - originalFilename.length);
79
+ console.log(formatCompressionLog(originalFilename, padding, content.length, zipContent.length, useGzip));
75
80
  }
76
81
  console.log('');
77
82
  filesByExtension.sort((left, right) => left.extension.localeCompare(right.extension));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelteesp32",
3
- "version": "1.9.1",
3
+ "version": "1.9.3",
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",
@@ -31,14 +31,17 @@
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
33
  "dev:psychic2": "nodemon src/index.ts -- -e psychic2 -s ./demo/svelte/dist -o ./demo/esp32/include/svelteesp32.h --etag=false --gzip=false --version=v$npm_package_version",
34
- "test:all": "./package.script && ~/.platformio/penv/bin/pio run -d ./demo/esp32 && ~/.platformio/penv/bin/pio run -d ./demo/esp32idf",
34
+ "test:esp32": "./package.script && ~/.platformio/penv/bin/pio run -d ./demo/esp32",
35
+ "test:esp32idf": "./package.script && ~/.platformio/penv/bin/pio run -d ./demo/esp32idf",
36
+ "test:all": "node --run test:esp32 && node --run test:esp32idf",
35
37
  "clean": "tsc --build --clean",
36
38
  "build": "tsc --build --clean && tsc --build --force",
37
39
  "format:check": "prettier --check .",
38
- "format:fix": "prettier --write .",
40
+ "format:fix": "prettier --write . | grep -v 'unchanged' | sed G",
39
41
  "lint:check": "eslint .",
40
42
  "lint:fix": "eslint --fix .",
41
- "fix": "npm run format:fix && npm run lint:fix && npm run format:fix",
43
+ "fix": "node --run format:fix && node --run lint:fix && node --run format:fix",
44
+ "all": "node --run fix && node --run build",
42
45
  "npm:reinstall": "rm -rf ./node_modules && rm -f ./package-lock.json && npm i && npm i"
43
46
  },
44
47
  "keywords": [
@@ -55,18 +58,18 @@
55
58
  ],
56
59
  "devDependencies": {
57
60
  "@types/mime-types": "^3.0.1",
58
- "@types/node": "^24.3.3",
59
- "@typescript-eslint/eslint-plugin": "^8.43.0",
60
- "@typescript-eslint/parser": "^8.43.0",
61
- "eslint": "^9.35.0",
61
+ "@types/node": "^24.9.2",
62
+ "@typescript-eslint/eslint-plugin": "^8.46.2",
63
+ "@typescript-eslint/parser": "^8.46.2",
64
+ "eslint": "^9.39.0",
62
65
  "eslint-config-prettier": "^10.1.8",
63
66
  "eslint-plugin-simple-import-sort": "^12.1.1",
64
- "eslint-plugin-unicorn": "^61.0.2",
67
+ "eslint-plugin-unicorn": "^62.0.0",
65
68
  "nodemon": "^3.1.10",
66
69
  "prettier": "^3.6.2",
67
70
  "ts-node": "^10.9.2",
68
- "tsx": "^4.20.5",
69
- "typescript": "^5.9.2"
71
+ "tsx": "^4.20.6",
72
+ "typescript": "^5.9.3"
70
73
  },
71
74
  "dependencies": {
72
75
  "glob": "^11.0.3",