# svelteesp32 ![image](https://badges.github.io/stability-badges/dist/stable.svg)

### Embed Any Web App in Your ESP32 — One Binary, Zero Filesystem Hassle

**Turn your Svelte, React, Angular, or Vue frontend into a single C++ header file.** Serve beautiful web interfaces directly from ESP32/ESP8266 flash memory with automatic gzip compression, ETag caching, and seamless OTA updates.

[Changelog](CHANGELOG.md)

<p align="center">
  <img src="svelteesp32.png" alt="svelteesp32" />
</p>

---

## Why SvelteESP32?

**The problem:** Traditional approaches like SPIFFS and LittleFS require separate partition uploads, complex OTA workflows, and manual compression. Your users end up managing multiple files, and your CI/CD pipeline becomes a mess.

**The solution:** SvelteESP32 compiles your entire web application into a single C++ header file. One firmware binary. One OTA update. Done.

### Key Benefits

- **Single Binary OTA** — Everything embedded in firmware. No partition juggling, no separate uploads.
- **Automatic Optimization** — Build-time gzip compression with intelligent thresholds (>1KB, >15% reduction).
- **Smart Caching** — Built-in SHA256 ETags deliver HTTP 304 responses, slashing bandwidth on constrained devices.
- **CI/CD Ready** — Simple npm package that slots into any build pipeline.
- **Zero Runtime Overhead** — Data served directly from flash. No filesystem reads, no RAM allocation.
- **4 Web Server Engines** — PsychicHttpServer V2, ESPAsyncWebServer, Arduino WebServer, and native ESP-IDF supported.

---

## SvelteESP32 vs Traditional Filesystem

| Feature               | SvelteESP32                       | SPIFFS / LittleFS                        |
| --------------------- | --------------------------------- | ---------------------------------------- |
| **Single Binary OTA** | ✓ Everything in firmware          | ✗ Separate partition upload required     |
| **Gzip Compression**  | ✓ Automatic at build time         | Manual or runtime compression            |
| **ETag Support**      | ✓ Built-in SHA256 + 304 responses | Manual implementation required           |
| **CI/CD Integration** | ✓ One npm command                 | Complex upload_fs tooling                |
| **Memory Efficiency** | Flash only (PROGMEM/const arrays) | Filesystem partition + overhead          |
| **Performance**       | Direct byte array serving         | Filesystem read latency                  |
| **Setup Complexity**  | Include header, call one function | Partition tables, upload tools, handlers |

**Best for:** Single-binary OTA, CI/CD pipelines, static web UIs that ship with firmware.

**Consider SPIFFS/LittleFS for:** User-uploadable files, runtime-editable configs, dynamic content.

---

## Quick Start

```bash
npm install -D svelteesp32
```

After building your frontend (Vite/Rollup/Webpack):

```bash
npx svelteesp32 -e psychic -s ./dist -o ./esp32/svelteesp32.h --etag=always
```

Include in your ESP32 project:

```c
#include <PsychicHttp.h>
#include "svelteesp32.h"

PsychicHttpServer server;

void setup() {
    server.listen(80);
    initSvelteStaticFiles(&server);
}
```

**That's it.** Your entire web app is now embedded and ready to serve.

> **Just want production-safe defaults?**
>
> ```bash
> npx svelteesp32 -e psychic -s ./dist -o ./esp32/svelteesp32.h \
>   --etag=always --gzip=always --cachetimehtml=0 --cachetimeassets=31536000
> ```
>
> ETags for instant 304s, gzip for smaller transfers, `no-cache` for HTML so updates are always picked up, and 1-year caching for content-hashed JS/CSS assets.

---

## What's New

- **v3.1.0** — Removed `handlebars` dependency; C++ generation is now pure TypeScript. `--cachetime-html` → `--cachetimehtml`, `--cachetime-assets` → `--cachetimeassets` (CLI now matches RC file keys); `--dry-run` alias removed — use `--dryrun`
- **v3.0.0** — **Vite plugin** (`import { svelteESP32 } from 'svelteesp32/vite'`) generates the header automatically after every build — call with no argument for RC file mode or pass an options object for plugin-options mode; `npx svelteesp32 init` interactive RC file wizard; Node.js >= 22 required
- **v2.4.0** — `--analyze` for CI size budget checks (per-file table, exits 1 on over-budget); `--manifest` to write a companion JSON manifest alongside the header
- **v2.3.0** — `--cachetimehtml` and `--cachetimeassets` for per-type cache control (e.g. `no-cache` for HTML, 1-year for content-hashed JS/CSS)
- **v2.2.0** — SPA routing catch-all (`--spa`) for client-side routers on all four engines
- **v2.1.0** — New Arduino WebServer engine (`-e webserver`), dependency updates
- **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
- **v1.16.0** — Size budget constraints (`--maxsize`, `--maxgzipsize`)
- **v1.15.0** — `--basepath` for multiple frontends (e.g., `/admin`, `/app`)
- **v1.13.0** — npm package variable interpolation in RC files
- **v1.12.0** — RC file configuration support
- **v1.11.0** — File exclusion patterns
- **v1.9.0** — Native ESP-IDF engine

---

## Requirements

- Node.js >= 22
- npm >= 10

---

## Installation & Usage

### Install

```bash
npm install -D svelteesp32
```

### Quick Setup with `init`

The `init` command creates a `.svelteesp32rc.json` configuration file interactively so you never have to remember CLI flags:

```bash
npx svelteesp32 init
```

It asks for engine, source path, output path, and ETag preference, writes the RC file, and optionally runs the tool immediately.

### Vite Plugin

For Vite-based projects (SvelteKit, React, Vue, Vanilla) you can skip the manual CLI step entirely — the plugin hooks into the build pipeline and regenerates the C++ header automatically after every `vite build`.

The plugin has two exclusive modes — pick one:

**RC file mode** — call with no argument (or a string path to a custom RC file). All settings come from `.svelteesp32rc.json`; `outputfile` in the RC file is required.

```ts
import { svelteKit } from '@sveltejs/kit/vite';
import { svelteESP32 } from 'svelteesp32/vite';
import { defineConfig } from 'vite';

export default defineConfig({
  plugins: [
    svelteKit(),
    svelteESP32() // auto-discover .svelteesp32rc.json
    // svelteESP32('/path/to/custom.rc.json')  // or specify path explicitly
  ]
});
```

**Plugin options mode** — call with an options object. The RC file is completely ignored; `output` is required.

```ts
export default defineConfig({
  plugins: [
    svelteKit(),
    svelteESP32({
      output: '../firmware/include/svelteesp32.h',
      engine: 'psychic',
      etag: 'always',
      gzip: 'always',
      cachetimehtml: 0,
      cachetimeassets: 31536000
    })
  ]
});
```

`sourcepath` defaults to Vite's `build.outDir` in both modes.

**Plugin options**

| Option            | Type                                        | Default                   | Description                                        |
| ----------------- | ------------------------------------------- | ------------------------- | -------------------------------------------------- |
| `output`          | `string`                                    | RC `outputfile`           | Output `.h` file path                              |
| `sourcepath`      | `string`                                    | Vite's `build.outDir`     | Source directory (compiled web files)              |
| `engine`          | `'psychic'\|'async'\|'espidf'\|'webserver'` | `'psychic'`               | Target web server engine                           |
| `etag`            | `'always'\|'never'\|'compiler'`             | `'never'`                 | ETag generation mode                               |
| `gzip`            | `'always'\|'never'\|'compiler'`             | `'always'`                | Gzip compression mode                              |
| `cachetime`       | `number`                                    | `0`                       | `Cache-Control: max-age` in seconds (all files)    |
| `cachetimehtml`   | `number`                                    | (unset)                   | max-age for HTML files (overrides `cachetime`)     |
| `cachetimeassets` | `number`                                    | (unset)                   | max-age for non-HTML files (overrides `cachetime`) |
| `exclude`         | `string[]`                                  | `[]`                      | Glob patterns to exclude                           |
| `basepath`        | `string`                                    | (none)                    | URL prefix for all routes                          |
| `espmethod`       | `string`                                    | `'initSvelteStaticFiles'` | Generated init function name                       |
| `define`          | `string`                                    | `'SVELTEESP32'`           | C++ `#define` prefix                               |
| `version`         | `string`                                    | (none)                    | Version string embedded in header                  |
| `created`         | `boolean`                                   | `false`                   | Include creation timestamp                         |
| `spa`             | `boolean`                                   | `false`                   | Serve `index.html` for unmatched routes            |
| `manifest`        | `boolean`                                   | `false`                   | Write companion `.manifest.json`                   |
| `noindexcheck`    | `boolean`                                   | `false`                   | Skip `index.html` validation                       |
| `maxsize`         | `number`                                    | (none)                    | Max total uncompressed size in bytes               |
| `maxgzipsize`     | `number`                                    | (none)                    | Max total gzip size in bytes                       |

### Generate Header File (CLI)

Choose your web server engine:

```bash
# PsychicHttpServer (recommended for ESP32)
npx svelteesp32 -e psychic -s ./dist -o ./esp32/svelteesp32.h --etag=always

# ESPAsyncWebServer (ESP32 + ESP8266)
npx svelteesp32 -e async -s ./dist -o ./esp32/svelteesp32.h --etag=always

# Arduino WebServer (ESP32, synchronous, no dependencies)
npx svelteesp32 -e webserver -s ./dist -o ./esp32/svelteesp32.h --etag=always

# Native ESP-IDF
npx svelteesp32 -e espidf -s ./dist -o ./esp32/svelteesp32.h --etag=always
```

### Build Output

Watch your files get optimized in real-time:

```
[assets/index-KwubEIf-.js]  ✓ gzip used (38850 -> 12547 = 32%)
[assets/index-Soe6cpLA.css] ✓ gzip used (32494 -> 5368 = 17%)
[favicon.png]               x gzip unused (33249 -> 33282 = 100%)
[index.html]                x gzip unused (too small) (472 -> 308 = 65%)
[roboto_regular.json]       ✓ gzip used (363757 -> 93567 = 26%)

5 files, 458kB original size, 142kB gzip size
../../../Arduino/EspSvelte/svelteesp32.h 842kB size
```

**Automatic optimizations:**

- Gzip level 9 compression when beneficial (>1KB, >15% size reduction)
- Duplicate file detection via SHA256 hashing
- Smart skip of pre-compressed files (.gz, .br) when originals exist

### ESP32 Integration

**PsychicHttpServer V2 (Recommended)**

```c
#include <PsychicHttp.h>
#include "svelteesp32.h"

PsychicHttpServer server;

void setup() {
    server.listen(80);
    initSvelteStaticFiles(&server);  // One line. Done.
}
```

**ESPAsyncWebServer**

```c
#include <ESPAsyncWebServer.h>
#include "svelteesp32.h"

AsyncWebServer server(80);

void setup() {
    initSvelteStaticFiles(&server);
    server.begin();
}
```

**Arduino WebServer (built-in, no dependencies)**

```c
#include <WebServer.h>
#include "svelteesp32.h"

WebServer server(80);

void setup() {
    initSvelteStaticFiles(&server);
    server.begin();
}

void loop() {
    server.handleClient();
}
```

**Native ESP-IDF**

```c
#include <esp_http_server.h>
#include "svelteesp32.h"

httpd_handle_t server = NULL;

void app_main() {
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    httpd_start(&server, &config);
    initSvelteStaticFiles(server);
}
```

Working examples with LED control via web interface: [Arduino/PlatformIO](demo/esp32) | [ESP-IDF](demo/esp32idf)

### What Gets Generated

The generated header file includes everything your ESP needs:

```c
//engine:   PsychicHttpServer V2
//config:   engine=psychic sourcepath=./dist outputfile=./output.h etag=always gzip=always cachetime=0 espmethod=initSvelteStaticFiles define=SVELTEESP32
//
#define SVELTEESP32_COUNT 5
#define SVELTEESP32_SIZE 468822
#define SVELTEESP32_SIZE_GZIP 145633
#define SVELTEESP32_FILE_INDEX_HTML
#define SVELTEESP32_HTML_FILES 1
#define SVELTEESP32_CSS_FILES 1
#define SVELTEESP32_JS_FILES 1
...

#include <Arduino.h>
#include <PsychicHttp.h>
#include <PsychicHttpsServer.h>

static const uint8_t datagzip_assets_index_KwubEIf__js[12547] = {0x1f, 0x8b, 0x8, 0x0, ...
static const uint8_t datagzip_assets_index_Soe6cpLA_css[5368] = {0x1f, 0x8b, 0x8, 0x0, 0x0, ...
static const char etag_assets_index_KwubEIf__js[] = "387b88e345cc56ef9091...";
static const char etag_assets_index_Soe6cpLA_css[] = "d4f23bc45ef67890ab12...";

// File manifest for runtime introspection
struct SVELTEESP32_FileInfo {
  const char* path;
  uint32_t size;
  uint32_t gzipSize;
  const char* etag;
  const char* contentType;
};
const SVELTEESP32_FileInfo SVELTEESP32_FILES[] = {
  { "/assets/index-KwubEIf-.js", 38850, 12547, etag_assets_index_KwubEIf__js, "text/javascript" },
  { "/assets/index-Soe6cpLA.css", 32494, 5368, etag_assets_index_Soe6cpLA_css, "text/css" },
  ...
};
const size_t SVELTEESP32_FILE_COUNT = sizeof(SVELTEESP32_FILES) / sizeof(SVELTEESP32_FILES[0]);
...

// File served hook - override with your own implementation for metrics/logging
extern "C" void __attribute__((weak)) SVELTEESP32_onFileServed(const char* path, int statusCode) {}

void initSvelteStaticFiles(PsychicHttpServer * server) {
  server->on("/assets/index-KwubEIf-.js", HTTP_GET, [](PsychicRequest * request, PsychicResponse * response) {
    if (request->hasHeader("If-None-Match") &&
        request->header("If-None-Match").equals(etag_assets_index_KwubEIf__js)) {
      response->setCode(304);
      SVELTEESP32_onFileServed("/assets/index-KwubEIf-.js", 304);
      return response->send();
    }

    response->setContentType("text/javascript");
    response->addHeader("Content-Encoding", "gzip");
    response->addHeader("Cache-Control", "no-cache");
    response->addHeader("ETag", etag_assets_index_KwubEIf__js);
    response->setContent(datagzip_assets_index_KwubEIf__js, 12547);
    SVELTEESP32_onFileServed("/assets/index-KwubEIf-.js", 200);
    return response->send();
  });

  // ... more routes
}
```

---

## Supported Web Server Engines

| Engine                   | Flag           | Best For                     | Platform        |
| ------------------------ | -------------- | ---------------------------- | --------------- |
| **PsychicHttpServer V2** | `-e psychic`   | Maximum performance          | ESP32 only      |
| **ESPAsyncWebServer**    | `-e async`     | Cross-platform compatibility | ESP32 + ESP8266 |
| **Arduino WebServer**    | `-e webserver` | No dependencies, simplicity  | ESP32 only      |
| **Native ESP-IDF**       | `-e espidf`    | Pure ESP-IDF projects        | ESP32 only      |

**Recommendation:** For ESP32-only projects, use PsychicHttpServer V2 (`-e psychic`) for the fastest, most stable experience.

**Note:** For PsychicHttp, configure `server.config.max_uri_handlers`. The generated header provides `SVELTEESP32_MAX_URI_HANDLERS` (file count + 5 safety margin) for use directly in your sketch.

---

## Features

### Automatic Gzip Compression

Your JS, CSS, and HTML files are automatically compressed at build time — not on the ESP32. Files are gzipped when they're >1KB and achieve >15% size reduction.

- **Enabled by default** — disable with `--gzip=never`
- **Compiler mode** — use `--gzip=compiler` and control via `-D SVELTEESP32_ENABLE_GZIP` in PlatformIO

### Smart ETag Caching

Reduce bandwidth dramatically with HTTP 304 "Not Modified" responses. When a browser has a cached file, the ESP32 sends just a status code instead of the entire file — perfect for bandwidth-constrained IoT devices.

- **Enable with** `--etag=always` (recommended)
- **Minimal overhead** — adds ~1-3% code size for significant bandwidth savings
- **Compiler mode** — use `--etag=compiler` and control via `-D SVELTEESP32_ENABLE_ETAG`

All four engines support full ETag validation.

> **Browser compatibility note:** ETags and `Cache-Control: max-age` are universally supported in all modern browsers. Very old clients (IE6/7, early Android 2.x WebViews) may ignore `must-revalidate` or mishandle 304 responses. If you target these environments, set `--etag=never` and `--cachetime=0` to force full downloads on every request.

### Browser Cache Control

Fine-tune how browsers cache your content:

- **Default:** `no-cache` — browsers always validate with server (ETag check)
- **Long-term caching:** `--cachetime=86400` — cache for 24 hours without any server requests
- **Per-type caching:** Use `--cachetimehtml` and `--cachetimeassets` independently

Vite and webpack produce content-hashed filenames for JS/CSS (e.g., `app.a1b2c3.js`). Those can be cached for up to a year because the hash changes with every build, but `index.html` must stay `no-cache` since it's the entry point that references them:

```bash
npx svelteesp32 -e psychic -s ./dist -o ./output.h \
  --etag=always --cachetimehtml=0 --cachetimeassets=31536000
```

This emits `Cache-Control: no-cache` for every `text/html` file and `Cache-Control: max-age=31536000` for all other assets in the same header, with no per-file configuration needed.

| Option              | Applies to                       | Falls back to  |
| ------------------- | -------------------------------- | -------------- |
| `--cachetimehtml`   | `text/html` only                 | `--cachetime`  |
| `--cachetimeassets` | everything else                  | `--cachetime`  |
| `--cachetime`       | all files (when no override set) | `0` (no-cache) |

### Automatic Index Handling

Your `index.html` is automatically served at the root URL — just like any web server. Visit `http://esp32.local/` and your app loads.

**API-only projects?** Skip index validation with `--noindexcheck`:

```bash
npx svelteesp32 -e psychic -s ./dist -o ./output.h --noindexcheck
```

### File Exclusion

Keep source maps, docs, and test files out of your firmware:

```bash
# Single pattern
npx svelteesp32 -s ./dist -o ./output.h --exclude="*.map"

# Multiple patterns
npx svelteesp32 -s ./dist -o ./output.h --exclude="*.map,*.md,test/**/*"
```

No patterns are excluded by default — specify everything you need explicitly.

Build output shows exactly what's excluded:

```
Excluded 3 file(s):
  - assets/index.js.map
  - assets/vendor.js.map
  - README.md
```

### Multiple Frontends (Base Path)

Serve multiple web apps from one ESP32 using URL prefixes:

```bash
npx svelteesp32 -s ./admin-dist -o ./admin.h --basepath=/admin
npx svelteesp32 -s ./user-dist -o ./user.h --basepath=/app
```

```c
#include "admin.h"  // Serves at /admin/*
#include "user.h"   // Serves at /app/*

void setup() {
    server.listen(80);
    initSvelteStaticFiles_admin(&server);
    initSvelteStaticFiles_user(&server);
    server.on("/api/data", HTTP_GET, handleApiData);
}
```

**Rules:** Must start with `/`, no trailing slash, no double slashes.

### SPA Routing (Client-Side Routers)

Modern JS frameworks use client-side routing. Without a catch-all, refreshing `/settings` on your ESP32 returns nothing. Add `--spa` to make all unmatched GET requests fall through to `index.html`:

```bash
npx svelteesp32 -e async -s ./dist -o ./output.h --spa
npx svelteesp32 -e psychic -s ./dist -o ./output.h --spa --basepath=/app
```

What gets generated per engine:

| Engine      | Catch-all mechanism                                                                                                          |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------- |
| `psychic`   | `server->on("/*", ...)` (no basePath) already handled by `defaultEndpoint`; `server->on("/app/*", ...)` when basePath is set |
| `async`     | `server->onNotFound(...)` with optional basePath prefix check                                                                |
| `webserver` | `server->onNotFound(...)` with optional basePath prefix check                                                                |
| `espidf`    | `httpd_register_err_handler(HTTPD_404_NOT_FOUND, ...)`                                                                       |

**Note:** `--spa` requires `index.html` or `index.htm` in the source directory — a warning is printed if it is missing.

### Analyze Mode (CI Size Budget Checks)

Use `--analyze` in CI to validate firmware size budgets without producing any output file:

```bash
npx svelteesp32 -e psychic -s ./dist --maxsize=400k --maxgzipsize=150k --analyze
```

Sample output:

```
index.html                           Original  Gzip
──────────────────────────────────── ────────  ────────
assets/index-KwubEIf-.js             37.9kB    12.3kB
assets/index-Soe6cpLA.css            31.7kB    5.2kB
favicon.png                          32.5kB    32.5kB  [no gzip]
index.html                           0.5kB     0.3kB
────────────────────────────────────────────────────────
Total                                102.6kB   50.3kB
Budget (maxsize)                     400.0kB   -         ✓ PASS
Budget (maxgzipsize)                 -         150.0kB   ✓ PASS
```

Exits with code **1** if any budget is exceeded — CI fails automatically. Mutually exclusive with `--dryrun`.

### JSON Manifest

Add `--manifest` to write a companion `.manifest.json` file alongside the header (same directory, same base name):

```bash
npx svelteesp32 -e psychic -s ./dist -o ./esp32/svelteesp32.h --manifest
# also writes ./esp32/svelteesp32.manifest.json
```

The manifest records build metadata and per-file details for tooling and dashboards:

```json
{
  "generated": "2026-04-26T12:00:00.000Z",
  "engine": "psychic",
  "etag": "never",
  "gzip": "always",
  "filecount": 4,
  "size": 104960,
  "gzipSize": 51507,
  "files": [
    {
      "path": "/assets/index-KwubEIf-.js",
      "mime": "text/javascript",
      "size": 38850,
      "gzipSize": 12547,
      "isGzip": true
    }
  ]
}
```

### C++ Build-Time Validation

Catch configuration issues at compile time with generated defines:

```c
#include "svelteesp32.h"

#if SVELTEESP32_COUNT != 5
  #error Unexpected file count - check your build
#endif

#ifndef SVELTEESP32_FILE_INDEX_HTML
  #error Missing index.html - frontend build failed?
#endif
```

**Available defines:** `SVELTEESP32_COUNT`, `SVELTEESP32_SIZE`, `SVELTEESP32_SIZE_GZIP`, `SVELTEESP32_FILE_*`, `SVELTEESP32_*_FILES`

### Runtime File Manifest

Query embedded files at runtime for logging, diagnostics, or API endpoints:

```c
// List all embedded files
for (size_t i = 0; i < SVELTEESP32_FILE_COUNT; i++) {
    const auto& f = SVELTEESP32_FILES[i];
    Serial.printf("%s (%d bytes, gzip: %d)\n", f.path, f.size, f.gzipSize);
}
```

Each file entry includes: `path`, `size`, `gzipSize`, `etag`, `contentType`

### Request Hook (Metrics & Logging)

Track every request with zero overhead when unused (weak linkage):

```c
extern "C" void SVELTEESP32_onFileServed(const char* path, int statusCode) {
    Serial.printf("[HTTP] %s -> %d\n", path, statusCode);
    if (statusCode == 304) cacheHits++;
}
```

Called for every response (200 = content served, 304 = cache hit).

---

## CLI Reference

| Option              | Description                                                                            | Default                 |
| ------------------- | -------------------------------------------------------------------------------------- | ----------------------- |
| `-s`                | Source folder with compiled web files                                                  | (required)              |
| `-e`                | Web server engine (psychic/async/espidf/webserver)                                     | `psychic`               |
| `-o`                | Output header file path                                                                | `svelteesp32.h`         |
| `--etag`            | ETag caching (always/never/compiler)                                                   | `never`                 |
| `--gzip`            | Gzip compression (always/never/compiler)                                               | `always`                |
| `--created`         | Include creation timestamp in header                                                   | `false`                 |
| `--exclude`         | Exclude files by glob pattern                                                          | (none)                  |
| `--basepath`        | URL prefix for all routes                                                              | (none)                  |
| `--maxsize`         | Max total uncompressed size (e.g., `400k`, `1m`)                                       | (none)                  |
| `--maxgzipsize`     | Max total gzip size (e.g., `150k`, `500k`)                                             | (none)                  |
| `--cachetime`       | Cache-Control max-age in seconds (all files)                                           | `0`                     |
| `--cachetimehtml`   | max-age for HTML files (overrides `--cachetime`)                                       | (unset)                 |
| `--cachetimeassets` | max-age for non-HTML files (overrides `--cachetime`)                                   | (unset)                 |
| `--version`         | Version string in header                                                               | (none)                  |
| `--define`          | C++ define prefix                                                                      | `SVELTEESP32`           |
| `--espmethod`       | Init function name                                                                     | `initSvelteStaticFiles` |
| `--config`          | Custom RC file path                                                                    | `.svelteesp32rc.json`   |
| `--dryrun`          | Show route table + summary without writing output                                      | `false`                 |
| `--analyze`         | Print per-file size table and budget status, no output written; exits 1 if over budget | `false`                 |
| `--manifest`        | Write companion `.manifest.json` alongside the header                                  | `false`                 |
| `--spa`             | Serve index.html for unmatched routes (SPA routing)                                    | `false`                 |
| `--noindexcheck`    | Skip index.html validation                                                             | `false`                 |
| `-h`                | Show help                                                                              |                         |

---

## Configuration File

Store your settings in `.svelteesp32rc.json` for zero-argument builds:

```json
{
  "engine": "psychic",
  "sourcepath": "./dist",
  "outputfile": "./esp32/svelteesp32.h",
  "etag": "always",
  "gzip": "always",
  "exclude": ["*.map", "*.md"],
  "basepath": "/ui",
  "maxsize": "400k",
  "maxgzipsize": "150k",
  "cachetime": 0,
  "cachetimehtml": 0,
  "cachetimeassets": 31536000,
  "noindexcheck": false,
  "dryrun": false,
  "analyze": false,
  "spa": false,
  "manifest": false
}
```

Boolean fields (`noindexcheck`, `dryrun`, `analyze`, `spa`, `manifest`, `created`) accept native JSON booleans (`true`/`false`) or their string equivalents (`"true"`/`"false"`), matching the existing behaviour of `etag` and `gzip`.

Then just run:

```bash
npx svelteesp32
```

### npm Variable Interpolation

Sync versions and names automatically from your `package.json`:

```json
{
  "version": "v$npm_package_version",
  "define": "$npm_package_name"
}
```

With `package.json` containing `"version": "2.1.0"`, this becomes `"version": "v2.1.0"`.

### Multiple Environments

```bash
npx svelteesp32 --config=.svelteesp32rc.prod.json
```

CLI arguments always override RC file values.

> **Security note:** When svelteesp32 auto-loads an RC file from the current directory it prints a warning. If you cloned a project that includes `.svelteesp32rc.json`, review it before running. `outputfile` in RC files must be a relative path — use `--output` on the CLI for absolute paths.

---

## Common Recipes

### Recipe A — Arduino IDE + Built-in WebServer

No PlatformIO required. Build your frontend, run svelteesp32 manually, then compile in Arduino IDE.

```bash
# 1. Build your frontend
npm run build

# 2. Generate the header
npx svelteesp32 -e webserver -s ./dist -o ./MyProject/svelteesp32.h \
  --gzip=always --etag=never
```

> **Note:** The Arduino `WebServer` library does not support ETag validation, so `--etag=never` is the correct setting here. Remember to call `server.handleClient()` in your `loop()`.

```c
#include <WebServer.h>
#include "svelteesp32.h"

WebServer server(80);

void setup() {
    WiFi.begin("ssid", "password");
    while (WiFi.status() != WL_CONNECTED) delay(500);
    initSvelteStaticFiles(&server);
    server.begin();
}

void loop() {
    server.handleClient();
}
```

---

### Recipe B — PlatformIO + Vite/SvelteKit, Auto-Generated Header on `pio run`

Run svelteesp32 automatically as a PlatformIO pre-build step so your header is always up to date.

**`platformio.ini`**

```ini
[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
lib_deps =
    ESP Async WebServer
extra_scripts = pre:scripts/build_frontend.py
```

**`scripts/build_frontend.py`**

```python
Import("env")
import subprocess

def build_frontend(source, target, env):
    print("Building frontend...")
    subprocess.run(["npm", "run", "build"], cwd="frontend", check=True)
    subprocess.run([
        "npx", "svelteesp32",
        "-e", "async",
        "-s", "frontend/dist",
        "-o", "src/svelteesp32.h",
        "--etag=always", "--gzip=always",
        "--cachetimehtml=0", "--cachetimeassets=31536000"
    ], check=True)

env.AddPreAction("buildprog", build_frontend)
```

This ensures the C++ header is regenerated from the latest frontend build every time you run `pio run`.

---

### Recipe C — ESP-IDF CMake, 1-Year Cached Content-Hashed Assets

Integrate svelteesp32 into a CMake build so the header is regenerated automatically.

**`CMakeLists.txt` (component or top-level)**

```cmake
add_custom_command(
    OUTPUT ${CMAKE_CURRENT_SOURCE_DIR}/main/svelteesp32.h
    COMMAND npm run build
    COMMAND npx svelteesp32
        -e espidf
        -s ${CMAKE_CURRENT_SOURCE_DIR}/frontend/dist
        -o ${CMAKE_CURRENT_SOURCE_DIR}/main/svelteesp32.h
        --etag=always --gzip=always
        --cachetimehtml=0 --cachetimeassets=31536000
    WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
    COMMENT "Generating svelteesp32.h from frontend build"
    VERBATIM
)

add_custom_target(frontend_header
    DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/main/svelteesp32.h
)

# Make your main component depend on the generated header
add_dependencies(${COMPONENT_LIB} frontend_header)
```

**`main/app_main.c`**

```c
#include <esp_http_server.h>
#include "svelteesp32.h"

void app_main(void) {
    // ... WiFi init ...
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    httpd_handle_t server = NULL;
    httpd_start(&server, &config);
    initSvelteStaticFiles(server);
}
```

The `--cachetimehtml=0 --cachetimeassets=31536000` combination gives you `no-cache` for `index.html` (so browser always checks for updates) and a 1-year `max-age` for content-hashed JS/CSS bundles.

---

## FAQ

**How large can my web app be?**
With gzip compression, 3-4MB asset directories work comfortably. That's enough for a full-featured SPA.

**Does this use RAM or Flash?**
Flash only. Data is stored in program memory (PROGMEM on ESP8266, const arrays on ESP32), leaving your heap and stack free for application logic.

**Why is the .h file so large?**
The text representation (comma-separated bytes) is larger than binary. Check `SVELTEESP32_SIZE_GZIP` for actual flash usage.

**Does compilation take forever?**
No. Large headers compile in seconds, and incremental builds skip recompilation if your frontend hasn't changed.

**Can frontend and firmware teams work separately?**
Absolutely. Frontend builds the app, runs svelteesp32, commits the header. Firmware team includes it and ships. Version sync via npm variables keeps everyone aligned.

---

## Development

```bash
npm run build        # Build TypeScript
npm run test         # Run unit tests
npm run test:watch   # Watch mode
npm run fix          # Fix formatting & linting
```

---

**Ready to ship your web UI in a single binary?**

```bash
npm install -D svelteesp32
```
