svelteesp32 1.16.0 → 2.0.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
@@ -6,6 +6,10 @@
6
6
 
7
7
  [Changelog](CHANGELOG.md)
8
8
 
9
+ <p align="center">
10
+ <img src="svelteesp32.png" alt="svelteesp32" />
11
+ </p>
12
+
9
13
  ---
10
14
 
11
15
  ## Why SvelteESP32?
@@ -21,7 +25,7 @@
21
25
  - **Smart Caching** — Built-in SHA256 ETags deliver HTTP 304 responses, slashing bandwidth on constrained devices.
22
26
  - **CI/CD Ready** — Simple npm package that slots into any build pipeline.
23
27
  - **Zero Runtime Overhead** — Data served directly from flash. No filesystem reads, no RAM allocation.
24
- - **4 Web Server Engines** — PsychicHttp V1/V2, ESPAsyncWebServer, and native ESP-IDF supported.
28
+ - **3 Web Server Engines** — PsychicHttpServer, ESPAsyncWebServer, and native ESP-IDF supported.
25
29
 
26
30
  ---
27
31
 
@@ -75,12 +79,13 @@ void setup() {
75
79
 
76
80
  ## What's New
77
81
 
82
+ - **v2.0.0** — **BREAKING**: PsychicHttpServer V2 is now the default `psychic` engine. The `psychic2` engine has been removed. Dry run mode, C++ identifier validation, improved MIME type warnings
83
+ - **v1.16.0** — Size budget constraints (`--maxsize`, `--maxgzipsize`)
78
84
  - **v1.15.0** — `--basepath` for multiple frontends (e.g., `/admin`, `/app`)
79
85
  - **v1.13.0** — npm package variable interpolation in RC files
80
86
  - **v1.12.0** — RC file configuration support
81
87
  - **v1.11.0** — File exclusion patterns
82
88
  - **v1.9.0** — Native ESP-IDF engine
83
- - **v1.5.0** — PsychicHttp V2 support
84
89
 
85
90
  ---
86
91
 
@@ -107,9 +112,6 @@ Choose your web server engine:
107
112
  # PsychicHttpServer (recommended for ESP32)
108
113
  npx svelteesp32 -e psychic -s ./dist -o ./esp32/svelteesp32.h --etag=true
109
114
 
110
- # PsychicHttpServer V2
111
- npx svelteesp32 -e psychic2 -s ./dist -o ./esp32/svelteesp32.h --etag=true
112
-
113
115
  # ESPAsyncWebServer (ESP32 + ESP8266)
114
116
  npx svelteesp32 -e async -s ./dist -o ./esp32/svelteesp32.h --etag=true
115
117
 
@@ -140,7 +142,7 @@ Watch your files get optimized in real-time:
140
142
 
141
143
  ### ESP32 Integration
142
144
 
143
- **PsychicHttpServer (Recommended)**
145
+ **PsychicHttpServer V2 (Recommended)**
144
146
 
145
147
  ```c
146
148
  #include <PsychicHttp.h>
@@ -183,14 +185,14 @@ void app_main() {
183
185
  }
184
186
  ```
185
187
 
186
- Working examples: [Arduino/PlatformIO](demo/esp32) | [ESP-IDF](demo/esp32idf)
188
+ Working examples with LED control via web interface: [Arduino/PlatformIO](demo/esp32) | [ESP-IDF](demo/esp32idf)
187
189
 
188
190
  ### What Gets Generated
189
191
 
190
192
  The generated header file includes everything your ESP needs:
191
193
 
192
194
  ```c
193
- //engine: PsychicHttpServer
195
+ //engine: PsychicHttpServer V2
194
196
  //config: engine=psychic sourcepath=./dist outputfile=./output.h etag=true gzip=true cachetime=0 espmethod=initSvelteStaticFiles define=SVELTEESP32
195
197
  //
196
198
  #define SVELTEESP32_COUNT 5
@@ -231,23 +233,21 @@ const size_t SVELTEESP32_FILE_COUNT = sizeof(SVELTEESP32_FILES) / sizeof(SVELTEE
231
233
  extern "C" void __attribute__((weak)) SVELTEESP32_onFileServed(const char* path, int statusCode) {}
232
234
 
233
235
  void initSvelteStaticFiles(PsychicHttpServer * server) {
234
- server->on("/assets/index-KwubEIf-.js", HTTP_GET, [](PsychicRequest * request) {
236
+ server->on("/assets/index-KwubEIf-.js", HTTP_GET, [](PsychicRequest * request, PsychicResponse * response) {
235
237
  if (request->hasHeader("If-None-Match") &&
236
238
  request->header("If-None-Match").equals(etag_assets_index_KwubEIf__js)) {
237
- PsychicResponse response304(request);
238
- response304.setCode(304);
239
+ response->setCode(304);
239
240
  SVELTEESP32_onFileServed("/assets/index-KwubEIf-.js", 304);
240
- return response304.send();
241
+ return response->send();
241
242
  }
242
243
 
243
- PsychicResponse response(request);
244
- response.setContentType("text/javascript");
245
- response.addHeader("Content-Encoding", "gzip");
246
- response.addHeader("Cache-Control", "no-cache");
247
- response.addHeader("ETag", etag_assets_index_KwubEIf__js);
248
- response.setContent(datagzip_assets_index_KwubEIf__js, 12547);
244
+ response->setContentType("text/javascript");
245
+ response->addHeader("Content-Encoding", "gzip");
246
+ response->addHeader("Cache-Control", "no-cache");
247
+ response->addHeader("ETag", etag_assets_index_KwubEIf__js);
248
+ response->setContent(datagzip_assets_index_KwubEIf__js, 12547);
249
249
  SVELTEESP32_onFileServed("/assets/index-KwubEIf-.js", 200);
250
- return response.send();
250
+ return response->send();
251
251
  });
252
252
 
253
253
  // ... more routes
@@ -258,14 +258,13 @@ void initSvelteStaticFiles(PsychicHttpServer * server) {
258
258
 
259
259
  ## Supported Web Server Engines
260
260
 
261
- | Engine | Flag | Best For | Platform |
262
- | --------------------- | ------------- | ---------------------------- | --------------- |
263
- | **PsychicHttp V1** | `-e psychic` | Maximum performance | ESP32 only |
264
- | **PsychicHttp V2** | `-e psychic2` | Modern API + performance | ESP32 only |
265
- | **ESPAsyncWebServer** | `-e async` | Cross-platform compatibility | ESP32 + ESP8266 |
266
- | **Native ESP-IDF** | `-e espidf` | Pure ESP-IDF projects | ESP32 only |
261
+ | Engine | Flag | Best For | Platform |
262
+ | ------------------------ | ------------ | ---------------------------- | --------------- |
263
+ | **PsychicHttpServer V2** | `-e psychic` | Maximum performance | ESP32 only |
264
+ | **ESPAsyncWebServer** | `-e async` | Cross-platform compatibility | ESP32 + ESP8266 |
265
+ | **Native ESP-IDF** | `-e espidf` | Pure ESP-IDF projects | ESP32 only |
267
266
 
268
- **Recommendation:** For ESP32-only projects, use PsychicHttpServer for the fastest, most stable experience.
267
+ **Recommendation:** For ESP32-only projects, use PsychicHttpServer V2 (`-e psychic`) for the fastest, most stable experience.
269
268
 
270
269
  **Note:** For PsychicHttp, configure `server.config.max_uri_handlers` to match your file count.
271
270
 
@@ -288,7 +287,7 @@ Reduce bandwidth dramatically with HTTP 304 "Not Modified" responses. When a bro
288
287
  - **Minimal overhead** — adds ~1-3% code size for significant bandwidth savings
289
288
  - **Compiler mode** — use `--etag=compiler` and control via `-D SVELTEESP32_ENABLE_ETAG`
290
289
 
291
- All four engines support full ETag validation.
290
+ All three engines support full ETag validation.
292
291
 
293
292
  ### Browser Cache Control
294
293
 
@@ -402,22 +401,25 @@ Called for every response (200 = content served, 304 = cache hit).
402
401
 
403
402
  ## CLI Reference
404
403
 
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
- | `--basepath` | 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
- | `--noindexcheck` | Skip index.html validation | `false` |
420
- | `-h` | Show help | |
404
+ | Option | Description | Default |
405
+ | ---------------- | ------------------------------------------------ | ----------------------- |
406
+ | `-s` | Source folder with compiled web files | (required) |
407
+ | `-e` | Web server engine (psychic/async/espidf) | `psychic` |
408
+ | `-o` | Output header file path | `svelteesp32.h` |
409
+ | `--etag` | ETag caching (true/false/compiler) | `false` |
410
+ | `--gzip` | Gzip compression (true/false/compiler) | `true` |
411
+ | `--exclude` | Exclude files by glob pattern | System files |
412
+ | `--basepath` | URL prefix for all routes | (none) |
413
+ | `--maxsize` | Max total uncompressed size (e.g., `400k`, `1m`) | (none) |
414
+ | `--maxgzipsize` | Max total gzip size (e.g., `150k`, `500k`) | (none) |
415
+ | `--cachetime` | Cache-Control max-age in seconds | `0` |
416
+ | `--version` | Version string in header | (none) |
417
+ | `--define` | C++ define prefix | `SVELTEESP32` |
418
+ | `--espmethod` | Init function name | `initSvelteStaticFiles` |
419
+ | `--config` | Custom RC file path | `.svelteesp32rc.json` |
420
+ | `--dryrun` | Show summary without writing output | `false` |
421
+ | `--noindexcheck` | Skip index.html validation | `false` |
422
+ | `-h` | Show help | |
421
423
 
422
424
  ---
423
425
 
@@ -432,7 +434,12 @@ Store your settings in `.svelteesp32rc.json` for zero-argument builds:
432
434
  "outputfile": "./esp32/svelteesp32.h",
433
435
  "etag": "true",
434
436
  "gzip": "true",
435
- "exclude": ["*.map", "*.md"]
437
+ "exclude": ["*.map", "*.md"],
438
+ "basepath": "/ui",
439
+ "maxsize": "400k",
440
+ "maxgzipsize": "150k",
441
+ "noindexcheck": false,
442
+ "dryrun": false
436
443
  }
437
444
  ```
438
445
 
@@ -1,5 +1,5 @@
1
1
  interface ICopyFilesArguments {
2
- engine: 'psychic' | 'psychic2' | 'async' | 'espidf';
2
+ engine: 'psychic' | 'async' | 'espidf';
3
3
  sourcepath: string;
4
4
  outputfile: string;
5
5
  espmethod: string;
@@ -14,10 +14,11 @@ interface ICopyFilesArguments {
14
14
  maxSize?: number;
15
15
  maxGzipSize?: number;
16
16
  noIndexCheck?: boolean;
17
+ dryRun?: boolean;
17
18
  help?: boolean;
18
19
  }
19
20
  interface IRcFileConfig {
20
- engine?: 'psychic' | 'psychic2' | 'async' | 'espidf';
21
+ engine?: 'psychic' | 'async' | 'espidf';
21
22
  sourcepath?: string;
22
23
  outputfile?: string;
23
24
  espmethod?: string;
@@ -28,14 +29,17 @@ interface IRcFileConfig {
28
29
  created?: boolean;
29
30
  version?: string;
30
31
  exclude?: string[];
31
- basePath?: string;
32
- maxSize?: number | string;
33
- maxGzipSize?: number | string;
32
+ basepath?: string;
33
+ maxsize?: number | string;
34
+ maxgzipsize?: number | string;
35
+ noindexcheck?: boolean;
36
+ dryrun?: boolean;
34
37
  }
38
+ declare function validateCppIdentifier(value: string, name: string): string;
35
39
  declare function parseSize(value: string, name: string): number;
36
40
  declare function getNpmPackageVariable(packageJson: Record<string, unknown>, variableName: string): string | undefined;
37
41
  declare function hasNpmVariables(config: IRcFileConfig): boolean;
38
42
  declare function interpolateNpmVariables(config: IRcFileConfig, rcFilePath: string): IRcFileConfig;
39
- export { getNpmPackageVariable, hasNpmVariables, interpolateNpmVariables, parseSize };
43
+ export { getNpmPackageVariable, hasNpmVariables, interpolateNpmVariables, parseSize, validateCppIdentifier };
40
44
  export declare function formatConfiguration(cmdLine: ICopyFilesArguments): string;
41
45
  export declare const cmdLine: ICopyFilesArguments;
@@ -8,6 +8,7 @@ exports.getNpmPackageVariable = getNpmPackageVariable;
8
8
  exports.hasNpmVariables = hasNpmVariables;
9
9
  exports.interpolateNpmVariables = interpolateNpmVariables;
10
10
  exports.parseSize = parseSize;
11
+ exports.validateCppIdentifier = validateCppIdentifier;
11
12
  exports.formatConfiguration = formatConfiguration;
12
13
  const node_fs_1 = require("node:fs");
13
14
  const node_os_1 = require("node:os");
@@ -23,7 +24,7 @@ Configuration:
23
24
 
24
25
  Options:
25
26
  -e, --engine <value> The engine for which the include file is created
26
- (psychic|psychic2|async|espidf) (default: "psychic")
27
+ (psychic|async|espidf) (default: "psychic")
27
28
  -s, --sourcepath <path> Source dist folder contains compiled web files (required)
28
29
  -o, --outputfile <path> Generated output file with path (default: "svelteesp32.h")
29
30
  --etag <value> Use ETAG header for cache (true|false|compiler) (default: "false")
@@ -38,6 +39,7 @@ Options:
38
39
  --basepath <path> URL prefix for all routes (e.g., "/ui") (default: "")
39
40
  --maxsize <size> Maximum total uncompressed size (e.g., 400k, 1.5m, 409600)
40
41
  --maxgzipsize <size> Maximum total gzip size (e.g., 150k, 1m, 153600)
42
+ --dryrun Show summary without writing the output file (default: false)
41
43
  -h, --help Shows this help
42
44
 
43
45
  RC File:
@@ -52,7 +54,11 @@ RC File:
52
54
  "outputfile": "./output.h",
53
55
  "etag": "true",
54
56
  "gzip": "true",
55
- "exclude": ["*.map", "*.md"]
57
+ "exclude": ["*.map", "*.md"],
58
+ "basepath": "/ui",
59
+ "maxsize": "400k",
60
+ "maxgzipsize": "150k",
61
+ "noindexcheck": false
56
62
  }
57
63
 
58
64
  CLI arguments override RC file values.
@@ -60,7 +66,7 @@ RC File:
60
66
  process.exit(0);
61
67
  }
62
68
  function validateEngine(value) {
63
- if (value === 'psychic' || value === 'psychic2' || value === 'async' || value === 'espidf')
69
+ if (value === 'psychic' || value === 'async' || value === 'espidf')
64
70
  return value;
65
71
  console.error((0, errorMessages_1.getInvalidEngineError)(value));
66
72
  process.exit(1);
@@ -70,6 +76,11 @@ function validateTriState(value, name) {
70
76
  return value;
71
77
  throw new Error(`Invalid ${name}: ${value}`);
72
78
  }
79
+ function validateCppIdentifier(value, name) {
80
+ if (!/^[A-Z_a-z]\w*$/.test(value))
81
+ throw new Error(`${name} must be a valid C++ identifier (letters, digits, underscores, not starting with a digit): ${value}`);
82
+ return value;
83
+ }
73
84
  function validateBasePath(value) {
74
85
  if (value === '')
75
86
  return value;
@@ -170,7 +181,7 @@ function hasNpmVariables(config) {
170
181
  checkStringForNpmVariable(config.espmethod) ||
171
182
  checkStringForNpmVariable(config.define) ||
172
183
  checkStringForNpmVariable(config.version) ||
173
- checkStringForNpmVariable(config.basePath) ||
184
+ checkStringForNpmVariable(config.basepath) ||
174
185
  (Array.isArray(config.exclude) && config.exclude.some((pattern) => checkStringForNpmVariable(pattern))) ||
175
186
  false);
176
187
  }
@@ -190,8 +201,8 @@ function interpolateNpmVariables(config, rcFilePath) {
190
201
  affectedFields.push('espmethod');
191
202
  if (config.define?.includes('$npm_package_'))
192
203
  affectedFields.push('define');
193
- if (config.basePath?.includes('$npm_package_'))
194
- affectedFields.push('basePath');
204
+ if (config.basepath?.includes('$npm_package_'))
205
+ affectedFields.push('basepath');
195
206
  if (config.exclude)
196
207
  for (const [index, pattern] of config.exclude.entries())
197
208
  if (pattern.includes('$npm_package_'))
@@ -216,8 +227,8 @@ function interpolateNpmVariables(config, rcFilePath) {
216
227
  result.define = interpolateString(result.define);
217
228
  if (result.version)
218
229
  result.version = interpolateString(result.version);
219
- if (result.basePath)
220
- result.basePath = interpolateString(result.basePath);
230
+ if (result.basepath)
231
+ result.basepath = interpolateString(result.basepath);
221
232
  if (result.exclude)
222
233
  result.exclude = result.exclude.map((pattern) => interpolateString(pattern));
223
234
  return result;
@@ -251,9 +262,11 @@ function validateRcConfig(config, rcPath) {
251
262
  'created',
252
263
  'version',
253
264
  'exclude',
254
- 'basePath',
255
- 'maxSize',
256
- 'maxGzipSize'
265
+ 'basepath',
266
+ 'maxsize',
267
+ 'maxgzipsize',
268
+ 'noindexcheck',
269
+ 'dryrun'
257
270
  ]);
258
271
  for (const key of Object.keys(configObject))
259
272
  if (!validKeys.has(key))
@@ -264,9 +277,16 @@ function validateRcConfig(config, rcPath) {
264
277
  configObject['etag'] = validateTriState(configObject['etag'], 'etag');
265
278
  if (configObject['gzip'] !== undefined)
266
279
  configObject['gzip'] = validateTriState(configObject['gzip'], 'gzip');
267
- if (configObject['cachetime'] !== undefined &&
268
- (typeof configObject['cachetime'] !== 'number' || Number.isNaN(configObject['cachetime'])))
269
- throw new TypeError(`Invalid cachetime in RC file: ${configObject['cachetime']}`);
280
+ if (configObject['espmethod'] !== undefined)
281
+ validateCppIdentifier(configObject['espmethod'], 'espmethod');
282
+ if (configObject['define'] !== undefined)
283
+ validateCppIdentifier(configObject['define'], 'define');
284
+ if (configObject['cachetime'] !== undefined) {
285
+ if (typeof configObject['cachetime'] !== 'number' || Number.isNaN(configObject['cachetime']))
286
+ throw new TypeError(`Invalid cachetime in RC file: ${configObject['cachetime']}`);
287
+ if (configObject['cachetime'] < 0)
288
+ throw new TypeError(`Invalid cachetime in RC file: ${configObject['cachetime']} (must be non-negative)`);
289
+ }
270
290
  if (configObject['exclude'] !== undefined) {
271
291
  if (!Array.isArray(configObject['exclude']))
272
292
  throw new TypeError("'exclude' in RC file must be an array");
@@ -274,30 +294,34 @@ function validateRcConfig(config, rcPath) {
274
294
  if (typeof pattern !== 'string')
275
295
  throw new TypeError('All exclude patterns must be strings');
276
296
  }
277
- if (configObject['maxSize'] !== undefined) {
278
- const value = configObject['maxSize'];
297
+ if (configObject['maxsize'] !== undefined) {
298
+ const value = configObject['maxsize'];
279
299
  if (typeof value === 'string')
280
300
  try {
281
- configObject['maxSize'] = parseSize(value, 'maxSize');
301
+ configObject['maxsize'] = parseSize(value, 'maxsize');
282
302
  }
283
303
  catch {
284
- throw new TypeError(`Invalid maxSize in RC file: ${value} (must be a positive number with optional k/m suffix)`);
304
+ throw new TypeError(`Invalid maxsize in RC file: ${value} (must be a positive number with optional k/m suffix)`);
285
305
  }
286
306
  else if (typeof value !== 'number' || Number.isNaN(value) || value <= 0)
287
- throw new TypeError(`Invalid maxSize in RC file: ${value} (must be a positive number)`);
307
+ throw new TypeError(`Invalid maxsize in RC file: ${value} (must be a positive number)`);
288
308
  }
289
- if (configObject['maxGzipSize'] !== undefined) {
290
- const value = configObject['maxGzipSize'];
309
+ if (configObject['maxgzipsize'] !== undefined) {
310
+ const value = configObject['maxgzipsize'];
291
311
  if (typeof value === 'string')
292
312
  try {
293
- configObject['maxGzipSize'] = parseSize(value, 'maxGzipSize');
313
+ configObject['maxgzipsize'] = parseSize(value, 'maxgzipsize');
294
314
  }
295
315
  catch {
296
- throw new TypeError(`Invalid maxGzipSize in RC file: ${value} (must be a positive number with optional k/m suffix)`);
316
+ throw new TypeError(`Invalid maxgzipsize in RC file: ${value} (must be a positive number with optional k/m suffix)`);
297
317
  }
298
318
  else if (typeof value !== 'number' || Number.isNaN(value) || value <= 0)
299
- throw new TypeError(`Invalid maxGzipSize in RC file: ${value} (must be a positive number)`);
319
+ throw new TypeError(`Invalid maxgzipsize in RC file: ${value} (must be a positive number)`);
300
320
  }
321
+ if (configObject['noindexcheck'] !== undefined && typeof configObject['noindexcheck'] !== 'boolean')
322
+ throw new TypeError(`Invalid noindexcheck in RC file: ${configObject['noindexcheck']} (must be boolean)`);
323
+ if (configObject['dryrun'] !== undefined && typeof configObject['dryrun'] !== 'boolean')
324
+ throw new TypeError(`Invalid dryrun in RC file: ${configObject['dryrun']} (must be boolean)`);
301
325
  return configObject;
302
326
  }
303
327
  function parseArguments() {
@@ -353,15 +377,75 @@ function parseArguments() {
353
377
  result.espmethod = rcConfig.espmethod;
354
378
  if (rcConfig.define)
355
379
  result.define = rcConfig.define;
356
- if (rcConfig.basePath !== undefined)
357
- result.basePath = validateBasePath(rcConfig.basePath);
358
- if (rcConfig.maxSize !== undefined)
359
- result.maxSize = rcConfig.maxSize;
360
- if (rcConfig.maxGzipSize !== undefined)
361
- result.maxGzipSize = rcConfig.maxGzipSize;
380
+ if (rcConfig.basepath !== undefined)
381
+ result.basePath = validateBasePath(rcConfig.basepath);
382
+ if (rcConfig.maxsize !== undefined)
383
+ result.maxSize = rcConfig.maxsize;
384
+ if (rcConfig.maxgzipsize !== undefined)
385
+ result.maxGzipSize = rcConfig.maxgzipsize;
386
+ if (rcConfig.noindexcheck !== undefined)
387
+ result.noIndexCheck = rcConfig.noindexcheck;
388
+ if (rcConfig.dryrun !== undefined)
389
+ result.dryRun = rcConfig.dryrun;
362
390
  if (rcConfig.exclude && rcConfig.exclude.length > 0)
363
391
  result.exclude = [...rcConfig.exclude];
364
392
  const cliExclude = [];
393
+ function applyFlag(flagName, value) {
394
+ switch (flagName) {
395
+ case 'config':
396
+ break;
397
+ case 'engine':
398
+ result.engine = validateEngine(value);
399
+ break;
400
+ case 'sourcepath':
401
+ result.sourcepath = value;
402
+ break;
403
+ case 'outputfile':
404
+ result.outputfile = value;
405
+ break;
406
+ case 'etag':
407
+ result.etag = validateTriState(value, 'etag');
408
+ break;
409
+ case 'gzip':
410
+ result.gzip = validateTriState(value, 'gzip');
411
+ break;
412
+ case 'version':
413
+ result.version = value;
414
+ break;
415
+ case 'espmethod':
416
+ result.espmethod = validateCppIdentifier(value, 'espmethod');
417
+ break;
418
+ case 'define':
419
+ result.define = validateCppIdentifier(value, 'define');
420
+ break;
421
+ case 'cachetime':
422
+ result.cachetime = Number.parseInt(value, 10);
423
+ if (Number.isNaN(result.cachetime))
424
+ throw new TypeError(`Invalid cachetime: ${value}`);
425
+ if (result.cachetime < 0)
426
+ throw new TypeError(`Invalid cachetime: ${value} (must be non-negative)`);
427
+ break;
428
+ case 'exclude': {
429
+ const patterns = value
430
+ .split(',')
431
+ .map((p) => p.trim())
432
+ .filter(Boolean);
433
+ cliExclude.push(...patterns);
434
+ break;
435
+ }
436
+ case 'basepath':
437
+ result.basePath = validateBasePath(value);
438
+ break;
439
+ case 'maxsize':
440
+ result.maxSize = parseSize(value, '--maxsize');
441
+ break;
442
+ case 'maxgzipsize':
443
+ result.maxGzipSize = parseSize(value, '--maxgzipsize');
444
+ break;
445
+ default:
446
+ throw new Error(`Unknown flag: --${flagName}`);
447
+ }
448
+ }
365
449
  for (let index = 0; index < arguments_.length; index++) {
366
450
  const argument = arguments_[index];
367
451
  if (!argument)
@@ -374,59 +458,7 @@ function parseArguments() {
374
458
  const value = parts.slice(1).join('=');
375
459
  if (!flag || !value)
376
460
  throw new Error(`Invalid argument format: ${argument}`);
377
- const flagName = flag.slice(2);
378
- switch (flagName) {
379
- case 'config':
380
- break;
381
- case 'engine':
382
- result.engine = validateEngine(value);
383
- break;
384
- case 'sourcepath':
385
- result.sourcepath = value;
386
- break;
387
- case 'outputfile':
388
- result.outputfile = value;
389
- break;
390
- case 'etag':
391
- result.etag = validateTriState(value, 'etag');
392
- break;
393
- case 'gzip':
394
- result.gzip = validateTriState(value, 'gzip');
395
- break;
396
- case 'version':
397
- result.version = value;
398
- break;
399
- case 'espmethod':
400
- result.espmethod = value;
401
- break;
402
- case 'define':
403
- result.define = value;
404
- break;
405
- case 'cachetime':
406
- result.cachetime = Number.parseInt(value, 10);
407
- if (Number.isNaN(result.cachetime))
408
- throw new TypeError(`Invalid cachetime: ${value}`);
409
- break;
410
- case 'exclude': {
411
- const patterns = value
412
- .split(',')
413
- .map((p) => p.trim())
414
- .filter(Boolean);
415
- cliExclude.push(...patterns);
416
- break;
417
- }
418
- case 'basepath':
419
- result.basePath = validateBasePath(value);
420
- break;
421
- case 'maxsize':
422
- result.maxSize = parseSize(value, '--maxsize');
423
- break;
424
- case 'maxgzipsize':
425
- result.maxGzipSize = parseSize(value, '--maxgzipsize');
426
- break;
427
- default:
428
- throw new Error(`Unknown flag: ${flag}`);
429
- }
461
+ applyFlag(flag.slice(2), value);
430
462
  continue;
431
463
  }
432
464
  if (argument === '--created') {
@@ -437,6 +469,10 @@ function parseArguments() {
437
469
  result.noIndexCheck = true;
438
470
  continue;
439
471
  }
472
+ if (argument === '--dryrun' || argument === '--dry-run') {
473
+ result.dryRun = true;
474
+ continue;
475
+ }
440
476
  if (argument.startsWith('-') && !argument.startsWith('--')) {
441
477
  const flag = argument.slice(1);
442
478
  const nextArgument = arguments_[index + 1];
@@ -465,72 +501,8 @@ function parseArguments() {
465
501
  const nextArgument = arguments_[index + 1];
466
502
  if (!nextArgument || nextArgument.startsWith('-'))
467
503
  throw new Error(`Missing value for flag: ${argument}`);
468
- switch (flag) {
469
- case 'config':
470
- index++;
471
- break;
472
- case 'engine':
473
- result.engine = validateEngine(nextArgument);
474
- index++;
475
- break;
476
- case 'sourcepath':
477
- result.sourcepath = nextArgument;
478
- index++;
479
- break;
480
- case 'outputfile':
481
- result.outputfile = nextArgument;
482
- index++;
483
- break;
484
- case 'etag':
485
- result.etag = validateTriState(nextArgument, 'etag');
486
- index++;
487
- break;
488
- case 'gzip':
489
- result.gzip = validateTriState(nextArgument, 'gzip');
490
- index++;
491
- break;
492
- case 'version':
493
- result.version = nextArgument;
494
- index++;
495
- break;
496
- case 'espmethod':
497
- result.espmethod = nextArgument;
498
- index++;
499
- break;
500
- case 'define':
501
- result.define = nextArgument;
502
- index++;
503
- break;
504
- case 'cachetime':
505
- result.cachetime = Number.parseInt(nextArgument, 10);
506
- if (Number.isNaN(result.cachetime))
507
- throw new TypeError(`Invalid cachetime: ${nextArgument}`);
508
- index++;
509
- break;
510
- case 'exclude': {
511
- const patterns = nextArgument
512
- .split(',')
513
- .map((p) => p.trim())
514
- .filter(Boolean);
515
- cliExclude.push(...patterns);
516
- index++;
517
- break;
518
- }
519
- case 'basepath':
520
- result.basePath = validateBasePath(nextArgument);
521
- index++;
522
- break;
523
- case 'maxsize':
524
- result.maxSize = parseSize(nextArgument, '--maxsize');
525
- index++;
526
- break;
527
- case 'maxgzipsize':
528
- result.maxGzipSize = parseSize(nextArgument, '--maxgzipsize');
529
- index++;
530
- break;
531
- default:
532
- throw new Error(`Unknown flag: --${flag}`);
533
- }
504
+ applyFlag(flag, nextArgument);
505
+ index++;
534
506
  continue;
535
507
  }
536
508
  throw new Error(`Unknown argument: ${argument}`);