svelteesp32 1.3.1 → 1.4.1

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
@@ -1,5 +1,7 @@
1
1
  # `svelteesp32` ![image](https://badges.github.io/stability-badges/dist/stable.svg)
2
2
 
3
+ [Changelog](CHANGELOG.md)
4
+
3
5
  # Convert Svelte (or React/Angular/Vue) JS application to serve it from ESP32/ESP8266 webserver
4
6
 
5
7
  ### Forget SPIFFS and LittleFS now
@@ -10,11 +12,13 @@ In order to be able to easily update OTA, it is important - from the users' poin
10
12
 
11
13
  This npm package provides a solution for **inserting any JS client application into the ESP web server** (PsychicHttp and also ESPAsyncWebServer available, PsychicHttp is the default). For this, JS, html, css, font, assets, etc. files must be converted to binary byte array. Npm mode is easy to use and easy to **integrate into your CI/CD pipeline**.
12
14
 
13
- > Starting with version v1.1.0, the ETag header is also supported.
15
+ > Version v1.4.0 has a breaking change! --no-gzip changed to --gzip. Starting with this version c++ compiler directives are available to setup operation in project level.
16
+
17
+ > Starting with version v1.3.0, c++ defines can be used.
14
18
 
15
19
  > Starting with version v1.2.0, ESP8266/ESP8285 is also supported.
16
20
 
17
- > Starting with version v1.3.0, c++ defines can be used.
21
+ > Starting with version v1.1.0, the ETag header is also supported.
18
22
 
19
23
  ### Usage
20
24
 
@@ -28,29 +32,20 @@ After a successful Svelte build (rollup/webpack/vite) **create an includeable c+
28
32
 
29
33
  ```bash
30
34
  // for PsychicHttpServer
31
- npx svelteesp32 -e psychic -s ../svelteapp/dist -o ../esp32project/svelteesp32.h --etag
35
+ npx svelteesp32 -e psychic -s ../svelteapp/dist -o ../esp32project/svelteesp32.h --etag=true
32
36
 
33
37
  // for ESPAsyncWebServer
34
- npx svelteesp32 -e async -s ../svelteapp/dist -o ../esp32project/svelteesp32.h --etag
38
+ npx svelteesp32 -e async -s ../svelteapp/dist -o ../esp32project/svelteesp32.h --etag=true
35
39
  ```
36
40
 
37
41
  During the **translation process**, the processed file details are visible, and at the end, the result shows the ESP's memory allocation (gzip size)
38
42
 
39
43
  ```
40
- [assets/index-KwubEIf-.js]
41
- ✓ gzip used (38850 -> 12547)
42
-
43
- [assets/index-Soe6cpLA.css]
44
- ✓ gzip used (32494 -> 5368)
45
-
46
- [favicon.png]
47
- x gzip unused (33249 -> 33282)
48
-
49
- [index.html]
50
- ✓ gzip used (472 -> 308)
51
-
52
- [roboto_regular.json]
53
- ✓ gzip used (363757 -> 93567)
44
+ [assets/index-KwubEIf-.js] ✓ gzip used (38850 -> 12547)
45
+ [assets/index-Soe6cpLA.css] ✓ gzip used (32494 -> 5368)
46
+ [favicon.png] x gzip unused (33249 -> 33282)
47
+ [index.html] x gzip unused (too small) (472 -> 308)
48
+ [roboto_regular.json] ✓ gzip used (363757 -> 93567)
54
49
 
55
50
  5 files, 458kB original size, 142kB gzip size
56
51
  ../../../Arduino/EspSvelte/svelteesp32.h 842kB size
@@ -158,9 +153,11 @@ If you **only work on ESP32**, I recommend using PsychicHttpServer, which uses t
158
153
 
159
154
  All modern browsers have been able to handle gzip-compressed content for years. For this reason, there is no question that the easily compressed JS and CSS files are stored compressed in the ESP32/ESP8266 and sent to the browser.
160
155
 
161
- During the translation process, data in gzip format is generated and will be used if the **size is greater than 100 bytes** and we experience a **reduction of at least 15%**. In such a case, the compressed data is unconditionally sent to the browser with the appropriate **Content-Encoding** header information.
156
+ During the translation process, data in gzip format is generated and will be used if the **size is greater than 1024 bytes** and we experience a **reduction of at least 15%**. In such a case, the compressed data is unconditionally sent to the browser with the appropriate **Content-Encoding** header information.
157
+
158
+ Automatic compression can be turned off with the `--gzip=false` option.
162
159
 
163
- Automatic compression can be turned off with the `--no-gzip` option.
160
+ > This setting has three states: yes, no, and compiler mode is available. In compiler mode, you can disable/enable Gzip by setting the `SVELTEESP32_ENABLE_GZIP` c++ compiler directive. For example, if using platformio, just write `-D SVELTEESP32_ENABLE_GZIP`.
164
161
 
165
162
  ### ETag
166
163
 
@@ -168,7 +165,9 @@ The ETag HTTP header can be used to significantly reduce network traffic. If the
168
165
 
169
166
  Since microcontroller data traffic is moderately expensive, it is an individual decision whether to use the ETag or not. We **recommend using ETag**, which adds a bit more code (about 1-3%) but results in a much cleaner operation.
170
167
 
171
- The use of ETag is **not enabled by default**, this can be achieved with the `--etag` switch.
168
+ The use of ETag is **not enabled by default**, this can be achieved with the `--etag=true` switch.
169
+
170
+ > 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`.
172
171
 
173
172
  ### Main entry point - index.html
174
173
 
@@ -214,27 +213,35 @@ You can include a blocker error if a named file accidentally missing from the bu
214
213
  ...
215
214
  ```
216
215
 
216
+ You can use the following c++ directives at the project level if you want to configure the usage there: `SVELTEESP32_ENABLE_ETAG` and `SVELTEESP32_ENABLE_GZIP`. (Do not forget `--etag=compiler` or `--gzip=compiler` command line arg!)
217
+
217
218
  ### Command line options
218
219
 
219
- | Option | Description | default |
220
- | ------------- | --------------------------------------------------------- | ----------------------- | ------- |
221
- | `-s` | **Source dist folder contains compiled web files** | |
222
- | `-e` | The engine for which the include file is created [psychic | async] | psychic |
223
- | `-o` | Generated output file with path | `svelteesp32.h` |
224
- | `--etag` | Use ETag header for cache | false |
225
- | `--no-gzip` | Do not compress content with gzip | false -> gzip used |
226
- | `--espmethod` | Name of generated method | `initSvelteStaticFiles` |
227
- | `--define` | Prefix of c++ defines | `SVELTEESP32` |
228
- | `-h` | Show help | |
220
+ | Option | Description | default |
221
+ | ------------- | ---------------------------------------------------------------- | ----------------------- |
222
+ | `-s` | **Source dist folder contains compiled web files** | |
223
+ | `-e` | The engine for which the include file is created (psychic/async) | psychic |
224
+ | `-o` | Generated output file with path | `svelteesp32.h` |
225
+ | `--etag` | Use ETag header for cache (true/false/compiler) | false |
226
+ | `--gzip` | Compress content with gzip (true/false/compiler) | true |
227
+ | `--created` | Include creation time | false |
228
+ | `--version` | Include a version string, `--version=v$npm_package_version` | '' |
229
+ | `--espmethod` | Name of generated method | `initSvelteStaticFiles` |
230
+ | `--define` | Prefix of c++ defines | `SVELTEESP32` |
231
+ | `-h` | Show help | |
229
232
 
230
233
  ### Q&A
231
234
 
232
235
  - **How big a frontend application can be placed?** If you compress the content with gzip, even a 3-4Mb assets directory can be placed. This is a serious enough amount to serve a complete application.
233
236
 
234
- - **How fast is cpp file compilation?** The cpp 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.
237
+ - **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.
235
238
 
236
239
  - **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.
237
240
 
238
- - **Is this safe to use in production?** I suggest you give it a try! If you find it useful and safe in several different situations, feel free to use it, just like any other free library.
241
+ - **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.
242
+
243
+ - **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.
239
244
 
240
245
  - **Will you develop it further?** Since I use it myself, I will do my best to make the solution better and better.
246
+
247
+ - **Is this safe to use in production?** I suggest you give it a try! If you find it useful and safe in several different situations, feel free to use it, just like any other free library.
@@ -4,8 +4,10 @@ interface ICopyFilesArguments {
4
4
  outputfile: string;
5
5
  espmethod: string;
6
6
  define: string;
7
- 'no-gzip': boolean;
8
- etag: boolean;
7
+ gzip: 'true' | 'false' | 'compiler';
8
+ etag: 'true' | 'false' | 'compiler';
9
+ created: boolean;
10
+ version: string;
9
11
  help?: boolean;
10
12
  }
11
13
  export declare const cmdLine: ICopyFilesArguments;
@@ -13,7 +13,7 @@ exports.cmdLine = (0, ts_command_line_args_1.parse)({
13
13
  throw new Error(`Invalid engine: ${value}`);
14
14
  },
15
15
  alias: 'e',
16
- description: 'The engine for which the include file is created',
16
+ description: 'The engine for which the include file is created (psychic|async)',
17
17
  defaultValue: 'psychic'
18
18
  },
19
19
  sourcepath: {
@@ -27,16 +27,42 @@ exports.cmdLine = (0, ts_command_line_args_1.parse)({
27
27
  description: 'Generated output file with path',
28
28
  defaultValue: 'svelteesp32.h'
29
29
  },
30
- 'no-gzip': {
31
- type: Boolean,
32
- description: 'Do not compress content with gzip',
33
- defaultValue: false
34
- },
35
30
  etag: {
36
- type: Boolean,
31
+ type: (value) => {
32
+ if (value === 'true')
33
+ return 'true';
34
+ if (value === 'false')
35
+ return 'false';
36
+ if (value === 'compiler')
37
+ return 'compiler';
38
+ throw new Error(`Invalid etag: ${value}`);
39
+ },
37
40
  description: 'Use ETAG header for cache',
41
+ defaultValue: 'false'
42
+ },
43
+ gzip: {
44
+ type: (value) => {
45
+ if (value === 'true')
46
+ return 'true';
47
+ if (value === 'false')
48
+ return 'false';
49
+ if (value === 'compiler')
50
+ return 'compiler';
51
+ throw new Error(`Invalid etag: ${value}`);
52
+ },
53
+ description: 'Compress content with gzip',
54
+ defaultValue: 'true'
55
+ },
56
+ created: {
57
+ type: Boolean,
58
+ description: 'Include creation time in the output file',
38
59
  defaultValue: false
39
60
  },
61
+ version: {
62
+ type: String,
63
+ description: 'Include version info in the output file',
64
+ defaultValue: ''
65
+ },
40
66
  espmethod: {
41
67
  type: String,
42
68
  description: 'Name of generated method',
@@ -0,0 +1,3 @@
1
+ export declare const greenLog: (s: string) => string;
2
+ export declare const yellowLog: (s: string) => string;
3
+ export declare const redLog: (s: string) => string;
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.redLog = exports.yellowLog = exports.greenLog = void 0;
4
+ const greenLog = (s) => `\u001B[32m${s}\u001B[0m`;
5
+ exports.greenLog = greenLog;
6
+ const yellowLog = (s) => `\u001B[33m${s}\u001B[0m`;
7
+ exports.yellowLog = yellowLog;
8
+ const redLog = (s) => `\u001B[31m${s}\u001B[0m`;
9
+ exports.redLog = redLog;
package/dist/cppCode.d.ts CHANGED
@@ -4,6 +4,7 @@ export type CppCodeSource = {
4
4
  datanameUpperCase: string;
5
5
  mime: string;
6
6
  content: Buffer;
7
+ contentGzip: Buffer;
7
8
  isGzip: boolean;
8
9
  md5: string;
9
10
  };
package/dist/cppCode.js CHANGED
@@ -5,52 +5,176 @@ const handlebars_1 = require("handlebars");
5
5
  const commandLine_1 = require("./commandLine");
6
6
  const psychicTemplate = `
7
7
  //engine: PsychicHttpServer
8
- //cmdline: {{commandLine}}
8
+ //cmdline: {{{commandLine}}}
9
+ {{#if created }}
9
10
  //created: {{now}}
10
- //files: {{fileCount}}
11
- //memory: {{fileSize}}
11
+ {{/if}}
12
+ //
12
13
 
13
- #include <Arduino.h>
14
- #include <PsychicHttp.h>
15
- #include <PsychicHttpsServer.h>
14
+ {{#switch etag}}
15
+ {{#case "true"}}
16
+ #ifdef {{definePrefix}}_ENABLE_ETAG
17
+ #warning {{definePrefix}}_ENABLE_ETAG has no effect because it is permanently switched ON
18
+ #endif
19
+ {{/case}}
20
+ {{#case "false"}}
21
+ #ifdef {{definePrefix}}_ENABLE_ETAG
22
+ #warning {{definePrefix}}_ENABLE_ETAG has no effect because it is permanently switched OFF
23
+ #endif
24
+ {{/case}}
25
+ {{/switch}}
16
26
 
27
+ {{#switch gzip}}
28
+ {{#case "true"}}
29
+ #ifdef {{definePrefix}}_ENABLE_GZIP
30
+ #warning {{definePrefix}}_ENABLE_GZIP has no effect because it is permanently switched ON
31
+ #endif
32
+ {{/case}}
33
+ {{#case "false"}}
34
+ #ifdef {{definePrefix}}_ENABLE_GZIP
35
+ #warning {{definePrefix}}_ENABLE_GZIP has no effect because it is permanently switched OFF
36
+ #endif
37
+ {{/case}}
38
+ {{/switch}}
39
+
40
+ //
41
+ {{#if version }}
42
+ #define {{definePrefix}}_VERSION "{{version}}"
43
+ {{/if}}
17
44
  #define {{definePrefix}}_COUNT {{fileCount}}
18
45
  #define {{definePrefix}}_SIZE {{fileSize}}
46
+ #define {{definePrefix}}_SIZE_GZIP {{fileGzipSize}}
19
47
 
48
+ //
20
49
  {{#each sources}}
21
50
  #define {{../definePrefix}}_FILE_{{this.datanameUpperCase}}
22
51
  {{/each}}
23
52
 
53
+ //
24
54
  {{#each filesByExtension}}
25
55
  #define {{../definePrefix}}_{{this.extension}}_FILES {{this.count}}
26
56
  {{/each}}
27
57
 
28
- {{#each sources}}
58
+ //
59
+ #include <Arduino.h>
60
+ #include <PsychicHttp.h>
61
+ #include <PsychicHttpsServer.h>
62
+
63
+ //
64
+ {{#switch gzip}}
65
+ {{#case "true"}}
66
+ {{#each sources}}
67
+ const uint8_t datagzip_{{this.dataname}}[{{this.lengthGzip}}] = { {{this.bytesGzip}} };
68
+ {{/each}}
69
+ {{/case}}
70
+ {{#case "false"}}
71
+ {{#each sources}}
72
+ const uint8_t data_{{this.dataname}}[{{this.length}}] = { {{this.bytes}} };
73
+ {{/each}}
74
+ {{/case}}
75
+ {{#case "compiler"}}
76
+ #ifdef {{definePrefix}}_ENABLE_GZIP
77
+ {{#each sources}}
78
+ const uint8_t datagzip_{{this.dataname}}[{{this.lengthGzip}}] = { {{this.bytesGzip}} };
79
+ {{/each}}
80
+ #else
81
+ {{#each sources}}
29
82
  const uint8_t data_{{this.dataname}}[{{this.length}}] = { {{this.bytes}} };
30
- {{#if ../isEtag}}
83
+ {{/each}}
84
+ #endif
85
+ {{/case}}
86
+ {{/switch}}
87
+
88
+ //
89
+ {{#switch etag}}
90
+ {{#case "true"}}
91
+ {{#each sources}}
31
92
  const char * etag_{{this.dataname}} = "{{this.md5}}";
32
- {{/if}}
33
- {{/each}}
93
+ {{/each}}
94
+ {{/case}}
95
+ {{#case "false"}}
96
+ {{/case}}
97
+ {{#case "compiler"}}
98
+ #ifdef {{definePrefix}}_ENABLE_ETAG
99
+ {{#each sources}}
100
+ const char * etag_{{this.dataname}} = "{{this.md5}}";
101
+ {{/each}}
102
+ #endif
103
+ {{/case}}
104
+ {{/switch}}
34
105
 
106
+ //
107
+ // Http Handlers
35
108
  void {{methodName}}(PsychicHttpServer * server) {
36
109
  {{#each sources}}
110
+ //
111
+ // {{this.filename}}
37
112
  {{#if this.isDefault}}server->defaultEndpoint = {{/if}}server->on("/{{this.filename}}", HTTP_GET, [](PsychicRequest * request) {
38
- {{#if ../isEtag}}
113
+
114
+ {{#switch ../etag}}
115
+ {{#case "true"}}
39
116
  if (request->hasHeader("If-None-Match") && request->header("If-None-Match") == String(etag_{{this.dataname}})) {
40
117
  PsychicResponse response304(request);
41
118
  response304.setCode(304);
42
119
  return response304.send();
43
120
  }
44
- {{/if}}
121
+ {{/case}}
122
+ {{#case "compiler"}}
123
+ #ifdef {{../definePrefix}}_ENABLE_ETAG
124
+ if (request->hasHeader("If-None-Match") && request->header("If-None-Match") == String(etag_{{this.dataname}})) {
125
+ PsychicResponse response304(request);
126
+ response304.setCode(304);
127
+ return response304.send();
128
+ }
129
+ #endif
130
+ {{/case}}
131
+ {{/switch}}
132
+
45
133
  PsychicResponse response(request);
46
134
  response.setContentType("{{this.mime}}");
47
- {{#if this.isGzip}}
135
+
136
+ {{#switch ../gzip}}
137
+ {{#case "true"}}
138
+ {{#if this.isGzip}}
48
139
  response.addHeader("Content-Encoding", "gzip");
49
- {{/if}}
50
- {{#if ../isEtag}}
140
+ {{/if}}
141
+ {{/case}}
142
+ {{#case "compiler"}}
143
+ {{#if this.isGzip}}
144
+ #ifdef {{../definePrefix}}_ENABLE_GZIP
145
+ response.addHeader("Content-Encoding", "gzip");
146
+ #endif
147
+ {{/if}}
148
+ {{/case}}
149
+ {{/switch}}
150
+
151
+ {{#switch ../etag}}
152
+ {{#case "true"}}
51
153
  response.addHeader("ETag", etag_{{this.dataname}});
52
- {{/if}}
53
- response.setContent(data_{{this.dataname}}, sizeof(data_{{this.dataname}}));
154
+ {{/case}}
155
+ {{#case "compiler"}}
156
+ #ifdef {{../definePrefix}}_ENABLE_ETAG
157
+ response.addHeader("ETag", etag_{{this.dataname}});
158
+ #endif
159
+ {{/case}}
160
+ {{/switch}}
161
+
162
+ {{#switch ../gzip}}
163
+ {{#case "true"}}
164
+ response.setContent(datagzip_{{this.dataname}}, {{this.lengthGzip}});
165
+ {{/case}}
166
+ {{#case "false"}}
167
+ response.setContent(data_{{this.dataname}}, {{this.length}});
168
+ {{/case}}
169
+ {{#case "compiler"}}
170
+ #ifdef {{../definePrefix}}_ENABLE_GZIP
171
+ response.setContent(datagzip_{{this.dataname}}, {{this.lengthGzip}});
172
+ #else
173
+ response.setContent(data_{{this.dataname}}, {{this.length}});
174
+ #endif
175
+ {{/case}}
176
+ {{/switch}}
177
+
54
178
  return response.send();
55
179
  });
56
180
 
@@ -58,48 +182,161 @@ void {{methodName}}(PsychicHttpServer * server) {
58
182
  }`;
59
183
  const asyncTemplate = `
60
184
  //engine: ESPAsyncWebServer
61
- //cmdline: {{commandLine}}
185
+ //cmdline: {{{commandLine}}}
186
+ {{#if created }}
62
187
  //created: {{now}}
63
- //files: {{fileCount}}
64
- //memory: {{fileSize}}
188
+ {{/if}}
189
+ //
65
190
 
66
- #include <Arduino.h>
67
- #include <ESPAsyncWebServer.h>
191
+ {{#switch etag}}
192
+ {{#case "true"}}
193
+ #ifdef {{definePrefix}}_ENABLE_ETAG
194
+ #warning {{definePrefix}}_ENABLE_ETAG has no effect because it is permanently switched ON
195
+ #endif
196
+ {{/case}}
197
+ {{#case "false"}}
198
+ #ifdef {{definePrefix}}_ENABLE_ETAG
199
+ #warning {{definePrefix}}_ENABLE_ETAG has no effect because it is permanently switched OFF
200
+ #endif
201
+ {{/case}}
202
+ {{/switch}}
68
203
 
204
+ {{#switch gzip}}
205
+ {{#case "true"}}
206
+ #ifdef {{definePrefix}}_ENABLE_GZIP
207
+ #warning {{definePrefix}}_ENABLE_GZIP has no effect because it is permanently switched ON
208
+ #endif
209
+ {{/case}}
210
+ {{#case "false"}}
211
+ #ifdef {{definePrefix}}_ENABLE_GZIP
212
+ #warning {{definePrefix}}_ENABLE_GZIP has no effect because it is permanently switched OFF
213
+ #endif
214
+ {{/case}}
215
+ {{/switch}}
216
+
217
+ //
218
+ {{#if version }}
219
+ #define {{definePrefix}}_VERSION "{{version}}"
220
+ {{/if}}
69
221
  #define {{definePrefix}}_COUNT {{fileCount}}
70
222
  #define {{definePrefix}}_SIZE {{fileSize}}
223
+ #define {{definePrefix}}_SIZE_GZIP {{fileGzipSize}}
71
224
 
225
+ //
72
226
  {{#each sources}}
73
227
  #define {{../definePrefix}}_FILE_{{this.datanameUpperCase}}
74
228
  {{/each}}
75
229
 
230
+ //
76
231
  {{#each filesByExtension}}
77
232
  #define {{../definePrefix}}_{{this.extension}}_FILES {{this.count}}
78
233
  {{/each}}
79
234
 
80
- {{#each sources}}
235
+ //
236
+ #include <Arduino.h>
237
+ #include <ESPAsyncWebServer.h>
238
+
239
+ //
240
+ {{#switch gzip}}
241
+ {{#case "true"}}
242
+ {{#each sources}}
243
+ const uint8_t datagzip_{{this.dataname}}[{{this.lengthGzip}}] PROGMEM = { {{this.bytesGzip}} };
244
+ {{/each}}
245
+ {{/case}}
246
+ {{#case "false"}}
247
+ {{#each sources}}
81
248
  const uint8_t data_{{this.dataname}}[{{this.length}}] PROGMEM = { {{this.bytes}} };
82
- {{#if ../isEtag}}
249
+ {{/each}}
250
+ {{/case}}
251
+ {{#case "compiler"}}
252
+ #ifdef {{definePrefix}}_ENABLE_GZIP
253
+ {{#each sources}}
254
+ const uint8_t datagzip_{{this.dataname}}[{{this.lengthGzip}}] PROGMEM = { {{this.bytesGzip}} };
255
+ {{/each}}
256
+ #else
257
+ {{#each sources}}
258
+ const uint8_t data_{{this.dataname}}[{{this.length}}] PROGMEM = { {{this.bytes}} };
259
+ {{/each}}
260
+ #endif
261
+ {{/case}}
262
+ {{/switch}}
263
+
264
+ //
265
+ {{#switch etag}}
266
+ {{#case "true"}}
267
+ {{#each sources}}
83
268
  const char * etag_{{this.dataname}} = "{{this.md5}}";
84
- {{/if}}
85
- {{/each}}
269
+ {{/each}}
270
+ {{/case}}
271
+ {{#case "false"}}
272
+ {{/case}}
273
+ {{#case "compiler"}}
274
+ #ifdef {{definePrefix}}_ENABLE_ETAG
275
+ {{#each sources}}
276
+ const char * etag_{{this.dataname}} = "{{this.md5}}";
277
+ {{/each}}
278
+ #endif
279
+ {{/case}}
280
+ {{/switch}}
86
281
 
282
+ //
283
+ // Http Handlers
87
284
  void {{methodName}}(AsyncWebServer * server) {
88
285
  {{#each sources}}
286
+ //
287
+ // {{this.filename}}
89
288
  ArRequestHandlerFunction func_{{this.dataname}} = [](AsyncWebServerRequest * request) {
90
- {{#if ../isEtag}}
289
+
290
+ {{#switch ../etag}}
291
+ {{#case "true"}}
292
+ if (request->hasHeader("If-None-Match") && request->getHeader("If-None-Match")->value() == String(etag_{{this.dataname}})) {
293
+ request->send(304);
294
+ return;
295
+ }
296
+ {{/case}}
297
+ {{#case "compiler"}}
298
+ #ifdef {{../definePrefix}}_ENABLE_ETAG
91
299
  if (request->hasHeader("If-None-Match") && request->getHeader("If-None-Match")->value() == String(etag_{{this.dataname}})) {
92
300
  request->send(304);
93
301
  return;
94
302
  }
303
+ #endif
304
+ {{/case}}
305
+ {{/switch}}
306
+
307
+ {{#switch ../gzip}}
308
+ {{#case "true"}}
309
+ AsyncWebServerResponse *response = request->beginResponse_P(200, "{{this.mime}}", datagzip_{{this.dataname}}, {{this.lengthGzip}});
310
+ {{#if this.isGzip}}
311
+ response->addHeader("Content-Encoding", "gzip");
95
312
  {{/if}}
313
+ {{/case}}
314
+ {{#case "false"}}
96
315
  AsyncWebServerResponse *response = request->beginResponse_P(200, "{{this.mime}}", data_{{this.dataname}}, {{this.length}});
316
+ {{/case}}
317
+ {{#case "compiler"}}
318
+ #ifdef {{../definePrefix}}_ENABLE_GZIP
319
+ AsyncWebServerResponse *response = request->beginResponse_P(200, "{{this.mime}}", datagzip_{{this.dataname}}, {{this.lengthGzip}});
97
320
  {{#if this.isGzip}}
98
321
  response->addHeader("Content-Encoding", "gzip");
99
322
  {{/if}}
100
- {{#if ../isEtag}}
323
+ #else
324
+ AsyncWebServerResponse *response = request->beginResponse_P(200, "{{this.mime}}", data_{{this.dataname}}, {{this.length}});
325
+ #endif
326
+ {{/case}}
327
+ {{/switch}}
328
+
329
+ {{#switch ../etag}}
330
+ {{#case "true"}}
101
331
  response->addHeader("ETag", etag_{{this.dataname}});
102
- {{/if}}
332
+ {{/case}}
333
+ {{#case "compiler"}}
334
+ #ifdef {{../definePrefix}}_ENABLE_ETAG
335
+ response->addHeader("ETag", etag_{{this.dataname}});
336
+ #endif
337
+ {{/case}}
338
+ {{/switch}}
339
+
103
340
  request->send(response);
104
341
  };
105
342
  server->on("/{{this.filename}}", HTTP_GET, func_{{this.dataname}});
@@ -109,20 +346,50 @@ void {{methodName}}(AsyncWebServer * server) {
109
346
 
110
347
  {{/each}}
111
348
  }`;
349
+ let switchValue;
112
350
  const getCppCode = (sources, filesByExtension) => (0, handlebars_1.compile)(commandLine_1.cmdLine.engine === 'psychic' ? psychicTemplate : asyncTemplate)({
113
351
  commandLine: process.argv.slice(2).join(' '),
114
352
  now: `${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()}`,
115
353
  fileCount: sources.length.toString(),
116
354
  fileSize: sources.reduce((previous, current) => previous + current.content.length, 0).toString(),
355
+ fileGzipSize: sources.reduce((previous, current) => previous + current.contentGzip.length, 0).toString(),
117
356
  sources: sources.map((s) => ({
118
357
  ...s,
119
358
  length: s.content.length,
120
- bytes: [...s.content].map((v) => `0x${v.toString(16)}`).join(', '),
359
+ bytes: [...s.content].map((v) => `${v.toString(10)}`).join(','),
360
+ lengthGzip: s.contentGzip.length,
361
+ bytesGzip: [...s.contentGzip].map((v) => `${v.toString(10)}`).join(','),
121
362
  isDefault: s.filename.startsWith('index.htm')
122
363
  })),
123
364
  filesByExtension,
124
- isEtag: commandLine_1.cmdLine.etag,
365
+ etag: commandLine_1.cmdLine.etag,
366
+ gzip: commandLine_1.cmdLine.gzip,
367
+ created: commandLine_1.cmdLine.created,
368
+ version: commandLine_1.cmdLine.version,
125
369
  methodName: commandLine_1.cmdLine.espmethod,
126
370
  definePrefix: commandLine_1.cmdLine.define
127
- }).trim();
371
+ }, {
372
+ helpers: {
373
+ ifeq: function (a, b, options) {
374
+ if (a == b)
375
+ return options.fn(this);
376
+ return options.inverse(this);
377
+ },
378
+ switch: function (value, options) {
379
+ switchValue = value;
380
+ return options.fn(this);
381
+ },
382
+ case: function (value, options) {
383
+ if (value == switchValue)
384
+ return options.fn(this);
385
+ return options.inverse(this);
386
+ }
387
+ }
388
+ })
389
+ .split('\n')
390
+ .map((line) => line.trimEnd())
391
+ .filter(Boolean)
392
+ .map((line) => (line == '//' ? '' : line))
393
+ .join('\n')
394
+ .replace(/\\n{2}/, '\n');
128
395
  exports.getCppCode = getCppCode;
package/dist/file.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const getFiles: () => string[];
1
+ export declare const getFiles: () => Map<string, Buffer>;
package/dist/file.js CHANGED
@@ -4,10 +4,45 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.getFiles = void 0;
7
+ const node_crypto_1 = require("node:crypto");
8
+ const node_fs_1 = require("node:fs");
7
9
  const node_path_1 = __importDefault(require("node:path"));
8
10
  const glob_1 = require("glob");
9
11
  const commandLine_1 = require("./commandLine");
10
- const getFiles = () => (0, glob_1.globSync)('**/*', { cwd: commandLine_1.cmdLine.sourcepath, nodir: true })
11
- .filter((filename) => !['.gz', '.brottli'].includes(node_path_1.default.extname(filename)))
12
- .sort();
12
+ const consoleColor_1 = require("./consoleColor");
13
+ const findSimilarFiles = (files) => {
14
+ const contentComparer = new Map();
15
+ for (const [filename, content] of files.entries()) {
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
20
+ contentComparer.set(hash, [filename]);
21
+ }
22
+ const result = [];
23
+ for (const filenames of contentComparer.values())
24
+ if (filenames.length > 1)
25
+ result.push(filenames);
26
+ return result;
27
+ };
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
+ }
38
+ }
39
+ return true;
40
+ });
41
+ 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`));
46
+ return result;
47
+ };
13
48
  exports.getFiles = getFiles;
package/dist/index.js CHANGED
@@ -9,6 +9,7 @@ const node_path_1 = __importDefault(require("node:path"));
9
9
  const node_zlib_1 = require("node:zlib");
10
10
  const mime_types_1 = require("mime-types");
11
11
  const commandLine_1 = require("./commandLine");
12
+ const consoleColor_1 = require("./consoleColor");
12
13
  const cppCode_1 = require("./cppCode");
13
14
  const file_1 = require("./file");
14
15
  const summary = {
@@ -18,16 +19,19 @@ const summary = {
18
19
  };
19
20
  const sources = [];
20
21
  const filesByExtension = [];
22
+ console.log('Collecting source files');
21
23
  const files = (0, file_1.getFiles)();
22
- if (files.length === 0) {
24
+ if (files.size === 0) {
23
25
  console.error(`Directory ${commandLine_1.cmdLine.sourcepath} is empty`);
24
26
  process.exit(1);
25
27
  }
26
- for (const file of files) {
27
- const mime = (0, mime_types_1.lookup)(file) || 'text/plain';
28
+ console.log();
29
+ console.log('Translation to header file');
30
+ const longestFilename = [...files.keys()].reduce((p, c) => (c.length > p ? c.length : p), 0);
31
+ for (const [originalFilename, content] of files) {
32
+ const mime = (0, mime_types_1.lookup)(originalFilename) || 'text/plain';
28
33
  summary.filecount++;
29
- console.log(`[${file}]`);
30
- const filename = file.replace(/\\/g, '/');
34
+ const filename = originalFilename.replace(/\\/g, '/');
31
35
  const dataname = filename.replace(/[./-]/g, '_');
32
36
  let extension = node_path_1.default.extname(filename).toUpperCase();
33
37
  if (extension.startsWith('.'))
@@ -37,58 +41,42 @@ for (const file of files) {
37
41
  group.count += 1;
38
42
  else
39
43
  filesByExtension.push({ extension, count: 1 });
40
- const rawContent = (0, node_fs_1.readFileSync)(node_path_1.default.join(commandLine_1.cmdLine.sourcepath, file), { flag: 'r' });
41
- const md5 = (0, node_crypto_1.createHash)('md5').update(rawContent).digest('hex');
42
- summary.size += rawContent.length;
43
- if (commandLine_1.cmdLine['no-gzip']) {
44
+ const md5 = (0, node_crypto_1.createHash)('md5').update(content).digest('hex');
45
+ summary.size += content.length;
46
+ const zipContent = (0, node_zlib_1.gzipSync)(content, { level: 9 });
47
+ 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) {
44
50
  sources.push({
45
- filename: filename,
51
+ filename,
46
52
  dataname,
47
53
  datanameUpperCase: dataname.toUpperCase(),
48
- content: rawContent,
49
- isGzip: false,
54
+ content,
55
+ contentGzip: zipContent,
56
+ isGzip: true,
50
57
  mime,
51
58
  md5
52
59
  });
53
- console.log(`- gzip skipped (${rawContent.length})`);
60
+ console.log((0, consoleColor_1.greenLog)(` [${originalFilename}] ${' '.repeat(longestFilename - originalFilename.length)} ✓ gzip used (${content.length} -> ${zipContent.length} = ${zipRatio}%)`));
54
61
  }
55
62
  else {
56
- const zipContent = (0, node_zlib_1.gzipSync)(rawContent, { level: 9 });
57
- summary.gzipsize += zipContent.length;
58
- if (rawContent.length > 100 && zipContent.length < rawContent.length * 0.85) {
59
- sources.push({
60
- filename: filename,
61
- dataname,
62
- datanameUpperCase: dataname.toUpperCase(),
63
- content: zipContent,
64
- isGzip: true,
65
- mime,
66
- md5
67
- });
68
- console.log(`✓ gzip used (${rawContent.length} -> ${zipContent.length})`);
69
- }
70
- else {
71
- sources.push({
72
- filename: filename,
73
- dataname,
74
- datanameUpperCase: dataname.toUpperCase(),
75
- content: rawContent,
76
- isGzip: false,
77
- mime,
78
- md5
79
- });
80
- console.log(`x gzip unused (${rawContent.length} -> ${zipContent.length})`);
81
- }
63
+ sources.push({
64
+ filename,
65
+ dataname,
66
+ datanameUpperCase: dataname.toUpperCase(),
67
+ content,
68
+ contentGzip: content,
69
+ isGzip: false,
70
+ mime,
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}%)`));
82
74
  }
83
- console.log('');
84
75
  }
76
+ console.log('');
85
77
  filesByExtension.sort((left, right) => left.extension.localeCompare(right.extension));
86
78
  const cppFile = (0, cppCode_1.getCppCode)(sources, filesByExtension);
87
- (0, node_fs_1.writeFileSync)(commandLine_1.cmdLine.outputfile, cppFile);
88
- if (commandLine_1.cmdLine['no-gzip']) {
89
- console.log(`${summary.filecount} files, ${Math.round(summary.size / 1024)}kB original size`);
90
- }
91
- else {
92
- console.log(`${summary.filecount} files, ${Math.round(summary.size / 1024)}kB original size, ${Math.round(summary.gzipsize / 1024)}kB gzip size`);
93
- }
79
+ (0, node_fs_1.mkdirSync)(node_path_1.default.normalize(node_path_1.default.dirname(commandLine_1.cmdLine.outputfile)), { recursive: true });
80
+ (0, node_fs_1.writeFileSync)(commandLine_1.cmdLine.outputfile, cppFile, { flush: true, encoding: 'utf8' });
81
+ console.log(`${summary.filecount} files, ${Math.round(summary.size / 1024)}kB original size, ${Math.round(summary.gzipsize / 1024)}kB gzip size`);
94
82
  console.log(`${commandLine_1.cmdLine.outputfile} ${Math.round(cppFile.length / 1024)}kB size`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelteesp32",
3
- "version": "1.3.1",
3
+ "version": "1.4.1",
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",
@@ -28,20 +28,17 @@
28
28
  },
29
29
  "homepage": "https://github.com/BCsabaEngine/svelteesp32",
30
30
  "scripts": {
31
- "dev:async": "nodemon src/index.ts -- -e async -s ./demo/svelte/dist -o ./demo/esp32/include/svelteesp32async.h --etag",
32
- "dev:psychic": "nodemon src/index.ts -- -e psychic -s ./demo/svelte/dist -o ./demo/esp32/include/svelteesp32psychic.h --etag",
33
- "run:async": "tsx src/index.ts -e async -s ./demo/svelte/dist -o ./demo/esp32/include/svelteesp32async.h --etag",
34
- "run:psychic": "tsx src/index.ts -e psychic -s ./demo/svelte/dist -o ./demo/esp32/include/svelteesp32psychic.h --etag",
35
- "run:both": "npm run run:async && npm run run:psychic",
36
- "platformio": "~/.platformio/penv/bin/pio run -d ./demo/esp32",
37
- "run:both:build": "npm run run:both && npm run platformio",
31
+ "dev:async": "nodemon src/index.ts -- -e async -s ./demo/svelte/dist -o ./demo/esp32/include/svelteesp32.h --etag=true --gzip=true --version=v$npm_package_version",
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
+ "test:all": "./package.script && ~/.platformio/penv/bin/pio run -d ./demo/esp32",
38
34
  "clean": "tsc --build --clean",
39
35
  "build": "tsc --build --clean && tsc --build --force",
40
36
  "format:check": "prettier --check .",
41
37
  "format:fix": "prettier --write .",
42
38
  "lint:check": "eslint .",
43
39
  "lint:fix": "eslint --fix .",
44
- "fix": "npm run format:fix && npm run lint:fix"
40
+ "fix": "npm run format:fix && npm run lint:fix",
41
+ "npm:reinstall": "rm -rf ./node_modules && rm -f ./package-lock.json && npm i"
45
42
  },
46
43
  "keywords": [
47
44
  "svelte",
@@ -56,7 +53,7 @@
56
53
  ],
57
54
  "devDependencies": {
58
55
  "@types/mime-types": "^2.1.4",
59
- "@types/node": "^22.3.0",
56
+ "@types/node": "^22.4.1",
60
57
  "@typescript-eslint/eslint-plugin": "^8.1.0",
61
58
  "@typescript-eslint/parser": "^8.1.0",
62
59
  "eslint": "^9.9.0",