svelteesp32 1.15.0 → 1.16.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
@@ -75,7 +75,8 @@ void setup() {
75
75
 
76
76
  ## What's New
77
77
 
78
- - **v1.15.0** — `--base-path` for multiple frontends (e.g., `/admin`, `/app`)
78
+ - **v1.16.0** — Size budget constraints (`--maxsize`, `--maxgzipsize`)
79
+ - **v1.15.0** — `--basepath` for multiple frontends (e.g., `/admin`, `/app`)
79
80
  - **v1.13.0** — npm package variable interpolation in RC files
80
81
  - **v1.12.0** — RC file configuration support
81
82
  - **v1.11.0** — File exclusion patterns
@@ -301,10 +302,10 @@ Fine-tune how browsers cache your content:
301
302
 
302
303
  Your `index.html` is automatically served at the root URL — just like any web server. Visit `http://esp32.local/` and your app loads.
303
304
 
304
- **API-only projects?** Skip index validation with `--no-index-check`:
305
+ **API-only projects?** Skip index validation with `--noindexcheck`:
305
306
 
306
307
  ```bash
307
- npx svelteesp32 -e psychic -s ./dist -o ./output.h --no-index-check
308
+ npx svelteesp32 -e psychic -s ./dist -o ./output.h --noindexcheck
308
309
  ```
309
310
 
310
311
  ### File Exclusion
@@ -335,8 +336,8 @@ Excluded 3 file(s):
335
336
  Serve multiple web apps from one ESP32 using URL prefixes:
336
337
 
337
338
  ```bash
338
- npx svelteesp32 -s ./admin-dist -o ./admin.h --base-path=/admin
339
- npx svelteesp32 -s ./user-dist -o ./user.h --base-path=/app
339
+ npx svelteesp32 -s ./admin-dist -o ./admin.h --basepath=/admin
340
+ npx svelteesp32 -s ./user-dist -o ./user.h --basepath=/app
340
341
  ```
341
342
 
342
343
  ```c
@@ -402,22 +403,24 @@ Called for every response (200 = content served, 304 = cache hit).
402
403
 
403
404
  ## CLI Reference
404
405
 
405
- | Option | Description | Default |
406
- | ------------------ | ------------------------------------------------- | ----------------------- |
407
- | `-s` | Source folder with compiled web files | (required) |
408
- | `-e` | Web server engine (psychic/psychic2/async/espidf) | `psychic` |
409
- | `-o` | Output header file path | `svelteesp32.h` |
410
- | `--etag` | ETag caching (true/false/compiler) | `false` |
411
- | `--gzip` | Gzip compression (true/false/compiler) | `true` |
412
- | `--exclude` | Exclude files by glob pattern | System files |
413
- | `--base-path` | URL prefix for all routes | (none) |
414
- | `--cachetime` | Cache-Control max-age in seconds | `0` |
415
- | `--version` | Version string in header | (none) |
416
- | `--define` | C++ define prefix | `SVELTEESP32` |
417
- | `--espmethod` | Init function name | `initSvelteStaticFiles` |
418
- | `--config` | Custom RC file path | `.svelteesp32rc.json` |
419
- | `--no-index-check` | Skip index.html validation | `false` |
420
- | `-h` | Show help | |
406
+ | Option | Description | Default |
407
+ | ---------------- | ------------------------------------------------- | ----------------------- |
408
+ | `-s` | Source folder with compiled web files | (required) |
409
+ | `-e` | Web server engine (psychic/psychic2/async/espidf) | `psychic` |
410
+ | `-o` | Output header file path | `svelteesp32.h` |
411
+ | `--etag` | ETag caching (true/false/compiler) | `false` |
412
+ | `--gzip` | Gzip compression (true/false/compiler) | `true` |
413
+ | `--exclude` | Exclude files by glob pattern | System files |
414
+ | `--basepath` | URL prefix for all routes | (none) |
415
+ | `--maxsize` | Max total uncompressed size (e.g., `400k`, `1m`) | (none) |
416
+ | `--maxgzipsize` | Max total gzip size (e.g., `150k`, `500k`) | (none) |
417
+ | `--cachetime` | Cache-Control max-age in seconds | `0` |
418
+ | `--version` | Version string in header | (none) |
419
+ | `--define` | C++ define prefix | `SVELTEESP32` |
420
+ | `--espmethod` | Init function name | `initSvelteStaticFiles` |
421
+ | `--config` | Custom RC file path | `.svelteesp32rc.json` |
422
+ | `--noindexcheck` | Skip index.html validation | `false` |
423
+ | `-h` | Show help | |
421
424
 
422
425
  ---
423
426
 
@@ -432,7 +435,11 @@ Store your settings in `.svelteesp32rc.json` for zero-argument builds:
432
435
  "outputfile": "./esp32/svelteesp32.h",
433
436
  "etag": "true",
434
437
  "gzip": "true",
435
- "exclude": ["*.map", "*.md"]
438
+ "exclude": ["*.map", "*.md"],
439
+ "basepath": "/ui",
440
+ "maxsize": "400k",
441
+ "maxgzipsize": "150k",
442
+ "noindexcheck": false
436
443
  }
437
444
  ```
438
445
 
@@ -11,6 +11,8 @@ interface ICopyFilesArguments {
11
11
  version: string;
12
12
  exclude: string[];
13
13
  basePath: string;
14
+ maxSize?: number;
15
+ maxGzipSize?: number;
14
16
  noIndexCheck?: boolean;
15
17
  help?: boolean;
16
18
  }
@@ -26,11 +28,15 @@ interface IRcFileConfig {
26
28
  created?: boolean;
27
29
  version?: string;
28
30
  exclude?: string[];
29
- basePath?: string;
31
+ basepath?: string;
32
+ maxsize?: number | string;
33
+ maxgzipsize?: number | string;
34
+ noindexcheck?: boolean;
30
35
  }
36
+ declare function parseSize(value: string, name: string): number;
31
37
  declare function getNpmPackageVariable(packageJson: Record<string, unknown>, variableName: string): string | undefined;
32
38
  declare function hasNpmVariables(config: IRcFileConfig): boolean;
33
39
  declare function interpolateNpmVariables(config: IRcFileConfig, rcFilePath: string): IRcFileConfig;
34
- export { getNpmPackageVariable, hasNpmVariables, interpolateNpmVariables };
40
+ export { getNpmPackageVariable, hasNpmVariables, interpolateNpmVariables, parseSize };
35
41
  export declare function formatConfiguration(cmdLine: ICopyFilesArguments): string;
36
42
  export declare const cmdLine: ICopyFilesArguments;
@@ -7,6 +7,7 @@ exports.cmdLine = void 0;
7
7
  exports.getNpmPackageVariable = getNpmPackageVariable;
8
8
  exports.hasNpmVariables = hasNpmVariables;
9
9
  exports.interpolateNpmVariables = interpolateNpmVariables;
10
+ exports.parseSize = parseSize;
10
11
  exports.formatConfiguration = formatConfiguration;
11
12
  const node_fs_1 = require("node:fs");
12
13
  const node_os_1 = require("node:os");
@@ -34,7 +35,9 @@ Options:
34
35
  --cachetime <seconds> max-age cache time in seconds (default: 0)
35
36
  --exclude <pattern> Exclude files matching glob pattern (repeatable or comma-separated)
36
37
  Examples: --exclude="*.map" --exclude="test/**/*.ts"
37
- --base-path <path> URL prefix for all routes (e.g., "/ui") (default: "")
38
+ --basepath <path> URL prefix for all routes (e.g., "/ui") (default: "")
39
+ --maxsize <size> Maximum total uncompressed size (e.g., 400k, 1.5m, 409600)
40
+ --maxgzipsize <size> Maximum total gzip size (e.g., 150k, 1m, 153600)
38
41
  -h, --help Shows this help
39
42
 
40
43
  RC File:
@@ -49,7 +52,11 @@ RC File:
49
52
  "outputfile": "./output.h",
50
53
  "etag": "true",
51
54
  "gzip": "true",
52
- "exclude": ["*.map", "*.md"]
55
+ "exclude": ["*.map", "*.md"],
56
+ "basepath": "/ui",
57
+ "maxsize": "400k",
58
+ "maxgzipsize": "150k",
59
+ "noindexcheck": false
53
60
  }
54
61
 
55
62
  CLI arguments override RC file values.
@@ -78,6 +85,25 @@ function validateBasePath(value) {
78
85
  throw new Error(`basePath must not contain //: ${value}`);
79
86
  return value;
80
87
  }
88
+ function parseSize(value, name) {
89
+ const trimmed = value.trim();
90
+ const match = trimmed.match(/^(\d+(?:\.\d+)?)\s*([KMkm])?$/);
91
+ if (!match || !match[1])
92
+ throw new Error(`${name} must be a positive number with optional k/K (×1024) or m/M (×1024²) suffix: ${value}`);
93
+ const numericPart = Number.parseFloat(match[1]);
94
+ const suffix = match[2]?.toLowerCase();
95
+ let bytes;
96
+ if (suffix === 'k')
97
+ bytes = numericPart * 1024;
98
+ else if (suffix === 'm')
99
+ bytes = numericPart * 1024 * 1024;
100
+ else
101
+ bytes = numericPart;
102
+ bytes = Math.round(bytes);
103
+ if (bytes <= 0 || !Number.isFinite(bytes))
104
+ throw new Error(`${name} must be a positive integer: ${value}`);
105
+ return bytes;
106
+ }
81
107
  const DEFAULT_EXCLUDE_PATTERNS = [
82
108
  '.DS_Store',
83
109
  'Thumbs.db',
@@ -148,7 +174,7 @@ function hasNpmVariables(config) {
148
174
  checkStringForNpmVariable(config.espmethod) ||
149
175
  checkStringForNpmVariable(config.define) ||
150
176
  checkStringForNpmVariable(config.version) ||
151
- checkStringForNpmVariable(config.basePath) ||
177
+ checkStringForNpmVariable(config.basepath) ||
152
178
  (Array.isArray(config.exclude) && config.exclude.some((pattern) => checkStringForNpmVariable(pattern))) ||
153
179
  false);
154
180
  }
@@ -168,8 +194,8 @@ function interpolateNpmVariables(config, rcFilePath) {
168
194
  affectedFields.push('espmethod');
169
195
  if (config.define?.includes('$npm_package_'))
170
196
  affectedFields.push('define');
171
- if (config.basePath?.includes('$npm_package_'))
172
- affectedFields.push('basePath');
197
+ if (config.basepath?.includes('$npm_package_'))
198
+ affectedFields.push('basepath');
173
199
  if (config.exclude)
174
200
  for (const [index, pattern] of config.exclude.entries())
175
201
  if (pattern.includes('$npm_package_'))
@@ -194,8 +220,8 @@ function interpolateNpmVariables(config, rcFilePath) {
194
220
  result.define = interpolateString(result.define);
195
221
  if (result.version)
196
222
  result.version = interpolateString(result.version);
197
- if (result.basePath)
198
- result.basePath = interpolateString(result.basePath);
223
+ if (result.basepath)
224
+ result.basepath = interpolateString(result.basepath);
199
225
  if (result.exclude)
200
226
  result.exclude = result.exclude.map((pattern) => interpolateString(pattern));
201
227
  return result;
@@ -229,7 +255,10 @@ function validateRcConfig(config, rcPath) {
229
255
  'created',
230
256
  'version',
231
257
  'exclude',
232
- 'basePath'
258
+ 'basepath',
259
+ 'maxsize',
260
+ 'maxgzipsize',
261
+ 'noindexcheck'
233
262
  ]);
234
263
  for (const key of Object.keys(configObject))
235
264
  if (!validKeys.has(key))
@@ -250,6 +279,32 @@ function validateRcConfig(config, rcPath) {
250
279
  if (typeof pattern !== 'string')
251
280
  throw new TypeError('All exclude patterns must be strings');
252
281
  }
282
+ if (configObject['maxsize'] !== undefined) {
283
+ const value = configObject['maxsize'];
284
+ if (typeof value === 'string')
285
+ try {
286
+ configObject['maxsize'] = parseSize(value, 'maxsize');
287
+ }
288
+ catch {
289
+ throw new TypeError(`Invalid maxsize in RC file: ${value} (must be a positive number with optional k/m suffix)`);
290
+ }
291
+ else if (typeof value !== 'number' || Number.isNaN(value) || value <= 0)
292
+ throw new TypeError(`Invalid maxsize in RC file: ${value} (must be a positive number)`);
293
+ }
294
+ if (configObject['maxgzipsize'] !== undefined) {
295
+ const value = configObject['maxgzipsize'];
296
+ if (typeof value === 'string')
297
+ try {
298
+ configObject['maxgzipsize'] = parseSize(value, 'maxgzipsize');
299
+ }
300
+ catch {
301
+ throw new TypeError(`Invalid maxgzipsize in RC file: ${value} (must be a positive number with optional k/m suffix)`);
302
+ }
303
+ else if (typeof value !== 'number' || Number.isNaN(value) || value <= 0)
304
+ throw new TypeError(`Invalid maxgzipsize in RC file: ${value} (must be a positive number)`);
305
+ }
306
+ if (configObject['noindexcheck'] !== undefined && typeof configObject['noindexcheck'] !== 'boolean')
307
+ throw new TypeError(`Invalid noindexcheck in RC file: ${configObject['noindexcheck']} (must be boolean)`);
253
308
  return configObject;
254
309
  }
255
310
  function parseArguments() {
@@ -305,8 +360,14 @@ function parseArguments() {
305
360
  result.espmethod = rcConfig.espmethod;
306
361
  if (rcConfig.define)
307
362
  result.define = rcConfig.define;
308
- if (rcConfig.basePath !== undefined)
309
- result.basePath = validateBasePath(rcConfig.basePath);
363
+ if (rcConfig.basepath !== undefined)
364
+ result.basePath = validateBasePath(rcConfig.basepath);
365
+ if (rcConfig.maxsize !== undefined)
366
+ result.maxSize = rcConfig.maxsize;
367
+ if (rcConfig.maxgzipsize !== undefined)
368
+ result.maxGzipSize = rcConfig.maxgzipsize;
369
+ if (rcConfig.noindexcheck !== undefined)
370
+ result.noIndexCheck = rcConfig.noindexcheck;
310
371
  if (rcConfig.exclude && rcConfig.exclude.length > 0)
311
372
  result.exclude = [...rcConfig.exclude];
312
373
  const cliExclude = [];
@@ -363,9 +424,15 @@ function parseArguments() {
363
424
  cliExclude.push(...patterns);
364
425
  break;
365
426
  }
366
- case 'base-path':
427
+ case 'basepath':
367
428
  result.basePath = validateBasePath(value);
368
429
  break;
430
+ case 'maxsize':
431
+ result.maxSize = parseSize(value, '--maxsize');
432
+ break;
433
+ case 'maxgzipsize':
434
+ result.maxGzipSize = parseSize(value, '--maxgzipsize');
435
+ break;
369
436
  default:
370
437
  throw new Error(`Unknown flag: ${flag}`);
371
438
  }
@@ -375,7 +442,7 @@ function parseArguments() {
375
442
  result.created = true;
376
443
  continue;
377
444
  }
378
- if (argument === '--no-index-check') {
445
+ if (argument === '--noindexcheck') {
379
446
  result.noIndexCheck = true;
380
447
  continue;
381
448
  }
@@ -458,10 +525,18 @@ function parseArguments() {
458
525
  index++;
459
526
  break;
460
527
  }
461
- case 'base-path':
528
+ case 'basepath':
462
529
  result.basePath = validateBasePath(nextArgument);
463
530
  index++;
464
531
  break;
532
+ case 'maxsize':
533
+ result.maxSize = parseSize(nextArgument, '--maxsize');
534
+ index++;
535
+ break;
536
+ case 'maxgzipsize':
537
+ result.maxGzipSize = parseSize(nextArgument, '--maxgzipsize');
538
+ index++;
539
+ break;
465
540
  default:
466
541
  throw new Error(`Unknown flag: --${flag}`);
467
542
  }
@@ -496,6 +571,10 @@ function formatConfiguration(cmdLine) {
496
571
  parts.push(`define=${cmdLine.define}`);
497
572
  if (cmdLine.basePath)
498
573
  parts.push(`basePath=${cmdLine.basePath}`);
574
+ if (cmdLine.maxSize !== undefined)
575
+ parts.push(`maxSize=${cmdLine.maxSize}`);
576
+ if (cmdLine.maxGzipSize !== undefined)
577
+ parts.push(`maxGzipSize=${cmdLine.maxGzipSize}`);
499
578
  if (cmdLine.exclude.length > 0)
500
579
  parts.push(`exclude=[${cmdLine.exclude.join(', ')}]`);
501
580
  return parts.join(' ');
@@ -1,4 +1,5 @@
1
1
  export declare function getMissingIndexError(engine: string): string;
2
2
  export declare function getInvalidEngineError(attempted: string): string;
3
3
  export declare function getSourcepathNotFoundError(sourcepath: string, reason: 'not_found' | 'not_directory'): string;
4
+ export declare function getSizeBudgetExceededError(type: 'size' | 'gzipSize', limit: number, actual: number): string;
4
5
  export declare function getMaxUriHandlersHint(engine: string, routeCount: number): string;
@@ -6,6 +6,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.getMissingIndexError = getMissingIndexError;
7
7
  exports.getInvalidEngineError = getInvalidEngineError;
8
8
  exports.getSourcepathNotFoundError = getSourcepathNotFoundError;
9
+ exports.getSizeBudgetExceededError = getSizeBudgetExceededError;
9
10
  exports.getMaxUriHandlersHint = getMaxUriHandlersHint;
10
11
  const node_path_1 = __importDefault(require("node:path"));
11
12
  const consoleColor_1 = require("./consoleColor");
@@ -45,7 +46,7 @@ How to fix (for ${getEngineName(engine)}):
45
46
  ${hint}
46
47
 
47
48
  Alternative:
48
- If you use a different entry point (e.g., main.html), you can add --no-index-check flag,
49
+ If you use a different entry point (e.g., main.html), you can add --noindexcheck flag,
49
50
  but users must navigate to http://your-esp32/main.html explicitly.`);
50
51
  }
51
52
  function getInvalidEngineError(attempted) {
@@ -109,6 +110,32 @@ How to fix:
109
110
  Current directory: ${currentDirectory}
110
111
  Attempted path: ${resolvedPath} (resolved)`);
111
112
  }
113
+ function getSizeBudgetExceededError(type, limit, actual) {
114
+ const typeLabel = type === 'size' ? 'Uncompressed' : 'Gzip';
115
+ const flagName = type === 'size' ? '--maxsize' : '--maxgzipsize';
116
+ const overage = actual - limit;
117
+ const overagePercent = Math.round((overage / limit) * 100);
118
+ return ((0, consoleColor_1.redLog)(`[ERROR] ${typeLabel} size budget exceeded`) +
119
+ `
120
+
121
+ Budget: ${limit.toLocaleString()} bytes
122
+ Actual: ${actual.toLocaleString()} bytes
123
+ Overage: ${overage.toLocaleString()} bytes (+${overagePercent}%)
124
+
125
+ Why this matters:
126
+ Size budgets help prevent frontend bloat and ensure your application
127
+ fits within ESP32 flash memory constraints.
128
+
129
+ How to fix:
130
+ 1. Reduce bundle size (tree-shaking, code splitting, smaller dependencies)
131
+ 2. Optimize assets (compress images, minify CSS/JS)
132
+ 3. Remove unused files with --exclude patterns
133
+ 4. Increase the budget: ${flagName}=${actual}
134
+
135
+ CI integration:
136
+ This non-zero exit code allows build pipelines to fail early when
137
+ the frontend exceeds allocated flash space.`);
138
+ }
112
139
  function getMaxUriHandlersHint(engine, routeCount) {
113
140
  const recommended = routeCount + 5;
114
141
  const hints = {
package/dist/index.js CHANGED
@@ -84,6 +84,14 @@ for (const [originalFilename, content] of files) {
84
84
  }
85
85
  console.log('');
86
86
  filesByExtension.sort((left, right) => left.extension.localeCompare(right.extension));
87
+ if (commandLine_1.cmdLine.maxSize !== undefined && summary.size > commandLine_1.cmdLine.maxSize) {
88
+ console.error((0, errorMessages_1.getSizeBudgetExceededError)('size', commandLine_1.cmdLine.maxSize, summary.size));
89
+ process.exit(1);
90
+ }
91
+ if (commandLine_1.cmdLine.maxGzipSize !== undefined && summary.gzipsize > commandLine_1.cmdLine.maxGzipSize) {
92
+ console.error((0, errorMessages_1.getSizeBudgetExceededError)('gzipSize', commandLine_1.cmdLine.maxGzipSize, summary.gzipsize));
93
+ process.exit(1);
94
+ }
87
95
  const cppFile = (0, cppCode_1.getCppCode)(sources, filesByExtension);
88
96
  (0, node_fs_1.mkdirSync)(node_path_1.default.normalize(node_path_1.default.dirname(commandLine_1.cmdLine.outputfile)), { recursive: true });
89
97
  (0, node_fs_1.writeFileSync)(commandLine_1.cmdLine.outputfile, cppFile, { flush: true, encoding: 'utf8' });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelteesp32",
3
- "version": "1.15.0",
3
+ "version": "1.16.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",