npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

svelteesp32

v3.2.2

Published

Convert Svelte (or any frontend) JS application to serve it from ESP32 webserver (PsychicHttp)

Readme

svelteesp32 image

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


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

npm install -D svelteesp32

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

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

Include in your ESP32 project:

#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?

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.


How It Works

Your web UI is just another build artifact: build the frontend, turn dist/ into a header, compile it into the firmware. But svelteesp32 does not belong in your inner loop — develop the UI with vite dev against the device's REST API as you always would, and only generate the header when the UI is ready to ship.

flowchart LR
    subgraph inner["Inner loop — all day, no svelteesp32"]
        direction LR
        A["Develop frontend<br/>vite dev + hot reload"]
        API["ESP32 REST API<br/>(real device or mock)"]
        A <--> API
    end

    inner --> B["UI is ready<br/>npm run build → dist/"]
    B --> C{"Generate header"}
    C -->|"npx svelteesp32"| D["svelteesp32.h<br/>gzip + ETag + routes"]
    C -->|"Vite plugin<br/>(automatic)"| D
    D --> E["Firmware build<br/>PlatformIO / Arduino IDE / ESP-IDF"]
    E --> F["Flash or OTA<br/>once or twice, not continuously"]
    F --> A

Inner loop (fast, repeated): the frontend dev server serves your UI with hot reload while fetch() calls go straight to the ESP32 on the network — the same API the embedded UI will call in production. Flash the firmware once so the API is up, then stay in the browser. Point Vite's server.proxy at the device's IP to avoid CORS.

Release step (rare): build the frontend, generate the header, compile it into the firmware, flash or OTA. Either the CLI (npx svelteesp32 …) or the Vite plugin — same pipeline, the plugin just runs it automatically after vite build.


What's New

  • v3.2.0HTTP HEAD support on the psychic and async engines (always on, no flag): curl -I, health checks and uptime monitors now get the same status and headers as GET, with no body. psychic file routes are registered as HTTP_ANY and return 405 Method Not Allowed for non-GET/HEAD instead of falling through to index.html. Also fixes ETag/304 on the webserver engine, which never fired because Arduino WebServer was not told to retain the If-None-Match header. RFC-compliant ETags: the tag is now quoted (ETag: "387b88e345cc56ef") as RFC 9110 requires and truncated to 16 hex characters — 18 bytes per file instead of 64, on every response header and every conditional request — and If-None-Match is matched with strstr(), so browsers sending a comma-separated list or a weak W/"…" validator now get their 304 instead of a full 200
  • v3.1.0 — Removed handlebars, picomatch, and mime-types dependencies; C++ generation is now pure TypeScript with a built-in MIME type map and direct tinyglobby exclude handling. --cachetime-html--cachetimehtml, --cachetime-assets--cachetimeassets (CLI now matches RC file keys); --dry-run alias removed — use --dryrun. SVELTEESP32_URI_HANDLERS/SVELTEESP32_MAX_URI_HANDLERS (psychic/espidf) now reflect the exact registered route count, including the default / route and --spa catch-all
  • v3.0.0Vite 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.0BREAKING: 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

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:

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.

It runs on vite build only (apply: 'build'), so the dev server never triggers it and stopping vite dev cannot overwrite your header. It also runs last (enforce: 'post'), so files that other plugins emit into outDir — a PWA service worker, compressed assets, copied statics — are already on disk when the header is generated, whatever order the plugins appear in.

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.

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.

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:

# 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)

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

PsychicHttpServer server;

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

ESPAsyncWebServer

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

AsyncWebServer server(80);

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

Arduino WebServer (built-in, no dependencies)

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

WebServer server(80);

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

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

Native ESP-IDF

#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 | ESP-IDF

What Gets Generated

The generated header file includes everything your ESP needs:

//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[] = "\"387b88e345cc56ef\"";
static const char etag_assets_index_Soe6cpLA_css[] = "\"d4f23bc45ef67890\"";

// 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_ANY, [](PsychicRequest * request, PsychicResponse * response) {
    if (request->method() != HTTP_GET && request->method() != HTTP_HEAD) {
      response->setCode(405);
      response->addHeader("Allow", "GET, HEAD");
      return response->send();
    }

    // A 304 repeats the ETag and Cache-Control of the 200 it replaces (RFC 7232 4.1), so the
    // client can refresh the cached entry's freshness lifetime instead of revalidating every load.
    if (request->hasHeader("If-None-Match") &&
        strstr(request->header("If-None-Match").c_str(), etag_assets_index_KwubEIf__js) != nullptr) {
      response->setCode(304);
      response->addHeader("Cache-Control", "no-cache");
      response->addHeader("ETag", etag_assets_index_KwubEIf__js);
      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);
    if (request->method() != HTTP_HEAD) {
      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 | Library | | ------------------------ | -------------- | ---------------------------- | --------------- | ---------------------------------------------------------------------------- | | PsychicHttpServer V2 | -e psychic | Maximum performance | ESP32 only | PsychicHttp 2.2.0, 3.1.1 | | ESPAsyncWebServer | -e async | Cross-platform compatibility | ESP32 + ESP8266 | ESPAsyncWebServer v3.11.1 | | Arduino WebServer | -e webserver | No dependencies, simplicity | ESP32 only | (built-in) | | Native ESP-IDF | -e espidf | Pure ESP-IDF projects | ESP32 only | (built-in) |

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

Note: For native ESP-IDF, configure httpd_config_t.max_uri_handlers. The generated header provides SVELTEESP32_URI_HANDLERS (the exact number of routes registered, accounting for the default / route and the --spa catch-all handler) and SVELTEESP32_MAX_URI_HANDLERS (SVELTEESP32_URI_HANDLERS + 5 safety margin for your own custom routes) for use directly in your sketch.

PsychicHttp does not need this: since 3.x it registers one wildcard esp-idf handler per HTTP method and routes endpoints internally, overwriting server.config.max_uri_handlers in start(). Assigning it has no effect. The two defines are still emitted for the psychic engine, but they are informational only.


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.

The tag is the file's SHA256 truncated to 16 hex characters and wrapped in quotes, exactly as RFC 9110 requires (opaque-tag = DQUOTE *etagc DQUOTE) — unquoted tags are mishandled by some proxies and stricter clients:

ETag: "387b88e345cc56ef"

16 hex characters is 64 bits of collision resistance, far more than enough for the few dozen files that fit in flash, and it costs 18 bytes per file instead of 64 — on every response header and every conditional request. The full 64-character hash is still written to the --manifest JSON, so change detection between builds is unaffected.

Validation uses a substring match rather than string equality, so a browser sending a comma-separated list or a weak validator still gets its 304:

If-None-Match: W/"387b88e345cc56ef"
If-None-Match: "0badcafe0badcafe", "387b88e345cc56ef"

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:

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) |

Cache-Control is independent of --etag: it is emitted on every response in every etag mode. --cachetime* therefore still applies with --etag=never, and an --etag=compiler build compiled without -D SVELTEESP32_ENABLE_ETAG keeps its caching — only the ETag header itself is gated. The two headers do different jobs: Cache-Control sets how long a response stays fresh, ETag is the validator used to revalidate it once it goes stale.

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:

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

File Exclusion

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

# 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:

npx svelteesp32 -s ./admin-dist -o ./admin.h --basepath=/admin
npx svelteesp32 -s ./user-dist -o ./user.h --basepath=/app
#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 (and, on psychic/async, HEAD) requests fall through to index.html:

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/*", ...) registered for HTTP_GET and HTTP_HEAD 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.

HEAD Request Support

curl -I, health checks and most uptime monitors send HEAD, not GET. Routes generated for the psychic and async engines answer HEAD alongside GET — same status code, same headers (Content-Type, Content-Encoding, ETag, Cache-Control), no body. There is no flag: it is always on, and it costs no extra route on either engine.

| Engine | HEAD | How | | ----------- | ---- | ------------------------------------------------------------------------------------------------ | | psychic | yes | Routes registered as HTTP_ANY; the handler serves GET/HEAD and returns 405 for anything else | | async | yes | Routes registered as HTTP_GET \| HTTP_HEAD (one handler, method is a bitmask) | | webserver | no | Registering HEAD would need a second handler per file — see below | | espidf | no | httpd_uri_t.method holds a single method, so HEAD would double the handler table — see below |

Two behaviours worth knowing about:

  • Content-Length is 0 on HEAD responses. ESP-IDF's httpd_resp_send() derives Content-Length from the buffer it is given, and there is no way to override it, so a body-less response necessarily reports zero. Async reports 0 too, for consistency. This is technically at odds with RFC 9110 §8.6, which wants the length the GET would have returned. In practice curl -I and uptime monitors read the status line, so it does not matter — but if you are diffing headers between GET and HEAD, that is the field that will differ.
  • psychic returns 405 Method Not Allowed (with an Allow: GET, HEAD header) for POST/PUT/DELETE to a static file path. Previously such requests fell through psychic's 404 path and were answered by defaultEndpoint — i.e. POST /app.js served index.html with a 200.

For webserver and espidf, HEAD still returns 404. Add it yourself next to the generated routes:

// Arduino WebServer — reuse the generated route's URI, send headers with no body
server->on("/index.html", HTTP_HEAD, [server]() {
  server->setContentLength(sizeof(datagzip_index_html));
  server->sendHeader("Content-Encoding", "gzip");
  server->send(200, "text/html", "");
});
/* ESP-IDF — a second httpd_uri_t reusing the generated handler.
   Remember to raise httpd_config_t.max_uri_handlers to cover the extra entries. */
static const httpd_uri_t head_INDEX_HTML = {
    .uri = "/index.html",
    .method = HTTP_HEAD,
    .handler = file_handler_INDEX_HTML,
};
httpd_register_uri_handler(server, &head_INDEX_HTML);

Note that the ESP-IDF handler above will still write a body, since esp_http_server does not suppress it for HEAD. Guard the httpd_resp_send() call on req->method != HTTP_HEAD if that matters to you.

Analyze Mode (CI Size Budget Checks)

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

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):

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:

{
  "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:

#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, and (psychic/espidf only) SVELTEESP32_URI_HANDLERS, SVELTEESP32_MAX_URI_HANDLERS

Runtime File Manifest

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

// 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):

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:

{
  "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:

npx svelteesp32

npm Variable Interpolation

Sync versions and names automatically from your package.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

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.

# 1. Build your frontend
npm run build

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

Note: ETag validation works on Arduino WebServer — the generated header calls server->collectAllHeaders() for you, which is what makes the incoming If-None-Match available to the 304 check. (WebServer only retains request headers it has been told to collect.) Remember to call server.handleClient() in your loop().

#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

[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
lib_deps =
    https://github.com/ESP32Async/ESPAsyncWebServer#v3.11.2
extra_scripts = pre:scripts/build_frontend.py

scripts/build_frontend.py

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)

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

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

void app_main(void) {
    // ... WiFi init ...
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    config.max_uri_handlers = SVELTEESP32_MAX_URI_HANDLERS;  // already defined in the header, includes a safety margin
    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

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?

npm install -D svelteesp32