Skip to content

Commit e2e1d21

Browse files
committed
feat: rc file
1 parent ee75eaf commit e2e1d21

File tree

4 files changed

+589
-9
lines changed

4 files changed

+589
-9
lines changed

.svelteesp32rc.example.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"engine": "psychic",
3+
"sourcepath": "./demo/svelte/dist",
4+
"outputfile": "./output.h",
5+
"etag": "true",
6+
"gzip": "true",
7+
"cachetime": 0,
8+
"created": false,
9+
"version": "",
10+
"espmethod": "initSvelteStaticFiles",
11+
"define": "SVELTEESP32",
12+
"exclude": ["*.map", "*.md", "test/**/*"]
13+
}

README.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ In order to be able to easily update OTA, it is important - from the users' poin
1212

1313
This npm package provides a solution for **inserting any JS client application into the ESP web server** (PsychicHttp and also ESPAsyncWebServer (https://github.com/ESP32Async/ESPAsyncWebServer) and ESP-IDF 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**.
1414

15+
> Starting with version v1.12.0, you can use .rc file to config
16+
1517
> Starting with version v1.11.0, you can exclude files by pattern
1618
1719
> Starting with version v1.10.0, we reduced npm dependencies
@@ -427,8 +429,117 @@ You can use the following c++ directives at the project level if you want to con
427429
| `--version` | Include a version string in generated header, e.g. `--version=v$npm_package_version` | '' |
428430
| `--espmethod` | Name of generated initialization method | `initSvelteStaticFiles` |
429431
| `--define` | Prefix of c++ defines (e.g., SVELTEESP32_COUNT) | `SVELTEESP32` |
432+
| `--config` | Use custom RC file path | `.svelteesp32rc.json` |
430433
| `-h` | Show help | |
431434

435+
### Configuration File
436+
437+
You can store frequently-used options in a configuration file to avoid repeating command line arguments. This is especially useful for CI/CD pipelines and team collaboration.
438+
439+
#### Quick Start
440+
441+
Create `.svelteesp32rc.json` in your project directory:
442+
443+
```json
444+
{
445+
"engine": "psychic",
446+
"sourcepath": "./dist",
447+
"outputfile": "./esp32/include/svelteesp32.h",
448+
"etag": "true",
449+
"gzip": "true",
450+
"cachetime": 86400,
451+
"exclude": ["*.map", "*.md"]
452+
}
453+
```
454+
455+
Then simply run:
456+
457+
```bash
458+
npx svelteesp32
459+
```
460+
461+
No command line arguments needed!
462+
463+
#### Search Locations
464+
465+
The tool automatically searches for `.svelteesp32rc.json` in:
466+
467+
1. Current working directory
468+
2. User home directory
469+
470+
Or specify a custom location:
471+
472+
```bash
473+
npx svelteesp32 --config=.svelteesp32rc.prod.json
474+
```
475+
476+
#### Configuration Reference
477+
478+
All CLI options can be specified in the RC file using long-form property names:
479+
480+
| RC Property | CLI Flag | Type | Example |
481+
| ------------ | ------------- | ------- | ------------------------------------------------ |
482+
| `engine` | `-e` | string | `"psychic"`, `"psychic2"`, `"async"`, `"espidf"` |
483+
| `sourcepath` | `-s` | string | `"./dist"` |
484+
| `outputfile` | `-o` | string | `"./output.h"` |
485+
| `etag` | `--etag` | string | `"true"`, `"false"`, `"compiler"` |
486+
| `gzip` | `--gzip` | string | `"true"`, `"false"`, `"compiler"` |
487+
| `cachetime` | `--cachetime` | number | `86400` |
488+
| `created` | `--created` | boolean | `true`, `false` |
489+
| `version` | `--version` | string | `"v1.0.0"` |
490+
| `espmethod` | `--espmethod` | string | `"initSvelteStaticFiles"` |
491+
| `define` | `--define` | string | `"SVELTEESP32"` |
492+
| `exclude` | `--exclude` | array | `["*.map", "*.md"]` |
493+
494+
#### CLI Override
495+
496+
Command line arguments always take precedence over RC file values:
497+
498+
```bash
499+
# Use RC settings but override etag
500+
npx svelteesp32 --etag=false
501+
502+
# Use RC settings but add different exclude pattern
503+
npx svelteesp32 --exclude="*.txt"
504+
```
505+
506+
#### Multiple Environments
507+
508+
Create different config files for different environments:
509+
510+
```bash
511+
# Development build
512+
npx svelteesp32 --config=.svelteesp32rc.dev.json
513+
514+
# Production build
515+
npx svelteesp32 --config=.svelteesp32rc.prod.json
516+
```
517+
518+
#### Exclude Pattern Behavior
519+
520+
**Replace mode**: When you specify `exclude` patterns in RC file or CLI, they completely replace the defaults.
521+
522+
- **No exclude specified**: Uses default system exclusions (`.DS_Store`, `Thumbs.db`, `.git`, etc.)
523+
- **RC file has exclude**: Replaces defaults with RC patterns
524+
- **CLI has --exclude**: Replaces RC patterns (or defaults if no RC)
525+
526+
Example:
527+
528+
```json
529+
// .svelteesp32rc.json
530+
{ "exclude": ["*.map"] }
531+
```
532+
533+
Result: Only `*.map` is excluded (default patterns are replaced)
534+
535+
To keep defaults, explicitly list them in your RC file:
536+
537+
```json
538+
{
539+
"exclude": [".DS_Store", "Thumbs.db", ".git", "*.map", "*.md"]
540+
}
541+
```
542+
432543
### Q&A
433544

434545
- **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.

src/commandLine.ts

Lines changed: 173 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
1-
import { existsSync, statSync } from 'node:fs';
1+
import { existsSync, readFileSync, statSync } from 'node:fs';
2+
import { homedir } from 'node:os';
3+
import path from 'node:path';
4+
5+
import { cyanLog, yellowLog } from './consoleColor';
26

37
interface ICopyFilesArguments {
48
engine: 'psychic' | 'psychic2' | 'async' | 'espidf';
@@ -15,10 +19,27 @@ interface ICopyFilesArguments {
1519
help?: boolean;
1620
}
1721

22+
interface IRcFileConfig {
23+
engine?: 'psychic' | 'psychic2' | 'async' | 'espidf';
24+
sourcepath?: string;
25+
outputfile?: string;
26+
espmethod?: string;
27+
define?: string;
28+
gzip?: 'true' | 'false' | 'compiler';
29+
etag?: 'true' | 'false' | 'compiler';
30+
cachetime?: number;
31+
created?: boolean;
32+
version?: string;
33+
exclude?: string[];
34+
}
35+
1836
function showHelp(): never {
1937
console.log(`
2038
svelteesp32 - Svelte JS to ESP32 converter
2139
40+
Configuration:
41+
--config <path> Use custom RC file (default: search for .svelteesp32rc.json)
42+
2243
Options:
2344
-e, --engine <value> The engine for which the include file is created
2445
(psychic|psychic2|async|espidf) (default: "psychic")
@@ -34,6 +55,23 @@ Options:
3455
--exclude <pattern> Exclude files matching glob pattern (repeatable or comma-separated)
3556
Examples: --exclude="*.map" --exclude="test/**/*.ts"
3657
-h, --help Shows this help
58+
59+
RC File:
60+
The tool searches for .svelteesp32rc.json in:
61+
1. Current directory (./.svelteesp32rc.json)
62+
2. User home directory (~/.svelteesp32rc.json)
63+
64+
Example RC file (all fields optional):
65+
{
66+
"engine": "psychic",
67+
"sourcepath": "./dist",
68+
"outputfile": "./output.h",
69+
"etag": "true",
70+
"gzip": "true",
71+
"exclude": ["*.map", "*.md"]
72+
}
73+
74+
CLI arguments override RC file values.
3775
`);
3876
process.exit(0);
3977
}
@@ -61,8 +99,110 @@ const DEFAULT_EXCLUDE_PATTERNS = [
6199
'.gitattributes' // Git attributes file
62100
];
63101

102+
function findRcFile(customConfigPath?: string): string | undefined {
103+
// If --config specified, use that exclusively
104+
if (customConfigPath) {
105+
if (existsSync(customConfigPath)) return customConfigPath;
106+
throw new Error(`Config file not found: ${customConfigPath}`);
107+
}
108+
109+
// Check current directory
110+
for (const filename of ['.svelteesp32rc.json', '.svelteesp32rc']) {
111+
const cwdPath = path.join(process.cwd(), filename);
112+
if (existsSync(cwdPath)) return cwdPath;
113+
}
114+
115+
// Check home directory
116+
const homeDirectory = homedir();
117+
for (const filename of ['.svelteesp32rc.json', '.svelteesp32rc']) {
118+
const homePath = path.join(homeDirectory, filename);
119+
if (existsSync(homePath)) return homePath;
120+
}
121+
122+
return undefined;
123+
}
124+
125+
function loadRcFile(rcPath: string): IRcFileConfig {
126+
try {
127+
const content = readFileSync(rcPath, 'utf8');
128+
const config = JSON.parse(content);
129+
return validateRcConfig(config, rcPath);
130+
} catch (error) {
131+
if (error instanceof SyntaxError) throw new Error(`Invalid JSON in RC file ${rcPath}: ${error.message}`);
132+
133+
throw error;
134+
}
135+
}
136+
137+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
138+
function validateRcConfig(config: any, rcPath: string): IRcFileConfig {
139+
if (typeof config !== 'object' || config === null) throw new Error(`RC file ${rcPath} must contain a JSON object`);
140+
141+
const validKeys = new Set([
142+
'engine',
143+
'sourcepath',
144+
'outputfile',
145+
'espmethod',
146+
'define',
147+
'gzip',
148+
'etag',
149+
'cachetime',
150+
'created',
151+
'version',
152+
'exclude'
153+
]);
154+
155+
// Warn about unknown keys
156+
for (const key of Object.keys(config))
157+
if (!validKeys.has(key)) console.warn(yellowLog(`Warning: Unknown property '${key}' in RC file ${rcPath}`));
158+
159+
// Validate individual properties
160+
if (config.engine !== undefined) config.engine = validateEngine(config.engine);
161+
162+
if (config.etag !== undefined) config.etag = validateTriState(config.etag, 'etag');
163+
164+
if (config.gzip !== undefined) config.gzip = validateTriState(config.gzip, 'gzip');
165+
166+
if (config.cachetime !== undefined && (typeof config.cachetime !== 'number' || Number.isNaN(config.cachetime)))
167+
throw new TypeError(`Invalid cachetime in RC file: ${config.cachetime}`);
168+
169+
if (config.exclude !== undefined) {
170+
if (!Array.isArray(config.exclude)) throw new TypeError("'exclude' in RC file must be an array");
171+
172+
// Validate each exclude pattern is a string
173+
for (const pattern of config.exclude)
174+
if (typeof pattern !== 'string') throw new TypeError('All exclude patterns must be strings');
175+
}
176+
177+
return config;
178+
}
179+
64180
function parseArguments(): ICopyFilesArguments {
65181
const arguments_ = process.argv.slice(2);
182+
183+
// STEP 1: Check for --config flag first
184+
let customConfigPath: string | undefined;
185+
for (let index = 0; index < arguments_.length; index++) {
186+
const argument = arguments_[index];
187+
if (!argument) continue;
188+
189+
if (argument === '--config' && arguments_[index + 1]) {
190+
customConfigPath = arguments_[index + 1];
191+
break;
192+
}
193+
if (argument.startsWith('--config=')) {
194+
customConfigPath = argument.slice('--config='.length);
195+
break;
196+
}
197+
}
198+
199+
// STEP 2: Find and load RC file
200+
const rcPath = findRcFile(customConfigPath);
201+
const rcConfig = rcPath ? loadRcFile(rcPath) : {};
202+
203+
if (rcPath) console.log(cyanLog(`[SvelteESP32] Using config from: ${rcPath}`));
204+
205+
// STEP 3: Initialize with defaults
66206
const result: Partial<ICopyFilesArguments> = {
67207
engine: 'psychic',
68208
outputfile: 'svelteesp32.h',
@@ -76,6 +216,24 @@ function parseArguments(): ICopyFilesArguments {
76216
exclude: [...DEFAULT_EXCLUDE_PATTERNS]
77217
};
78218

219+
// STEP 4: Merge RC file values
220+
if (rcConfig.engine) result.engine = rcConfig.engine;
221+
if (rcConfig.sourcepath) result.sourcepath = rcConfig.sourcepath;
222+
if (rcConfig.outputfile) result.outputfile = rcConfig.outputfile;
223+
if (rcConfig.etag) result.etag = rcConfig.etag;
224+
if (rcConfig.gzip) result.gzip = rcConfig.gzip;
225+
if (rcConfig.cachetime !== undefined) result.cachetime = rcConfig.cachetime;
226+
if (rcConfig.created !== undefined) result.created = rcConfig.created;
227+
if (rcConfig.version) result.version = rcConfig.version;
228+
if (rcConfig.espmethod) result.espmethod = rcConfig.espmethod;
229+
if (rcConfig.define) result.define = rcConfig.define;
230+
231+
// Replace defaults with RC exclude if provided
232+
if (rcConfig.exclude && rcConfig.exclude.length > 0) result.exclude = [...rcConfig.exclude];
233+
234+
// STEP 5: Parse CLI arguments
235+
const cliExclude: string[] = [];
236+
79237
for (let index = 0; index < arguments_.length; index++) {
80238
const argument = arguments_[index];
81239

@@ -95,6 +253,9 @@ function parseArguments(): ICopyFilesArguments {
95253
const flagName = flag.slice(2);
96254

97255
switch (flagName) {
256+
case 'config':
257+
// Already processed, skip
258+
break;
98259
case 'engine':
99260
result.engine = validateEngine(value);
100261
break;
@@ -129,8 +290,7 @@ function parseArguments(): ICopyFilesArguments {
129290
.split(',')
130291
.map((p) => p.trim())
131292
.filter(Boolean);
132-
result.exclude = result.exclude || [...DEFAULT_EXCLUDE_PATTERNS];
133-
result.exclude.push(...patterns);
293+
cliExclude.push(...patterns);
134294
break;
135295
}
136296
default:
@@ -179,6 +339,10 @@ function parseArguments(): ICopyFilesArguments {
179339
if (!nextArgument || nextArgument.startsWith('-')) throw new Error(`Missing value for flag: ${argument}`);
180340

181341
switch (flag) {
342+
case 'config':
343+
// Already processed, skip
344+
index++;
345+
break;
182346
case 'engine':
183347
result.engine = validateEngine(nextArgument);
184348
index++;
@@ -222,8 +386,7 @@ function parseArguments(): ICopyFilesArguments {
222386
.split(',')
223387
.map((p) => p.trim())
224388
.filter(Boolean);
225-
result.exclude = result.exclude || [...DEFAULT_EXCLUDE_PATTERNS];
226-
result.exclude.push(...patterns);
389+
cliExclude.push(...patterns);
227390
index++;
228391
break;
229392
}
@@ -236,9 +399,12 @@ function parseArguments(): ICopyFilesArguments {
236399
throw new Error(`Unknown argument: ${argument}`);
237400
}
238401

239-
// Validate required arguments
402+
// STEP 6: Apply CLI exclude (replaces RC/defaults)
403+
if (cliExclude.length > 0) result.exclude = [...cliExclude];
404+
405+
// STEP 7: Validate required arguments
240406
if (!result.sourcepath) {
241-
console.error('Error: --sourcepath is required');
407+
console.error('Error: --sourcepath is required (can be specified in RC file or CLI)');
242408
showHelp();
243409
}
244410

0 commit comments

Comments
 (0)