svelteesp32 1.0.4 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,14 +1,18 @@
1
1
  # `svelteesp32` ![image](https://badges.github.io/stability-badges/dist/stable.svg)
2
2
 
3
- # Convert Svelte (or React/Angular/Vue) JS application to serve it from ESP32 webserver
3
+ # Convert Svelte (or React/Angular/Vue) JS application to serve it from ESP32/ESP8266 webserver
4
4
 
5
5
  ### Forget SPIFFS and LittleFS now
6
6
 
7
- I often make small to medium-sized microcontroller solutions that run on ESP32. If a web interface is needed, I create a Svelte application. The Svelte application is practically served by the ESP32.
7
+ I often make small to medium-sized microcontroller solutions that run on ESP32 or ESP8266. If a web interface is needed, I create a Svelte application. The Svelte application is practically served by the ESP32/ESP8266.
8
8
 
9
9
  In order to be able to easily update OTA, it is important - from the users' point of view - that the update file **consists of one file**. I can't use the SPIFFS/LittleFS solution for this. It is necessary that the WebUI files are included inline in the Arduino or PlatformIO c++ code.
10
10
 
11
- This npm package provides a solution for **inserting any JS client application into the ESP32 web server** (PsychicHttp is my favorite, faster than ESPAsyncWebServer). 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**.
11
+ 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
+
13
+ > Starting with version v1.1.0, the ETag header is also supported.
14
+
15
+ > Starting with version v1.3.0, ESP8266/ESP8285 is also supported.
12
16
 
13
17
  ### Usage
14
18
 
@@ -21,10 +25,14 @@ npm install -D svelteesp32
21
25
  After a successful Svelte build (rollup/webpack/vite) **create an includeable c++ header** file
22
26
 
23
27
  ```bash
24
- npx svelteesp32 -s ../svelteapp/dist -o ../esp32project/svelteesp32.h
28
+ // for PsychicHttpServer
29
+ npx svelteesp32 -e psychic -s ../svelteapp/dist -o ../esp32project/svelteesp32.h --etag
30
+
31
+ // for ESPAsyncWebServer
32
+ npx svelteesp32 -e async -s ../svelteapp/dist -o ../esp32project/svelteesp32.h --etag
25
33
  ```
26
34
 
27
- During the **translation process**, the processed file details are visible, and at the end, the result shows the ESP32's memory allocation (gzip size)
35
+ During the **translation process**, the processed file details are visible, and at the end, the result shows the ESP's memory allocation (gzip size)
28
36
 
29
37
  ```
30
38
  [assets/index-KwubEIf-.js]
@@ -68,15 +76,44 @@ void setup()
68
76
  }
69
77
  ```
70
78
 
79
+ or
80
+
81
+ ```c
82
+ ...
83
+
84
+ #include <ESPAsyncWebServer.h>
85
+ #include "svelteesp32.h"
86
+
87
+ AsyncWebServer server(80);
88
+
89
+ ...
90
+
91
+ void setup()
92
+ {
93
+ ...
94
+
95
+ initSvelteStaticFiles(&server);
96
+
97
+ server.begin();
98
+ }
99
+ ```
100
+
71
101
  The content of **generated file** (do not edit, just use)
72
102
 
73
103
  ```c
74
104
  const uint8_t data0[12547] = {0x1f, 0x8b, 0x8, 0x0, ...
75
105
  const uint8_t data1[5368] = {0x1f, 0x8b, 0x8, 0x0, 0x0, ...
106
+ const char * etag0 = "387b88e345cc56ef9091...";
76
107
 
77
108
  void initSvelteStaticFiles(PsychicHttpServer * server) {
78
109
  server->on("/assets/index-KwubEIf-.js", HTTP_GET, [](PsychicRequest * request)
79
110
  {
111
+ if (request->hasHeader("If-None-Match") && ...) {
112
+ PsychicResponse response304(request);
113
+ response304.setCode(304);
114
+ return response304.send();
115
+ }
116
+
80
117
  PsychicResponse response(request);
81
118
  response.setContentType("application/javascript");
82
119
  response.addHeader("Content-Encoding", "gzip");
@@ -86,6 +123,10 @@ void initSvelteStaticFiles(PsychicHttpServer * server) {
86
123
 
87
124
  server->on("/assets/index-Soe6cpLA.css", HTTP_GET, [](PsychicRequest * request)
88
125
  {
126
+ if (request->hasHeader("If-None-Match") && ...) {
127
+ ...
128
+ }
129
+
89
130
  PsychicResponse response(request);
90
131
  response.setContentType("text/css");
91
132
  response.addHeader("Content-Encoding", "gzip");
@@ -97,35 +138,51 @@ void initSvelteStaticFiles(PsychicHttpServer * server) {
97
138
  }
98
139
  ```
99
140
 
141
+ ### Engines and ESP variants
142
+
143
+ ESPAsyncWebServer is a popular web server that can be used on **both ESP32 and ESP8266 microcontrollers**. When you want to generate a file for this, use the `-e async` switch.
144
+
145
+ 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.
146
+
100
147
  ### Gzip
101
148
 
102
- 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 and sent to the browser.
149
+ 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.
103
150
 
104
- 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 header information.
151
+ 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.
105
152
 
106
153
  Automatic compression can be turned off with the `--no-gzip` option.
107
154
 
155
+ ### ETag
156
+
157
+ The ETag HTTP header can be used to significantly reduce network traffic. If the server sends ETag information, the client can check the integrity of the file by sending back this ETag (in `If-None-Match` header) without sending the data back again. All browsers use this function, the 304 HTTP response code is clearly visible in the network traffic.
158
+
159
+ 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.
160
+
161
+ The use of ETag is **not enabled by default**, this can be achieved with the `--etag` switch.
162
+
108
163
  ### Main entry point - index.html
109
164
 
110
- Typically, the entry point for web applications is the **index.htm or index.html** file. This does not need to be listed in the browser's address bar because web servers know that this file should be served by default. Svelteesp32 also does this: if there is an index.htm or index.html file, it sets it as the main file to be served. So using `http://esp32_xxx.local` or just entering the `http://x.y.w.z/` IP address will serve this main file.
165
+ Typically, the entry point for web applications is the **index.htm or index.html** file. This does not need to be listed in the browser's address bar because web servers know that this file should be served by default. Svelteesp32 also does this: if there is an index.htm or index.html file, it sets it as the main file to be served. So using `http://esp_xxx.local` or just entering the `http://x.y.w.z/` IP address will serve this main file.
111
166
 
112
167
  ### Command line options
113
168
 
114
- | Option | Mandatory | Description | default |
115
- | ------------- | :-------: | ---------------------------------------------- | ----------------------- |
116
- | `-s` | x | Source dist folder contains compiled web files | |
117
- | `-o` | x | Generated output file with path | `svelteesp32.h` |
118
- | `--no-gzip` | | Do not compress content with gzip | |
119
- | `--espmethod` | x | Name of generated method | `initSvelteStaticFiles` |
120
- | `-h` | | Show help | |
169
+ | Option | Required | Description | default |
170
+ | ------------- | :------: | ---------------------------------------------------------------- | ----------------------- |
171
+ | `-e` | x | The engine for which the include file is created (psychic/async) | psychic |
172
+ | `-s` | x | Source dist folder contains compiled web files | |
173
+ | `-o` | x | Generated output file with path | `svelteesp32.h` |
174
+ | `--etag` | | Use ETag header for cache | false |
175
+ | `--no-gzip` | | Do not compress content with gzip | false -> gzip used |
176
+ | `--espmethod` | x | Name of generated method | `initSvelteStaticFiles` |
177
+ | `-h` | | Show help | |
121
178
 
122
179
  ### Q&A
123
180
 
124
181
  - **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.
125
182
 
126
- - **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 development.
183
+ - **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.
127
184
 
128
- - **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.
185
+ - **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.
129
186
 
130
187
  - **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.
131
188
 
@@ -1,8 +1,10 @@
1
1
  interface ICopyFilesArguments {
2
+ engine: 'psychic' | 'async';
2
3
  sourcepath: string;
3
4
  outputfile: string;
4
5
  espmethod: string;
5
6
  'no-gzip': boolean;
7
+ etag: boolean;
6
8
  help?: boolean;
7
9
  }
8
10
  export declare const cmdLine: ICopyFilesArguments;
@@ -4,6 +4,18 @@ exports.cmdLine = void 0;
4
4
  const node_fs_1 = require("node:fs");
5
5
  const ts_command_line_args_1 = require("ts-command-line-args");
6
6
  exports.cmdLine = (0, ts_command_line_args_1.parse)({
7
+ engine: {
8
+ type: (value) => {
9
+ if (value === 'psychic')
10
+ return 'psychic';
11
+ if (value === 'async')
12
+ return 'async';
13
+ throw new Error(`Invalid engine: ${value}`);
14
+ },
15
+ alias: 'e',
16
+ description: 'The engine for which the include file is created',
17
+ defaultValue: 'psychic'
18
+ },
7
19
  sourcepath: {
8
20
  type: String,
9
21
  alias: 's',
@@ -20,6 +32,11 @@ exports.cmdLine = (0, ts_command_line_args_1.parse)({
20
32
  description: 'Do not compress content with gzip',
21
33
  defaultValue: false
22
34
  },
35
+ etag: {
36
+ type: Boolean,
37
+ description: 'Use ETAG header for cache',
38
+ defaultValue: false
39
+ },
23
40
  espmethod: {
24
41
  type: String,
25
42
  description: 'Name of generated method',
@@ -34,3 +51,4 @@ if (!(0, node_fs_1.existsSync)(exports.cmdLine.sourcepath) || !(0, node_fs_1.sta
34
51
  console.error(`Directory ${exports.cmdLine.sourcepath} not exists or not a directory`);
35
52
  process.exit(1);
36
53
  }
54
+ console.log(`[SvelteESP32] Generate code for ${exports.cmdLine.engine} engine`);
package/dist/cppCode.d.ts CHANGED
@@ -5,5 +5,6 @@ export type cppCodeSource = {
5
5
  mime: string;
6
6
  content: Buffer;
7
7
  isGzip: boolean;
8
+ md5: string;
8
9
  };
9
10
  export declare const getCppCode: (sources: cppCodeSource[]) => string;
package/dist/cppCode.js CHANGED
@@ -1,50 +1,99 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getCppCode = void 0;
4
+ const handlebars_1 = require("handlebars");
4
5
  const commandLine_1 = require("./commandLine");
5
- const replaceAll = (s, from, to) => {
6
- while (s.includes(from))
7
- s = s.replace(from, to);
8
- return s;
9
- };
10
- const blockTemplate = `
11
- $default$server->on("/$filename$", HTTP_GET, [](PsychicRequest * request)
6
+ const psychicTemplate = `
7
+ //engine: PsychicHttpServer
8
+ //cmdline: {{commandLine}}
9
+ //created: {{now}}
10
+ //files: {{fileCount}}
11
+ //memory: {{fileSize}}
12
+
13
+ {{#each sources}}
14
+ const uint8_t data{{this.index}}[{{this.length}}] = { {{this.bytes}} };
15
+ {{#if ../isEtag}}
16
+ const char * etag{{this.index}} = "{{this.md5}}";
17
+ {{/if}}
18
+ {{/each}}
19
+
20
+ void {{methodName}}(PsychicHttpServer * server) {
21
+ {{#each sources}}
22
+
23
+ {{#if this.isDefault}}server->defaultEndpoint = {{/if}}server->on("/{{this.filename}}", HTTP_GET, [](PsychicRequest * request)
12
24
  {
25
+ {{#if ../isEtag}}
26
+ if (request->hasHeader("If-None-Match") && request->header("If-None-Match") == String(etag{{this.index}})) {
27
+ PsychicResponse response304(request);
28
+ response304.setCode(304);
29
+ return response304.send();
30
+ }
31
+ {{/if}}
13
32
  PsychicResponse response(request);
14
- response.setContentType("$mime$");
15
- response.addHeader("Content-Encoding", "$encoding$");
16
- response.setContent(data$index$, sizeof(data$index$));
33
+ response.setContentType("{{this.mime}}");
34
+ {{#if this.isGzip}}
35
+ response.addHeader("Content-Encoding", "gzip");
36
+ {{/if}}
37
+ {{#if ../isEtag}}
38
+ response.addHeader("ETag", etag{{this.index}});
39
+ {{/if}}
40
+ response.setContent(data{{this.index}}, sizeof(data{{this.index}}));
17
41
  return response.send();
18
42
  });
19
- `;
20
- const getCppBlock = (source) => {
21
- let result = blockTemplate;
22
- result = replaceAll(result, '$default$', source.filename.startsWith('index.htm') ? 'server->defaultEndpoint = ' : '');
23
- result = replaceAll(result, '$index$', source.index.toString());
24
- result = replaceAll(result, '$filename$', source.filename);
25
- result = replaceAll(result, '$size$', source.content.length.toString());
26
- result = replaceAll(result, '$mime$', source.mime);
27
- result = replaceAll(result, '$encoding$', source.isGzip ? 'gzip' : 'identity');
28
- return result;
29
- };
30
- const fileTemplate = `
31
- $arrays$
32
-
33
- void $method$(PsychicHttpServer * server) {
34
- $code$
43
+ {{/each}}
35
44
  }`;
36
- const getCppCode = (sources) => {
37
- const arrays = [];
38
- const blocks = [];
39
- for (const source of sources) {
40
- const bytesString = [...source.content].map((v) => `0x${v.toString(16)}`).join(', ');
41
- arrays.push(`const uint8_t data${source.index}[${source.content.length}] = {${bytesString}};`);
42
- blocks.push(getCppBlock(source));
45
+ const asyncTemplate = `
46
+ //engine: ESPAsyncWebServer
47
+ //cmdline: {{commandLine}}
48
+ //created: {{now}}
49
+ //files: {{fileCount}}
50
+ //memory: {{fileSize}}
51
+
52
+ {{#each sources}}
53
+ const uint8_t data{{this.index}}[{{this.length}}] PROGMEM = { {{this.bytes}} };
54
+ {{#if ../isEtag}}
55
+ const char * etag{{this.index}} = "{{this.md5}}";
56
+ {{/if}}
57
+ {{/each}}
58
+
59
+ void {{methodName}}(AsyncWebServer * server) {
60
+ {{#each sources}}
61
+
62
+ ArRequestHandlerFunction func{{this.index}} = [](AsyncWebServerRequest * request)
63
+ {
64
+ {{#if ../isEtag}}
65
+ if (request->hasHeader("If-None-Match") && request->getHeader("If-None-Match")->value() == String(etag{{this.index}})) {
66
+ request->send(304);
67
+ return;
43
68
  }
44
- let result = fileTemplate;
45
- result = replaceAll(result, '$arrays$', arrays.join('\n'));
46
- result = replaceAll(result, '$method$', commandLine_1.cmdLine.espmethod);
47
- result = replaceAll(result, '$code$', blocks.join(''));
48
- return result;
49
- };
69
+ {{/if}}
70
+ AsyncWebServerResponse *response = request->beginResponse_P(200, "{{this.mime}}", data{{this.index}}, {{this.length}});
71
+ {{#if this.isGzip}}
72
+ response->addHeader("Content-Encoding", "gzip");
73
+ {{/if}}
74
+ {{#if ../isEtag}}
75
+ response->addHeader("ETag", etag{{this.index}});
76
+ {{/if}}
77
+ request->send(response);
78
+ };
79
+ server->on("/{{this.filename}}", HTTP_GET, func{{this.index}});
80
+ {{#if this.isDefault}}
81
+ server->on("/", HTTP_GET, func{{this.index}});
82
+ {{/if}}
83
+ {{/each}}
84
+ }`;
85
+ const getCppCode = (sources) => (0, handlebars_1.compile)(commandLine_1.cmdLine.engine === 'psychic' ? psychicTemplate : asyncTemplate)({
86
+ commandLine: process.argv.slice(2).join(' '),
87
+ now: `${new Date().toLocaleDateString()} ${new Date().toLocaleTimeString()}`,
88
+ fileCount: sources.length.toString(),
89
+ fileSize: sources.reduce((previous, current) => previous + current.content.length, 0).toString(),
90
+ sources: sources.map((s) => ({
91
+ ...s,
92
+ length: s.content.length,
93
+ bytes: [...s.content].map((v) => `0x${v.toString(16)}`).join(', '),
94
+ isDefault: s.filename.startsWith('index.htm')
95
+ })),
96
+ isEtag: commandLine_1.cmdLine.etag,
97
+ methodName: commandLine_1.cmdLine.espmethod
98
+ }).trim();
50
99
  exports.getCppCode = getCppCode;
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ const node_crypto_1 = require("node:crypto");
3
4
  const node_fs_1 = require("node:fs");
4
5
  const node_path_1 = require("node:path");
5
6
  const node_zlib_1 = require("node:zlib");
@@ -19,6 +20,7 @@ for (const file of (0, file_1.getFiles)()) {
19
20
  summary.filecount++;
20
21
  console.log(`[${file}]`);
21
22
  const rawContent = (0, node_fs_1.readFileSync)((0, node_path_1.join)(commandLine_1.cmdLine.sourcepath, file), { flag: 'r' });
23
+ const md5 = (0, node_crypto_1.createHash)('md5').update(rawContent).digest('hex');
22
24
  summary.size += rawContent.length;
23
25
  if (commandLine_1.cmdLine['no-gzip']) {
24
26
  sources.push({
@@ -26,7 +28,8 @@ for (const file of (0, file_1.getFiles)()) {
26
28
  filename: file,
27
29
  content: rawContent,
28
30
  isGzip: false,
29
- mime
31
+ mime,
32
+ md5
30
33
  });
31
34
  console.log(`- gzip skipped (${rawContent.length})`);
32
35
  }
@@ -39,7 +42,8 @@ for (const file of (0, file_1.getFiles)()) {
39
42
  filename: file,
40
43
  content: zipContent,
41
44
  isGzip: true,
42
- mime
45
+ mime,
46
+ md5
43
47
  });
44
48
  console.log(`✓ gzip used (${rawContent.length} -> ${zipContent.length})`);
45
49
  }
@@ -49,7 +53,8 @@ for (const file of (0, file_1.getFiles)()) {
49
53
  filename: file,
50
54
  content: rawContent,
51
55
  isGzip: false,
52
- mime
56
+ mime,
57
+ md5
53
58
  });
54
59
  console.log(`x gzip unused (${rawContent.length} -> ${zipContent.length})`);
55
60
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelteesp32",
3
- "version": "1.0.4",
3
+ "version": "1.2.0",
4
4
  "description": "Convert Svelte (or any frontend) JS application to serve it from ESP32 webserver (PsychicHttp)",
5
5
  "author": "BCsabaEngine",
6
6
  "license": "ISC",
@@ -28,7 +28,10 @@
28
28
  },
29
29
  "homepage": "https://github.com/BCsabaEngine/svelteesp32",
30
30
  "scripts": {
31
- "dev": "nodemon src/index.ts -- -s ./svelte/dist -o ../../../Arduino/EspSvelte/svelteesp32.h",
31
+ "dev:psychic": "nodemon src/index.ts -- -e psychic -s ./svelte/dist -o ../../../Arduino/SvelteEsp32/svelteesp32.h --etag",
32
+ "dev:psychic:local": "nodemon src/index.ts -- -e psychic -s ./svelte/dist -o ./svelteesp32.h --etag",
33
+ "dev:async": "nodemon src/index.ts -- -e async -s ./svelte/dist -o ../../../Arduino/SvelteEsp8266/svelteesp32.h --etag",
34
+ "dev:async:local": "nodemon src/index.ts -- -e async -s ./svelte/dist -o ./svelteesp32.h --etag",
32
35
  "clean": "tsc --build --clean",
33
36
  "build": "tsc --build --clean && tsc --build --force",
34
37
  "format:check": "prettier --check .",
@@ -40,16 +43,21 @@
40
43
  "npm-publish-minor": "npm run build && npm version minor && npm publish && npm run clean && git push"
41
44
  },
42
45
  "keywords": [
43
- "Svelte",
46
+ "svelte",
47
+ "angular",
48
+ "react",
49
+ "vue",
44
50
  "eps32",
51
+ "esp8266",
45
52
  "webserver",
46
- "PsychicHttp"
53
+ "psychichttpserver",
54
+ "espasyncwebserver"
47
55
  ],
48
56
  "devDependencies": {
49
57
  "@types/mime-types": "^2.1.4",
50
- "@types/node": "^20.11.17",
51
- "@typescript-eslint/eslint-plugin": "^7.0.1",
52
- "@typescript-eslint/parser": "^7.0.1",
58
+ "@types/node": "^20.11.19",
59
+ "@typescript-eslint/eslint-plugin": "^7.0.2",
60
+ "@typescript-eslint/parser": "^7.0.2",
53
61
  "eslint": "^8.56.0",
54
62
  "eslint-config-prettier": "^9.1.0",
55
63
  "eslint-plugin-simple-import-sort": "^12.0.0",
@@ -62,6 +70,7 @@
62
70
  },
63
71
  "dependencies": {
64
72
  "glob": "^10.3.10",
73
+ "handlebars": "^4.7.8",
65
74
  "mime-types": "^2.1.35",
66
75
  "ts-command-line-args": "^2.5.1"
67
76
  }