From 61c8a04d22406a90821ae12a266ac943caf8b51a Mon Sep 17 00:00:00 2001 From: BCsabaEngine Date: Thu, 11 Dec 2025 20:26:41 +0100 Subject: [PATCH 1/4] feat: error messages --- CHANGELOG.md | 56 ++++++++ package-lock.json | 4 +- package.json | 2 +- src/commandLine.ts | 19 ++- src/errorMessages.ts | 179 ++++++++++++++++++++++++ src/file.ts | 11 ++ src/index.ts | 5 + test/unit/commandLine.test.ts | 65 +++++++-- test/unit/errorMessages.test.ts | 238 ++++++++++++++++++++++++++++++++ test/unit/file.test.ts | 102 +++++++++++++- 10 files changed, 665 insertions(+), 16 deletions(-) create mode 100644 src/errorMessages.ts create mode 100644 test/unit/errorMessages.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 876df0d..bcd8668 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,61 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.13.1] - 2025-12-11 + +### Added + +- **Enhanced Error Messages with Framework-Specific Hints**: Comprehensive error messages with actionable "How to fix" guidance for all 4 engines (psychic, psychic2, async, espidf) + - **Missing index.html Validation**: Automatic check for default entry point with engine-specific routing examples + - Framework-specific hints: `server->defaultEndpoint` (psychic/psychic2), `server.on("/")` (async), `httpd_register_uri_handler` (espidf) + - Detects index.html/index.htm in root or subdirectories + - `--no-index-check` flag to bypass validation for API-only applications + - Clear explanation of why index.html matters and alternative solutions + - **Invalid Engine Error**: Enhanced error message with complete engine reference + - Lists all 4 valid engines with descriptions and platform compatibility + - Example commands and RC file format + - Documentation link for further reference + - **Sourcepath Not Found Error**: Detailed troubleshooting guidance + - Build tool configuration hints (Vite, Webpack, Rollup) + - Framework-specific build commands (Svelte, React, Vue, Angular) + - Shows current directory and resolved path for debugging + - Separate messages for "not found" vs "not a directory" scenarios + - **max_uri_handlers Configuration Hints**: Console output after successful generation + - Only shown for engines that require it (psychic, psychic2, espidf) + - Calculates recommended value: `routeCount + 5` (safety margin) + - Engine-specific configuration examples with code snippets + - Lists runtime symptoms to watch for (404 errors, ESP_ERR_HTTPD_HANDLERS_FULL) +- New `src/errorMessages.ts` module (~180 lines) with pure, testable functions: + - `getMissingIndexError()` - Engine-specific index.html error messages + - `getInvalidEngineError()` - Comprehensive invalid engine guidance + - `getSourcepathNotFoundError()` - Build tool and framework hints + - `getMaxUriHandlersHint()` - Configuration guidance for handler limits + - Consistent multi-line error format with color-coded sections +- 32 comprehensive unit tests in `test/unit/errorMessages.test.ts` with 100% coverage: + - Tests for all 4 engines (psychic, psychic2, async, espidf) + - Message content validation (error title, explanations, hints) + - Edge cases (unknown engines, empty strings) + - Framework-specific code snippet verification +- Enhanced test suite with 156 total tests passing: + - Updated `test/unit/commandLine.test.ts` with enhanced error validation + - Updated `test/unit/file.test.ts` with index.html validation tests + - All tests use proper TypeScript types (replaced `any` with `vi.mocked()`) + +### Changed + +- Improved developer experience with clear, actionable error messages +- Error messages now include framework-specific code examples +- Console output uses color-coded sections for better readability +- Validation happens early with helpful guidance before processing begins +- Updated `src/commandLine.ts` to use enhanced error messages (lines 84-85, 559-567) +- Updated `src/file.ts` with index.html validation (lines 117-125) +- Updated `src/index.ts` to show max_uri_handlers hints (lines 150-153) + +### Fixed + +- Linting errors: renamed `currentDir` to `currentDirectory` (unicorn/prevent-abbreviations) +- Test type safety: replaced explicit `any` types with `vi.mocked()` helper + ## [1.13.0] - 2025-12-04 ### Added @@ -374,6 +429,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - CLI interface with `-s`, `-e`, `-o` options - `index.html` automatic default route handling +[1.13.1]: https://github.com/BCsabaEngine/svelteesp32/compare/v1.13.0...v1.13.1 [1.13.0]: https://github.com/BCsabaEngine/svelteesp32/compare/v1.12.1...v1.13.0 [1.12.1]: https://github.com/BCsabaEngine/svelteesp32/compare/v1.12.0...v1.12.1 [1.12.0]: https://github.com/BCsabaEngine/svelteesp32/compare/v1.11.0...v1.12.0 diff --git a/package-lock.json b/package-lock.json index af25c46..d30f2fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "svelteesp32", - "version": "1.13.0", + "version": "1.13.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "svelteesp32", - "version": "1.13.0", + "version": "1.13.1", "license": "ISC", "dependencies": { "handlebars": "^4.7.8", diff --git a/package.json b/package.json index 6015d18..1c1907b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "svelteesp32", - "version": "1.13.0", + "version": "1.13.1", "description": "Convert Svelte (or any frontend) JS application to serve it from ESP32 webserver (PsychicHttp)", "author": "BCsabaEngine", "license": "ISC", diff --git a/src/commandLine.ts b/src/commandLine.ts index 9583766..f237261 100644 --- a/src/commandLine.ts +++ b/src/commandLine.ts @@ -3,6 +3,7 @@ import { homedir } from 'node:os'; import path from 'node:path'; import { cyanLog, yellowLog } from './consoleColor'; +import { getInvalidEngineError, getSourcepathNotFoundError } from './errorMessages'; interface ICopyFilesArguments { engine: 'psychic' | 'psychic2' | 'async' | 'espidf'; @@ -16,6 +17,7 @@ interface ICopyFilesArguments { created: boolean; version: string; exclude: string[]; + noIndexCheck?: boolean; help?: boolean; } @@ -79,7 +81,8 @@ RC File: function validateEngine(value: string): 'psychic' | 'psychic2' | 'async' | 'espidf' { if (value === 'psychic' || value === 'psychic2' || value === 'async' || value === 'espidf') return value; - throw new Error(`Invalid engine: ${value}`); + console.error(getInvalidEngineError(value)); + process.exit(1); } function validateTriState(value: string, name: string): 'true' | 'false' | 'compiler' { @@ -414,6 +417,11 @@ function parseArguments(): ICopyFilesArguments { continue; } + if (argument === '--no-index-check') { + result.noIndexCheck = true; + continue; + } + // Handle -flag value format if (argument.startsWith('-') && !argument.startsWith('--')) { const flag = argument.slice(1); @@ -548,8 +556,13 @@ export function formatConfiguration(cmdLine: ICopyFilesArguments): string { export const cmdLine = parseArguments(); -if (!existsSync(cmdLine.sourcepath) || !statSync(cmdLine.sourcepath).isDirectory()) { - console.error(`Directory ${cmdLine.sourcepath} not exists or not a directory`); +if (!existsSync(cmdLine.sourcepath)) { + console.error(getSourcepathNotFoundError(cmdLine.sourcepath, 'not_found')); + process.exit(1); +} + +if (!statSync(cmdLine.sourcepath).isDirectory()) { + console.error(getSourcepathNotFoundError(cmdLine.sourcepath, 'not_directory')); process.exit(1); } diff --git a/src/errorMessages.ts b/src/errorMessages.ts new file mode 100644 index 0000000..0655e16 --- /dev/null +++ b/src/errorMessages.ts @@ -0,0 +1,179 @@ +import path from 'node:path'; + +import { cyanLog, redLog, yellowLog } from './consoleColor'; + +/** + * Get human-readable engine name + */ +function getEngineName(engine: string): string { + const names: Record = { + psychic: 'PsychicHttpServer', + psychic2: 'PsychicHttpServer V2', + async: 'ESPAsyncWebServer', + espidf: 'ESP-IDF' + }; + return names[engine] ?? engine; +} + +/** + * Error: Missing index.html or index.htm + */ +export function getMissingIndexError(engine: string): string { + const hints: Record = { + psychic: ` 1. Add an index.html file to your source directory + 2. The file will automatically be set as the default route ("/") + 3. PsychicHttpServer uses: server->defaultEndpoint = ...`, + + psychic2: ` 1. Add an index.html file to your source directory + 2. The file will automatically be set as the default route ("/") + 3. PsychicHttpServer V2 uses: server->defaultEndpoint = ...`, + + async: ` 1. Add an index.html file to your source directory + 2. The file will automatically create a "/" route handler + 3. ESPAsyncWebServer uses: server.on("/", HTTP_GET, ...)`, + + espidf: ` 1. Add an index.html file to your source directory + 2. The file will register both "/" and "/index.html" routes + 3. ESP-IDF uses: httpd_register_uri_handler(server, &route_def_...)` + }; + + const hint = hints[engine] ?? hints['psychic']; + + return ( + redLog('[ERROR] No index.html or index.htm found in source files') + + ` + +Why this matters: + Web applications typically need a default entry point. Without index.html, + users visiting http://your-esp32/ will get a 404 error. + +How to fix (for ${getEngineName(engine)}): +${hint} + +Alternative: + If you use a different entry point (e.g., main.html), you can add --no-index-check flag, + but users must navigate to http://your-esp32/main.html explicitly.` + ); +} + +/** + * Error: Invalid engine specified + */ +export function getInvalidEngineError(attempted: string): string { + return ( + redLog(`[ERROR] Invalid engine: '${attempted}'`) + + ` + +Valid engines are: + ${cyanLog('• psychic')} - PsychicHttpServer (ESP32 only, fastest performance) + ${cyanLog('• psychic2')} - PsychicHttpServer V2 (ESP32 only, modern API) + ${cyanLog('• async')} - ESPAsyncWebServer (ESP32/ESP8266 compatible) + ${cyanLog('• espidf')} - Native ESP-IDF web server (ESP32 only, no Arduino) + +How to fix: + npx svelteesp32 --engine=psychic --sourcepath=./dist + +Example RC file (.svelteesp32rc.json): + { + "engine": "psychic", + "sourcepath": "./dist" + } + +Documentation: https://github.com/hpieroni/svelteesp32#readme` + ); +} + +/** + * Error: Source path not found or not a directory + */ +export function getSourcepathNotFoundError(sourcepath: string, reason: 'not_found' | 'not_directory'): string { + if (reason === 'not_directory') + return ( + redLog(`[ERROR] Source path is not a directory: '${sourcepath}'`) + + ` + +The --sourcepath option must point to a directory containing your built web files, +not an individual file. + +How to fix: + npx svelteesp32 --sourcepath=./dist --engine=psychic` + ); + + const resolvedPath = path.resolve(sourcepath); + const currentDirectory = process.cwd(); + + return ( + redLog(`[ERROR] Source directory not found: '${sourcepath}'`) + + ` + +Why this matters: + SvelteESP32 needs your compiled web assets (HTML, CSS, JS) to convert them + into C++ header files for the ESP32. + +How to fix: + 1. Build your frontend application first: + • Svelte: npm run build + • React: npm run build + • Vue: npm run build + • Angular: ng build + + 2. Verify the build output directory exists: + ${cyanLog(`ls -la ${sourcepath}`)} + + 3. Check your build tool configuration: + • Vite: vite.config.js → build.outDir + • Webpack: webpack.config.js → output.path + • Rollup: rollup.config.js → output.dir + + 4. Update your svelteesp32 command to match: + ${cyanLog(`npx svelteesp32 --sourcepath=./build --engine=psychic`)} + +Current directory: ${currentDirectory} +Attempted path: ${resolvedPath} (resolved)` + ); +} + +/** + * Hint: max_uri_handlers configuration (console output, not an error) + */ +export function getMaxUriHandlersHint(engine: string, routeCount: number): string { + const recommended = routeCount + 5; + + const hints: Record = { + psychic: `PsychicHttpServer server; + server.config.max_uri_handlers = ${recommended}; // Default is 8, you need at least ${routeCount} + initSvelteStaticFiles(&server); + server.listen(80);`, + + psychic2: `PsychicHttpServer server; + server.config.max_uri_handlers = ${recommended}; // Default is 8, you need at least ${routeCount} + initSvelteStaticFiles(&server); + server.listen(80);`, + + espidf: `httpd_config_t config = HTTPD_DEFAULT_CONFIG(); + config.max_uri_handlers = ${recommended}; // Default is 8, you need at least ${routeCount} + httpd_handle_t server = NULL; + httpd_start(&server, &config); + initSvelteStaticFiles(server);` + }; + + const hint = hints[engine]; + if (!hint) return ''; // No hint for async engine (no limit) + + return ( + yellowLog('[CONFIG TIP] max_uri_handlers configuration') + + ` + +Your generated code includes ${routeCount} routes. Make sure your server can handle them: + +For ${getEngineName(engine)}: + ${hint} + +Recommended formula: max_uri_handlers = file_count + 5 (safety margin) + +Runtime symptoms if too low: + • Routes not registered (HTTP 404 errors) + • ESP_ERR_HTTPD_HANDLERS_FULL error in logs + • Some files load, others don't (random behavior)` + ); +} diff --git a/src/file.ts b/src/file.ts index 63483ab..4f05e7e 100644 --- a/src/file.ts +++ b/src/file.ts @@ -7,6 +7,7 @@ import { globSync } from 'tinyglobby'; import { cmdLine } from './commandLine'; import { cyanLog, redLog, yellowLog } from './consoleColor'; +import { getMissingIndexError } from './errorMessages'; /** * Find files with identical content based on SHA256 hash @@ -113,5 +114,15 @@ export const getFiles = (): Map => { const duplicates = findSimilarFiles(result); for (const sameFiles of duplicates) console.log(yellowLog(` ${sameFiles.join(', ')} files look like identical`)); + // Check for index.html or index.htm (in root or subdirectories) + const hasIndex = [...result.keys()].some( + (f) => f === 'index.html' || f === 'index.htm' || f.endsWith('/index.html') || f.endsWith('/index.htm') + ); + + if (!hasIndex && !cmdLine.noIndexCheck) { + console.error('\n' + getMissingIndexError(cmdLine.engine)); + process.exit(1); + } + return result; }; diff --git a/src/index.ts b/src/index.ts index 551cf10..a166e31 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ import { lookup as mimeLookup } from 'mime-types'; import { cmdLine } from './commandLine'; import { greenLog, yellowLog } from './consoleColor'; import { CppCodeSource, CppCodeSources, ExtensionGroups, getCppCode } from './cppCode'; +import { getMaxUriHandlersHint } from './errorMessages'; import { getFiles } from './file'; // Compression thresholds @@ -145,3 +146,7 @@ console.log( ); console.log(`${cmdLine.outputfile} ${Math.round(cppFile.length / 1024)}kB size`); + +// Show max_uri_handlers hint for applicable engines +if (cmdLine.engine === 'psychic' || cmdLine.engine === 'psychic2' || cmdLine.engine === 'espidf') + console.log('\n' + getMaxUriHandlersHint(cmdLine.engine, sources.length)); diff --git a/test/unit/commandLine.test.ts b/test/unit/commandLine.test.ts index 59cdc31..fd2efab 100644 --- a/test/unit/commandLine.test.ts +++ b/test/unit/commandLine.test.ts @@ -110,6 +110,14 @@ describe('commandLine', () => { expect(cmdLine.created).toBe(true); }); + it('should parse --no-index-check flag', async () => { + process.argv = ['node', 'script.js', '--sourcepath=/test/dist', '--no-index-check']; + + const { cmdLine } = await import('../../src/commandLine'); + + expect(cmdLine.noIndexCheck).toBe(true); + }); + it('should parse version, espmethod, define, and cachetime', async () => { process.argv = [ 'node', @@ -219,10 +227,19 @@ describe('commandLine', () => { }); describe('validation', () => { - it('should validate engine values', async () => { + it('should show enhanced error for invalid engine', async () => { process.argv = ['node', 'script.js', '--engine=invalid', '--sourcepath=/test/dist']; - await expect(import('../../src/commandLine')).rejects.toThrow('Invalid engine: invalid'); + await import('../../src/commandLine').catch(() => {}); + + expect(console.error).toHaveBeenCalled(); + const errorMessage = vi.mocked(console.error).mock.calls[0]?.[0]; + expect(errorMessage).toContain('[ERROR]'); + expect(errorMessage).toContain('Invalid engine'); + expect(errorMessage).toContain("'invalid'"); + expect(errorMessage).toContain('psychic'); + expect(errorMessage).toContain('async'); + expect(process.exit).toHaveBeenCalledWith(1); }); it('should accept all valid engine values', async () => { @@ -296,7 +313,7 @@ describe('commandLine', () => { }); describe('directory validation', () => { - it('should exit when source directory does not exist', async () => { + it('should show enhanced error when source directory does not exist', async () => { process.argv = ['node', 'script.js', '--sourcepath=/nonexistent/path']; const fsModule = await import('node:fs'); @@ -304,11 +321,16 @@ describe('commandLine', () => { await import('../../src/commandLine').catch(() => {}); - expect(console.error).toHaveBeenCalledWith('Directory /nonexistent/path not exists or not a directory'); + expect(console.error).toHaveBeenCalled(); + const errorMessage = vi.mocked(console.error).mock.calls[0]?.[0]; + expect(errorMessage).toContain('[ERROR]'); + expect(errorMessage).toContain('Source directory not found'); + expect(errorMessage).toContain('/nonexistent/path'); + expect(errorMessage).toContain('npm run build'); expect(process.exit).toHaveBeenCalledWith(1); }); - it('should exit when source path is not a directory', async () => { + it('should show enhanced error when source path is not a directory', async () => { process.argv = ['node', 'script.js', '--sourcepath=/test/file.txt']; const fsModule = await import('node:fs'); @@ -317,7 +339,11 @@ describe('commandLine', () => { await import('../../src/commandLine').catch(() => {}); - expect(console.error).toHaveBeenCalledWith('Directory /test/file.txt not exists or not a directory'); + expect(console.error).toHaveBeenCalled(); + const errorMessage = vi.mocked(console.error).mock.calls[0]?.[0]; + expect(errorMessage).toContain('[ERROR]'); + expect(errorMessage).toContain('not a directory'); + expect(errorMessage).toContain('/test/file.txt'); expect(process.exit).toHaveBeenCalledWith(1); }); }); @@ -532,7 +558,14 @@ describe('commandLine', () => { process.argv = ['node', 'script.js']; - await expect(import('../../src/commandLine')).rejects.toThrow('Invalid engine: invalid'); + await import('../../src/commandLine').catch(() => {}); + + expect(console.error).toHaveBeenCalled(); + const errorMessage = vi.mocked(console.error).mock.calls[0]?.[0]; + expect(errorMessage).toContain('[ERROR]'); + expect(errorMessage).toContain('Invalid engine'); + expect(errorMessage).toContain("'invalid'"); + expect(process.exit).toHaveBeenCalledWith(1); }); it('should validate etag tri-state values from RC file', async () => { @@ -832,8 +865,15 @@ describe('commandLine', () => { }); it('should throw error when variables present but package.json not found', async () => { + process.argv = ['node', 'script.js', '--sourcepath=/test/dist']; + const fsModule = await import('node:fs'); - vi.mocked(fsModule.existsSync).mockReturnValue(false); + vi.mocked(fsModule.existsSync).mockImplementation((path: string) => { + if (path === '/test/dist') return true; // sourcepath exists + if (path.includes('package.json')) return false; // package.json does not exist + return false; + }); + vi.mocked(fsModule.statSync).mockReturnValue({ isDirectory: () => true } as fs.Stats); const { interpolateNpmVariables } = await import('../../src/commandLine'); const config = { @@ -846,8 +886,15 @@ describe('commandLine', () => { }); it('should list affected fields in error message', async () => { + process.argv = ['node', 'script.js', '--sourcepath=/test/dist']; + const fsModule = await import('node:fs'); - vi.mocked(fsModule.existsSync).mockReturnValue(false); + vi.mocked(fsModule.existsSync).mockImplementation((path: string) => { + if (path === '/test/dist') return true; // sourcepath exists + if (path.includes('package.json')) return false; // package.json does not exist + return false; + }); + vi.mocked(fsModule.statSync).mockReturnValue({ isDirectory: () => true } as fs.Stats); const { interpolateNpmVariables } = await import('../../src/commandLine'); const config = { diff --git a/test/unit/errorMessages.test.ts b/test/unit/errorMessages.test.ts new file mode 100644 index 0000000..eaec41e --- /dev/null +++ b/test/unit/errorMessages.test.ts @@ -0,0 +1,238 @@ +import { describe, expect, it } from 'vitest'; + +import { + getInvalidEngineError, + getMaxUriHandlersHint, + getMissingIndexError, + getSourcepathNotFoundError +} from '../../src/errorMessages'; + +describe('errorMessages', () => { + describe('getMissingIndexError', () => { + it('should include error title', () => { + const result = getMissingIndexError('psychic'); + expect(result).toContain('[ERROR]'); + expect(result).toContain('No index.html or index.htm found'); + }); + + it('should include why this matters section', () => { + const result = getMissingIndexError('psychic'); + expect(result).toContain('Why this matters:'); + expect(result).toContain('default entry point'); + expect(result).toContain('404 error'); + }); + + it('should include engine-specific hint for psychic', () => { + const result = getMissingIndexError('psychic'); + expect(result).toContain('PsychicHttpServer'); + expect(result).toContain('server->defaultEndpoint'); + }); + + it('should include engine-specific hint for psychic2', () => { + const result = getMissingIndexError('psychic2'); + expect(result).toContain('PsychicHttpServer V2'); + expect(result).toContain('server->defaultEndpoint'); + }); + + it('should include engine-specific hint for async', () => { + const result = getMissingIndexError('async'); + expect(result).toContain('ESPAsyncWebServer'); + expect(result).toContain('server.on'); + }); + + it('should include engine-specific hint for espidf', () => { + const result = getMissingIndexError('espidf'); + expect(result).toContain('ESP-IDF'); + expect(result).toContain('httpd_register_uri_handler'); + }); + + it('should include alternative solution', () => { + const result = getMissingIndexError('psychic'); + expect(result).toContain('Alternative:'); + expect(result).toContain('--no-index-check'); + }); + + it('should fallback to psychic hint for unknown engine', () => { + const result = getMissingIndexError('unknown' as 'psychic'); + expect(result).toContain('server->defaultEndpoint'); + }); + }); + + describe('getInvalidEngineError', () => { + it('should show attempted value', () => { + const result = getInvalidEngineError('invalid'); + expect(result).toContain('[ERROR]'); + expect(result).toContain("'invalid'"); + }); + + it('should list all valid engines', () => { + const result = getInvalidEngineError('foo'); + expect(result).toContain('psychic'); + expect(result).toContain('psychic2'); + expect(result).toContain('async'); + expect(result).toContain('espidf'); + }); + + it('should include engine descriptions', () => { + const result = getInvalidEngineError('foo'); + expect(result).toContain('PsychicHttpServer'); + expect(result).toContain('ESPAsyncWebServer'); + expect(result).toContain('ESP-IDF'); + expect(result).toContain('fastest performance'); + expect(result).toContain('ESP32/ESP8266 compatible'); + }); + + it('should include example command', () => { + const result = getInvalidEngineError('foo'); + expect(result).toContain('npx svelteesp32'); + expect(result).toContain('--engine='); + expect(result).toContain('--sourcepath='); + }); + + it('should include RC file example', () => { + const result = getInvalidEngineError('foo'); + expect(result).toContain('.svelteesp32rc.json'); + expect(result).toContain('"engine"'); + expect(result).toContain('"sourcepath"'); + }); + + it('should include documentation link', () => { + const result = getInvalidEngineError('foo'); + expect(result).toContain('Documentation:'); + expect(result).toContain('github.com'); + }); + }); + + describe('getSourcepathNotFoundError', () => { + describe('not_found variant', () => { + it('should show error with path', () => { + const result = getSourcepathNotFoundError('./dist', 'not_found'); + expect(result).toContain('[ERROR]'); + expect(result).toContain('./dist'); + expect(result).toContain('not found'); + }); + + it('should include why this matters section', () => { + const result = getSourcepathNotFoundError('./dist', 'not_found'); + expect(result).toContain('Why this matters:'); + expect(result).toContain('compiled web assets'); + expect(result).toContain('C++ header files'); + }); + + it('should include build commands for all frameworks', () => { + const result = getSourcepathNotFoundError('./dist', 'not_found'); + expect(result).toContain('Svelte:'); + expect(result).toContain('React:'); + expect(result).toContain('Vue:'); + expect(result).toContain('Angular:'); + expect(result).toContain('npm run build'); + expect(result).toContain('ng build'); + }); + + it('should include build tool configuration hints', () => { + const result = getSourcepathNotFoundError('./dist', 'not_found'); + expect(result).toContain('Vite:'); + expect(result).toContain('Webpack:'); + expect(result).toContain('Rollup:'); + expect(result).toContain('vite.config.js'); + expect(result).toContain('webpack.config.js'); + expect(result).toContain('rollup.config.js'); + }); + + it('should include current and resolved paths', () => { + const result = getSourcepathNotFoundError('./dist', 'not_found'); + expect(result).toContain('Current directory:'); + expect(result).toContain('Attempted path:'); + expect(result).toContain('(resolved)'); + }); + + it('should include ls command suggestion', () => { + const result = getSourcepathNotFoundError('./dist', 'not_found'); + expect(result).toContain('ls -la'); + }); + }); + + describe('not_directory variant', () => { + it('should show simpler error message', () => { + const result = getSourcepathNotFoundError('./dist/index.html', 'not_directory'); + expect(result).toContain('[ERROR]'); + expect(result).toContain('./dist/index.html'); + expect(result).toContain('not a directory'); + }); + + it('should explain sourcepath must be a directory', () => { + const result = getSourcepathNotFoundError('./dist/index.html', 'not_directory'); + expect(result).toContain('must point to a directory'); + expect(result).toContain('not an individual file'); + }); + + it('should include example fix', () => { + const result = getSourcepathNotFoundError('./dist/index.html', 'not_directory'); + expect(result).toContain('npx svelteesp32'); + expect(result).toContain('--sourcepath=./dist'); + }); + }); + }); + + describe('getMaxUriHandlersHint', () => { + it('should calculate recommended handler count', () => { + const result = getMaxUriHandlersHint('psychic', 10); + expect(result).toContain('15'); // 10 + 5 + expect(result).toContain('at least 10'); + }); + + it('should include route count in message', () => { + const result = getMaxUriHandlersHint('psychic', 10); + expect(result).toContain('10 routes'); + }); + + it('should show engine-specific code for psychic', () => { + const result = getMaxUriHandlersHint('psychic', 10); + expect(result).toContain('[CONFIG TIP]'); + expect(result).toContain('PsychicHttpServer'); + expect(result).toContain('server.config.max_uri_handlers'); + expect(result).toContain('initSvelteStaticFiles'); + expect(result).toContain('server.listen(80)'); + }); + + it('should show engine-specific code for psychic2', () => { + const result = getMaxUriHandlersHint('psychic2', 12); + expect(result).toContain('PsychicHttpServer'); + expect(result).toContain('server.config.max_uri_handlers = 17'); + expect(result).toContain('at least 12'); + }); + + it('should show engine-specific code for espidf', () => { + const result = getMaxUriHandlersHint('espidf', 8); + expect(result).toContain('ESP-IDF'); + expect(result).toContain('HTTPD_DEFAULT_CONFIG'); + expect(result).toContain('config.max_uri_handlers = 13'); + expect(result).toContain('httpd_start'); + }); + + it('should return empty string for async engine', () => { + const result = getMaxUriHandlersHint('async', 10); + expect(result).toBe(''); + }); + + it('should return empty string for unknown engine', () => { + const result = getMaxUriHandlersHint('unknown' as 'psychic', 10); + expect(result).toBe(''); + }); + + it('should include recommended formula', () => { + const result = getMaxUriHandlersHint('psychic', 10); + expect(result).toContain('Recommended formula'); + expect(result).toContain('file_count + 5'); + expect(result).toContain('safety margin'); + }); + + it('should include runtime symptoms', () => { + const result = getMaxUriHandlersHint('psychic', 10); + expect(result).toContain('Runtime symptoms'); + expect(result).toContain('HTTP 404'); + expect(result).toContain('ESP_ERR_HTTPD_HANDLERS_FULL'); + expect(result).toContain('random behavior'); + }); + }); +}); diff --git a/test/unit/file.test.ts b/test/unit/file.test.ts index f853e13..070402f 100644 --- a/test/unit/file.test.ts +++ b/test/unit/file.test.ts @@ -17,7 +17,8 @@ vi.mock('../../src/commandLine', () => ({ created: false, version: 'test-version', prefix: 'SVELTEESP32', - exclude: [] + exclude: [], + noIndexCheck: false } })); @@ -35,6 +36,12 @@ describe('file', () => { }); describe('getFiles', () => { + beforeEach(async () => { + // Set noIndexCheck to true for most tests (index.html validation has its own section) + const commandLineModule = await import('../../src/commandLine'); + vi.mocked(commandLineModule.cmdLine).noIndexCheck = true; + }); + it('should read all files from source directory', () => { const mockFiles = ['index.html', 'style.css', 'script.js']; const mockContent = Buffer.from('test content'); @@ -153,6 +160,7 @@ describe('file', () => { // Reset cmdLine mock before each test const commandLineModule = await import('../../src/commandLine'); vi.mocked(commandLineModule.cmdLine).exclude = []; + vi.mocked(commandLineModule.cmdLine).noIndexCheck = true; // Skip index.html validation for these tests }); it('should exclude files matching simple glob pattern', async () => { @@ -278,4 +286,96 @@ describe('file', () => { expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('README.md')); }); }); + + describe('index.html validation', () => { + const originalExit = process.exit; + + beforeEach(async () => { + process.exit = vi.fn() as never; + vi.spyOn(console, 'error').mockImplementation(() => {}); + + // Reset noIndexCheck to false + const commandLineModule = await import('../../src/commandLine'); + vi.mocked(commandLineModule.cmdLine).noIndexCheck = false; + }); + + afterEach(() => { + process.exit = originalExit; + }); + + it('should fail if no index.html or index.htm exists', async () => { + const mockFiles = ['style.css', 'script.js']; + const mockContent = Buffer.from('test content'); + + vi.mocked(tinyglobby.globSync).mockReturnValue(mockFiles); + vi.mocked(fs.readFileSync).mockReturnValue(mockContent); + + getFiles(); + + expect(console.error).toHaveBeenCalled(); + const errorMessage = vi.mocked(console.error).mock.calls[0]?.[0]; + expect(errorMessage).toContain('[ERROR]'); + expect(errorMessage).toContain('No index.html or index.htm found'); + expect(process.exit).toHaveBeenCalledWith(1); + }); + + it('should pass if index.html exists', async () => { + const mockFiles = ['index.html', 'style.css']; + const mockContent = Buffer.from('test content'); + + vi.mocked(tinyglobby.globSync).mockReturnValue(mockFiles); + vi.mocked(fs.readFileSync).mockReturnValue(mockContent); + + const result = getFiles(); + + expect(result.size).toBe(2); + expect(console.error).not.toHaveBeenCalled(); + expect(process.exit).not.toHaveBeenCalled(); + }); + + it('should pass if index.htm exists', async () => { + const mockFiles = ['index.htm', 'style.css']; + const mockContent = Buffer.from('test content'); + + vi.mocked(tinyglobby.globSync).mockReturnValue(mockFiles); + vi.mocked(fs.readFileSync).mockReturnValue(mockContent); + + const result = getFiles(); + + expect(result.size).toBe(2); + expect(console.error).not.toHaveBeenCalled(); + expect(process.exit).not.toHaveBeenCalled(); + }); + + it('should skip validation if --no-index-check is true', async () => { + const mockFiles = ['style.css', 'script.js']; + const mockContent = Buffer.from('test content'); + + vi.mocked(tinyglobby.globSync).mockReturnValue(mockFiles); + vi.mocked(fs.readFileSync).mockReturnValue(mockContent); + + const commandLineModule = await import('../../src/commandLine'); + vi.mocked(commandLineModule.cmdLine).noIndexCheck = true; + + const result = getFiles(); + + expect(result.size).toBe(2); + expect(console.error).not.toHaveBeenCalled(); + expect(process.exit).not.toHaveBeenCalled(); + }); + + it('should pass if index.html is in subdirectory', async () => { + const mockFiles = ['assets/index.html', 'style.css']; + const mockContent = Buffer.from('test content'); + + vi.mocked(tinyglobby.globSync).mockReturnValue(mockFiles); + vi.mocked(fs.readFileSync).mockReturnValue(mockContent); + + const result = getFiles(); + + expect(result.size).toBe(2); + expect(console.error).not.toHaveBeenCalled(); + expect(process.exit).not.toHaveBeenCalled(); + }); + }); }); From 34d3ea66ab938a7fea277a5c32a28f2fdd310e68 Mon Sep 17 00:00:00 2001 From: BCsabaEngine Date: Thu, 11 Dec 2025 20:42:11 +0100 Subject: [PATCH 2/4] chore: deps --- package-lock.json | 378 +++++++++++++++++++++++----------------------- package.json | 6 +- 2 files changed, 188 insertions(+), 196 deletions(-) diff --git a/package-lock.json b/package-lock.json index d30f2fb..3e36413 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,10 +19,10 @@ }, "devDependencies": { "@types/mime-types": "^3.0.1", - "@types/node": "^24.10.1", + "@types/node": "^25.0.0", "@types/picomatch": "^4.0.2", - "@typescript-eslint/eslint-plugin": "^8.48.1", - "@typescript-eslint/parser": "^8.48.1", + "@typescript-eslint/eslint-plugin": "^8.49.0", + "@typescript-eslint/parser": "^8.49.0", "@vitest/coverage-v8": "^4.0.15", "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", @@ -126,9 +126,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.0.tgz", - "integrity": "sha512-KuZrd2hRjz01y5JK9mEBSD3Vj3mbCvemhT466rSuJYeE/hjuBrHfjjcjMdTm/sz7au+++sdbJZJmuBwQLuw68A==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz", + "integrity": "sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==", "cpu": [ "ppc64" ], @@ -143,9 +143,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.0.tgz", - "integrity": "sha512-j67aezrPNYWJEOHUNLPj9maeJte7uSMM6gMoxfPC9hOg8N02JuQi/T7ewumf4tNvJadFkvLZMlAq73b9uwdMyQ==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.1.tgz", + "integrity": "sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==", "cpu": [ "arm" ], @@ -160,9 +160,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.0.tgz", - "integrity": "sha512-CC3vt4+1xZrs97/PKDkl0yN7w8edvU2vZvAFGD16n9F0Cvniy5qvzRXjfO1l94efczkkQE6g1x0i73Qf5uthOQ==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz", + "integrity": "sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==", "cpu": [ "arm64" ], @@ -177,9 +177,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.0.tgz", - "integrity": "sha512-wurMkF1nmQajBO1+0CJmcN17U4BP6GqNSROP8t0X/Jiw2ltYGLHpEksp9MpoBqkrFR3kv2/te6Sha26k3+yZ9Q==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.1.tgz", + "integrity": "sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==", "cpu": [ "x64" ], @@ -194,9 +194,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.0.tgz", - "integrity": "sha512-uJOQKYCcHhg07DL7i8MzjvS2LaP7W7Pn/7uA0B5S1EnqAirJtbyw4yC5jQ5qcFjHK9l6o/MX9QisBg12kNkdHg==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz", + "integrity": "sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==", "cpu": [ "arm64" ], @@ -211,9 +211,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.0.tgz", - "integrity": "sha512-8mG6arH3yB/4ZXiEnXof5MK72dE6zM9cDvUcPtxhUZsDjESl9JipZYW60C3JGreKCEP+p8P/72r69m4AZGJd5g==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz", + "integrity": "sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==", "cpu": [ "x64" ], @@ -228,9 +228,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.0.tgz", - "integrity": "sha512-9FHtyO988CwNMMOE3YIeci+UV+x5Zy8fI2qHNpsEtSF83YPBmE8UWmfYAQg6Ux7Gsmd4FejZqnEUZCMGaNQHQw==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz", + "integrity": "sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==", "cpu": [ "arm64" ], @@ -245,9 +245,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.0.tgz", - "integrity": "sha512-zCMeMXI4HS/tXvJz8vWGexpZj2YVtRAihHLk1imZj4efx1BQzN76YFeKqlDr3bUWI26wHwLWPd3rwh6pe4EV7g==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz", + "integrity": "sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==", "cpu": [ "x64" ], @@ -262,9 +262,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.0.tgz", - "integrity": "sha512-t76XLQDpxgmq2cNXKTVEB7O7YMb42atj2Re2Haf45HkaUpjM2J0UuJZDuaGbPbamzZ7bawyGFUkodL+zcE+jvQ==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz", + "integrity": "sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==", "cpu": [ "arm" ], @@ -279,9 +279,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.0.tgz", - "integrity": "sha512-AS18v0V+vZiLJyi/4LphvBE+OIX682Pu7ZYNsdUHyUKSoRwdnOsMf6FDekwoAFKej14WAkOef3zAORJgAtXnlQ==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz", + "integrity": "sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==", "cpu": [ "arm64" ], @@ -296,9 +296,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.0.tgz", - "integrity": "sha512-Mz1jxqm/kfgKkc/KLHC5qIujMvnnarD9ra1cEcrs7qshTUSksPihGrWHVG5+osAIQ68577Zpww7SGapmzSt4Nw==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz", + "integrity": "sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==", "cpu": [ "ia32" ], @@ -313,9 +313,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.0.tgz", - "integrity": "sha512-QbEREjdJeIreIAbdG2hLU1yXm1uu+LTdzoq1KCo4G4pFOLlvIspBm36QrQOar9LFduavoWX2msNFAAAY9j4BDg==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz", + "integrity": "sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==", "cpu": [ "loong64" ], @@ -330,9 +330,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.0.tgz", - "integrity": "sha512-sJz3zRNe4tO2wxvDpH/HYJilb6+2YJxo/ZNbVdtFiKDufzWq4JmKAiHy9iGoLjAV7r/W32VgaHGkk35cUXlNOg==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz", + "integrity": "sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==", "cpu": [ "mips64el" ], @@ -347,9 +347,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.0.tgz", - "integrity": "sha512-z9N10FBD0DCS2dmSABDBb5TLAyF1/ydVb+N4pi88T45efQ/w4ohr/F/QYCkxDPnkhkp6AIpIcQKQ8F0ANoA2JA==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz", + "integrity": "sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==", "cpu": [ "ppc64" ], @@ -364,9 +364,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.0.tgz", - "integrity": "sha512-pQdyAIZ0BWIC5GyvVFn5awDiO14TkT/19FTmFcPdDec94KJ1uZcmFs21Fo8auMXzD4Tt+diXu1LW1gHus9fhFQ==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz", + "integrity": "sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==", "cpu": [ "riscv64" ], @@ -381,9 +381,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.0.tgz", - "integrity": "sha512-hPlRWR4eIDDEci953RI1BLZitgi5uqcsjKMxwYfmi4LcwyWo2IcRP+lThVnKjNtk90pLS8nKdroXYOqW+QQH+w==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz", + "integrity": "sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==", "cpu": [ "s390x" ], @@ -398,9 +398,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.0.tgz", - "integrity": "sha512-1hBWx4OUJE2cab++aVZ7pObD6s+DK4mPGpemtnAORBvb5l/g5xFGk0vc0PjSkrDs0XaXj9yyob3d14XqvnQ4gw==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz", + "integrity": "sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==", "cpu": [ "x64" ], @@ -415,9 +415,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.0.tgz", - "integrity": "sha512-6m0sfQfxfQfy1qRuecMkJlf1cIzTOgyaeXaiVaaki8/v+WB+U4hc6ik15ZW6TAllRlg/WuQXxWj1jx6C+dfy3w==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz", + "integrity": "sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==", "cpu": [ "arm64" ], @@ -432,9 +432,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.0.tgz", - "integrity": "sha512-xbbOdfn06FtcJ9d0ShxxvSn2iUsGd/lgPIO2V3VZIPDbEaIj1/3nBBe1AwuEZKXVXkMmpr6LUAgMkLD/4D2PPA==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz", + "integrity": "sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==", "cpu": [ "x64" ], @@ -449,9 +449,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.0.tgz", - "integrity": "sha512-fWgqR8uNbCQ/GGv0yhzttj6sU/9Z5/Sv/VGU3F5OuXK6J6SlriONKrQ7tNlwBrJZXRYk5jUhuWvF7GYzGguBZQ==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz", + "integrity": "sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==", "cpu": [ "arm64" ], @@ -466,9 +466,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.0.tgz", - "integrity": "sha512-aCwlRdSNMNxkGGqQajMUza6uXzR/U0dIl1QmLjPtRbLOx3Gy3otfFu/VjATy4yQzo9yFDGTxYDo1FfAD9oRD2A==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz", + "integrity": "sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==", "cpu": [ "x64" ], @@ -483,9 +483,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.0.tgz", - "integrity": "sha512-nyvsBccxNAsNYz2jVFYwEGuRRomqZ149A39SHWk4hV0jWxKM0hjBPm3AmdxcbHiFLbBSwG6SbpIcUbXjgyECfA==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz", + "integrity": "sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==", "cpu": [ "arm64" ], @@ -500,9 +500,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.0.tgz", - "integrity": "sha512-Q1KY1iJafM+UX6CFEL+F4HRTgygmEW568YMqDA5UV97AuZSm21b7SXIrRJDwXWPzr8MGr75fUZPV67FdtMHlHA==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz", + "integrity": "sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==", "cpu": [ "x64" ], @@ -517,9 +517,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.0.tgz", - "integrity": "sha512-W1eyGNi6d+8kOmZIwi/EDjrL9nxQIQ0MiGqe/AWc6+IaHloxHSGoeRgDRKHFISThLmsewZ5nHFvGFWdBYlgKPg==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz", + "integrity": "sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==", "cpu": [ "arm64" ], @@ -534,9 +534,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.0.tgz", - "integrity": "sha512-30z1aKL9h22kQhilnYkORFYt+3wp7yZsHWus+wSKAJR8JtdfI76LJ4SBdMsCopTR3z/ORqVu5L1vtnHZWVj4cQ==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz", + "integrity": "sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==", "cpu": [ "ia32" ], @@ -551,9 +551,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.0.tgz", - "integrity": "sha512-aIitBcjQeyOhMTImhLZmtxfdOcuNRpwlPNmlFKPcHQYPhEssw75Cl1TSXJXpMkzaua9FUetx/4OQKq7eJul5Cg==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz", + "integrity": "sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==", "cpu": [ "x64" ], @@ -1339,9 +1339,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.10.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.1.tgz", - "integrity": "sha512-GNWcUTRBgIRJD5zj+Tq0fKOJ5XZajIiBroOF0yvj2bSU1WvNdYS/dn9UxwsujGW4JX06dnHyjV2y9rRaybH0iQ==", + "version": "25.0.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.0.0.tgz", + "integrity": "sha512-rl78HwuZlaDIUSeUKkmogkhebA+8K1Hy7tddZuJ3D0xV8pZSfsYGTsliGUol1JPzu9EKnTxPC4L1fiWouStRew==", "dev": true, "license": "MIT", "peer": true, @@ -1357,18 +1357,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.48.1.tgz", - "integrity": "sha512-X63hI1bxl5ohelzr0LY5coufyl0LJNthld+abwxpCoo6Gq+hSqhKwci7MUWkXo67mzgUK6YFByhmaHmUcuBJmA==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.49.0.tgz", + "integrity": "sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.48.1", - "@typescript-eslint/type-utils": "8.48.1", - "@typescript-eslint/utils": "8.48.1", - "@typescript-eslint/visitor-keys": "8.48.1", - "graphemer": "^1.4.0", + "@typescript-eslint/scope-manager": "8.49.0", + "@typescript-eslint/type-utils": "8.49.0", + "@typescript-eslint/utils": "8.49.0", + "@typescript-eslint/visitor-keys": "8.49.0", "ignore": "^7.0.0", "natural-compare": "^1.4.0", "ts-api-utils": "^2.1.0" @@ -1381,23 +1380,23 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.48.1", + "@typescript-eslint/parser": "^8.49.0", "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.48.1.tgz", - "integrity": "sha512-PC0PDZfJg8sP7cmKe6L3QIL8GZwU5aRvUFedqSIpw3B+QjRSUZeeITC2M5XKeMXEzL6wccN196iy3JLwKNvDVA==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.49.0.tgz", + "integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==", "dev": true, "license": "MIT", "peer": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.48.1", - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/typescript-estree": "8.48.1", - "@typescript-eslint/visitor-keys": "8.48.1", + "@typescript-eslint/scope-manager": "8.49.0", + "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/typescript-estree": "8.49.0", + "@typescript-eslint/visitor-keys": "8.49.0", "debug": "^4.3.4" }, "engines": { @@ -1413,14 +1412,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.48.1.tgz", - "integrity": "sha512-HQWSicah4s9z2/HifRPQ6b6R7G+SBx64JlFQpgSSHWPKdvCZX57XCbszg/bapbRsOEv42q5tayTYcEFpACcX1w==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.49.0.tgz", + "integrity": "sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.48.1", - "@typescript-eslint/types": "^8.48.1", + "@typescript-eslint/tsconfig-utils": "^8.49.0", + "@typescript-eslint/types": "^8.49.0", "debug": "^4.3.4" }, "engines": { @@ -1435,14 +1434,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.48.1.tgz", - "integrity": "sha512-rj4vWQsytQbLxC5Bf4XwZ0/CKd362DkWMUkviT7DCS057SK64D5lH74sSGzhI6PDD2HCEq02xAP9cX68dYyg1w==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.49.0.tgz", + "integrity": "sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/visitor-keys": "8.48.1" + "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/visitor-keys": "8.49.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1453,9 +1452,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.48.1.tgz", - "integrity": "sha512-k0Jhs4CpEffIBm6wPaCXBAD7jxBtrHjrSgtfCjUvPp9AZ78lXKdTR8fxyZO5y4vWNlOvYXRtngSZNSn+H53Jkw==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.49.0.tgz", + "integrity": "sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA==", "dev": true, "license": "MIT", "engines": { @@ -1470,15 +1469,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.48.1.tgz", - "integrity": "sha512-1jEop81a3LrJQLTf/1VfPQdhIY4PlGDBc/i67EVWObrtvcziysbLN3oReexHOM6N3jyXgCrkBsZpqwH0hiDOQg==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.49.0.tgz", + "integrity": "sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/typescript-estree": "8.48.1", - "@typescript-eslint/utils": "8.48.1", + "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/typescript-estree": "8.49.0", + "@typescript-eslint/utils": "8.49.0", "debug": "^4.3.4", "ts-api-utils": "^2.1.0" }, @@ -1495,9 +1494,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.48.1.tgz", - "integrity": "sha512-+fZ3LZNeiELGmimrujsDCT4CRIbq5oXdHe7chLiW8qzqyPMnn1puNstCrMNVAqwcl2FdIxkuJ4tOs/RFDBVc/Q==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.49.0.tgz", + "integrity": "sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ==", "dev": true, "license": "MIT", "engines": { @@ -1509,16 +1508,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.48.1.tgz", - "integrity": "sha512-/9wQ4PqaefTK6POVTjJaYS0bynCgzh6ClJHGSBj06XEHjkfylzB+A3qvyaXnErEZSaxhIo4YdyBgq6j4RysxDg==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.49.0.tgz", + "integrity": "sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.48.1", - "@typescript-eslint/tsconfig-utils": "8.48.1", - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/visitor-keys": "8.48.1", + "@typescript-eslint/project-service": "8.49.0", + "@typescript-eslint/tsconfig-utils": "8.49.0", + "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/visitor-keys": "8.49.0", "debug": "^4.3.4", "minimatch": "^9.0.4", "semver": "^7.6.0", @@ -1537,16 +1536,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.48.1.tgz", - "integrity": "sha512-fAnhLrDjiVfey5wwFRwrweyRlCmdz5ZxXz2G/4cLn0YDLjTapmN4gcCsTBR1N2rWnZSDeWpYtgLDsJt+FpmcwA==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.49.0.tgz", + "integrity": "sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.48.1", - "@typescript-eslint/types": "8.48.1", - "@typescript-eslint/typescript-estree": "8.48.1" + "@typescript-eslint/scope-manager": "8.49.0", + "@typescript-eslint/types": "8.49.0", + "@typescript-eslint/typescript-estree": "8.49.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1561,13 +1560,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.48.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.48.1.tgz", - "integrity": "sha512-BmxxndzEWhE4TIEEMBs8lP3MBWN3jFPs/p6gPm/wkv02o41hI6cq9AuSmGAaTTHPtA1FTi2jBre4A9rm5ZmX+Q==", + "version": "8.49.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.49.0.tgz", + "integrity": "sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.48.1", + "@typescript-eslint/types": "8.49.0", "eslint-visitor-keys": "^4.2.1" }, "engines": { @@ -1875,9 +1874,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.0.tgz", - "integrity": "sha512-Mh++g+2LPfzZToywfE1BUzvZbfOY52Nil0rn9H1CPC5DJ7fX+Vir7nToBeoiSbB1zTNeGYbELEvJESujgGrzXw==", + "version": "2.9.6", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.6.tgz", + "integrity": "sha512-v9BVVpOTLB59C9E7aSnmIF8h7qRsFpx+A2nugVMTszEOMcfjlZMsXRm4LF23I3Z9AJxc8ANpIvzbzONoX9VJlg==", "dev": true, "license": "Apache-2.0", "bin": { @@ -1979,9 +1978,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001759", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001759.tgz", - "integrity": "sha512-Pzfx9fOKoKvevQf8oCXoyNRQ5QyxJj+3O0Rqx2V5oxT61KGx8+n6hV/IUyJeifUci2clnmmKVpvtiqRzgiWjSw==", + "version": "1.0.30001760", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz", + "integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==", "dev": true, "funding": [ { @@ -2209,9 +2208,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.263", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.263.tgz", - "integrity": "sha512-DrqJ11Knd+lo+dv+lltvfMDLU27g14LMdH2b0O3Pio4uk0x+z7OR+JrmyacTPN2M8w3BrZ7/RTwG3R9B7irPlg==", + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", "dev": true, "license": "ISC" }, @@ -2223,9 +2222,9 @@ "license": "MIT" }, "node_modules/esbuild": { - "version": "0.27.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.0.tgz", - "integrity": "sha512-jd0f4NHbD6cALCyGElNpGAOtWxSq46l9X/sWB0Nzd5er4Kz2YTm+Vl0qKFT9KUJvD8+fiO8AvoHhFvEatfVixA==", + "version": "0.27.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.1.tgz", + "integrity": "sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2236,32 +2235,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.0", - "@esbuild/android-arm": "0.27.0", - "@esbuild/android-arm64": "0.27.0", - "@esbuild/android-x64": "0.27.0", - "@esbuild/darwin-arm64": "0.27.0", - "@esbuild/darwin-x64": "0.27.0", - "@esbuild/freebsd-arm64": "0.27.0", - "@esbuild/freebsd-x64": "0.27.0", - "@esbuild/linux-arm": "0.27.0", - "@esbuild/linux-arm64": "0.27.0", - "@esbuild/linux-ia32": "0.27.0", - "@esbuild/linux-loong64": "0.27.0", - "@esbuild/linux-mips64el": "0.27.0", - "@esbuild/linux-ppc64": "0.27.0", - "@esbuild/linux-riscv64": "0.27.0", - "@esbuild/linux-s390x": "0.27.0", - "@esbuild/linux-x64": "0.27.0", - "@esbuild/netbsd-arm64": "0.27.0", - "@esbuild/netbsd-x64": "0.27.0", - "@esbuild/openbsd-arm64": "0.27.0", - "@esbuild/openbsd-x64": "0.27.0", - "@esbuild/openharmony-arm64": "0.27.0", - "@esbuild/sunos-x64": "0.27.0", - "@esbuild/win32-arm64": "0.27.0", - "@esbuild/win32-ia32": "0.27.0", - "@esbuild/win32-x64": "0.27.0" + "@esbuild/aix-ppc64": "0.27.1", + "@esbuild/android-arm": "0.27.1", + "@esbuild/android-arm64": "0.27.1", + "@esbuild/android-x64": "0.27.1", + "@esbuild/darwin-arm64": "0.27.1", + "@esbuild/darwin-x64": "0.27.1", + "@esbuild/freebsd-arm64": "0.27.1", + "@esbuild/freebsd-x64": "0.27.1", + "@esbuild/linux-arm": "0.27.1", + "@esbuild/linux-arm64": "0.27.1", + "@esbuild/linux-ia32": "0.27.1", + "@esbuild/linux-loong64": "0.27.1", + "@esbuild/linux-mips64el": "0.27.1", + "@esbuild/linux-ppc64": "0.27.1", + "@esbuild/linux-riscv64": "0.27.1", + "@esbuild/linux-s390x": "0.27.1", + "@esbuild/linux-x64": "0.27.1", + "@esbuild/netbsd-arm64": "0.27.1", + "@esbuild/netbsd-x64": "0.27.1", + "@esbuild/openbsd-arm64": "0.27.1", + "@esbuild/openbsd-x64": "0.27.1", + "@esbuild/openharmony-arm64": "0.27.1", + "@esbuild/sunos-x64": "0.27.1", + "@esbuild/win32-arm64": "0.27.1", + "@esbuild/win32-ia32": "0.27.1", + "@esbuild/win32-x64": "0.27.1" } }, "node_modules/escalade": { @@ -2588,9 +2587,9 @@ } }, "node_modules/expect-type": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", - "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -2783,13 +2782,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/graphemer": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", - "dev": true, - "license": "MIT" - }, "node_modules/handlebars": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", @@ -4049,9 +4041,9 @@ "license": "MIT" }, "node_modules/update-browserslist-db": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.0.tgz", - "integrity": "sha512-Dn+NlSF/7+0lVSEZ57SYQg6/E44arLzsVOGgrElBn/BlG1B8WKdbLppOocFrXwRNTkNlgdGNaBgH1o0lggDPiw==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz", + "integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==", "dev": true, "funding": [ { @@ -4097,9 +4089,9 @@ "license": "MIT" }, "node_modules/vite": { - "version": "7.2.6", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.6.tgz", - "integrity": "sha512-tI2l/nFHC5rLh7+5+o7QjKjSR04ivXDF4jcgV0f/bTQ+OJiITy5S6gaynVsEM+7RqzufMnVbIon6Sr5x1SDYaQ==", + "version": "7.2.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.2.7.tgz", + "integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==", "dev": true, "license": "MIT", "peer": true, diff --git a/package.json b/package.json index 1c1907b..29100bb 100644 --- a/package.json +++ b/package.json @@ -61,10 +61,10 @@ ], "devDependencies": { "@types/mime-types": "^3.0.1", - "@types/node": "^24.10.1", + "@types/node": "^25.0.0", "@types/picomatch": "^4.0.2", - "@typescript-eslint/eslint-plugin": "^8.48.1", - "@typescript-eslint/parser": "^8.48.1", + "@typescript-eslint/eslint-plugin": "^8.49.0", + "@typescript-eslint/parser": "^8.49.0", "@vitest/coverage-v8": "^4.0.15", "eslint": "^9.39.1", "eslint-config-prettier": "^10.1.8", From cf828f970a6cd03a9446adca1769c2c233cc7203 Mon Sep 17 00:00:00 2001 From: BCsabaEngine Date: Thu, 11 Dec 2025 20:42:24 +0100 Subject: [PATCH 3/4] chore: compact claude --- CLAUDE.md | 451 ++++++++++++++---------------------------------------- 1 file changed, 117 insertions(+), 334 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index dd6b2c3..14ad354 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,398 +4,181 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project Overview -SvelteESP32 is a TypeScript CLI tool that converts frontend JS applications (Svelte, React, Angular, Vue) into C++ header files that can be embedded in ESP32/ESP8266 microcontroller web servers. The tool processes web assets and generates optimized C++ code with optional gzip compression and ETag support for different web server engines. +SvelteESP32 is a TypeScript CLI tool that converts frontend JS applications (Svelte, React, Angular, Vue) into C++ header files embedded in ESP32/ESP8266 microcontroller web servers. Generates optimized C++ code with gzip compression and ETag support for 4 web server engines. -## Key Commands - -### Development Commands - -```bash -# Build the project -npm run build - -# Clean build artifacts -npm run clean - -# Development with live reload (async engine) -npm run dev:async - -# Development with live reload (psychic engine) -npm run dev:psychic - -# Development with live reload (psychic2 engine) -npm run dev:psychic2 - -# Run TypeScript unit tests -npm run test - -# Run tests in watch mode -npm run test:watch - -# Generate test coverage report -npm run test:coverage - -# Run comprehensive ESP32 tests (requires PlatformIO) -npm run test:all -``` - -### Code Quality Commands - -```bash -# Check formatting -npm run format:check - -# Fix formatting -npm run format:fix - -# Check linting -npm run lint:check - -# Fix linting issues -npm run lint:fix - -# Fix all formatting and linting issues -npm run fix -``` - -### CLI Usage +## Quick Commands ```bash -# Generate header for PsychicHttpServer -npx svelteesp32 -e psychic -s ./dist -o ./output.h --etag=true --gzip=true - -# Generate header for PsychicHttpServer V2 -npx svelteesp32 -e psychic2 -s ./dist -o ./output.h --etag=true --gzip=true - -# Generate header for ESPAsyncWebServer -npx svelteesp32 -e async -s ./dist -o ./output.h --etag=true --gzip=true - -# Generate header for ESP-IDF -npx svelteesp32 -e espidf -s ./dist -o ./output.h --etag=true --gzip=true - -# Test the CLI directly using tsx (for development) +# Build & Quality +npm run build # Build TypeScript +npm run clean # Clean build artifacts +npm run fix # Fix all formatting and linting +npm run test # Run unit tests +npm run test:coverage # Generate coverage report + +# Development +npm run dev:psychic # Live reload (psychic engine) npx tsx src/index.ts -e psychic -s ./demo/svelte/dist -o ./output.h --etag=true --gzip=true -# Generate header with file exclusions -npx svelteesp32 -e psychic -s ./dist -o ./output.h --exclude="*.map" --exclude="*.md" +# CLI Usage +npx svelteesp32 -e psychic -s ./dist -o ./output.h --etag=true --gzip=true --exclude="*.map" +npx svelteesp32 --no-index-check # Skip index.html validation (API-only apps) ``` ## Architecture -### Core Components +### Core Files -- **`src/index.ts`**: Main entry point that orchestrates the file processing pipeline -- **`src/commandLine.ts`**: CLI argument parsing and validation using native Node.js `process.argv` implementation -- **`src/file.ts`**: File system operations for reading web assets -- **`src/cppCode.ts`**: C++ code generation engine with Handlebars templates for PsychicHttp and ESPAsyncWebServer -- **`src/cppCodeEspIdf.ts`**: Specialized C++ code generation for native ESP-IDF -- **`src/consoleColor.ts`**: Colored console output utilities +- **`src/index.ts`** - Main entry point, orchestrates file processing pipeline +- **`src/commandLine.ts`** - CLI parsing using native `process.argv`, supports RC files with npm variable interpolation +- **`src/file.ts`** - File operations: glob scanning, duplicate detection (SHA256), index.html validation +- **`src/cppCode.ts`** - C++ code generation (Handlebars templates) for psychic/psychic2/async engines +- **`src/cppCodeEspIdf.ts`** - C++ code generation for ESP-IDF native engine +- **`src/errorMessages.ts`** - Framework-specific error messages with actionable hints ### Processing Pipeline -1. **File Collection** (`src/file.ts`): Scans source directory recursively using glob, skips pre-compressed files (.gz, .br) if originals exist, filters files matching exclude patterns, detects duplicate files via SHA256 hashing -2. **Content Analysis** (`src/index.ts`): Determines MIME types using `mime-types` library, calculates MD5 hashes for ETag generation, groups files by extension for statistics -3. **Compression** (`src/index.ts`): Applies gzip level 9 compression, uses compressed version only when size reduction >15% and original >1024 bytes -4. **Code Generation** (`src/cppCode.ts`, `src/cppCodeEspIdf.ts`): Uses Handlebars templates with custom helpers (switch/case), generates optimized engine-specific C++ code with: - - Inlined lambda handlers (ESPAsyncWebServer) - - Optimized header lookups for ETag validation (all engines) - - Proper const-correctness and modern C++ patterns - - Conditional compilation support for etag/gzip options -5. **Output**: Writes optimized header with embedded binary data arrays, route handlers with ETag validation, ETag MD5 strings, and C++ defines for build-time validation +1. **File Collection** (`file.ts`): Glob scan → skip pre-compressed (.gz/.br) if original exists → filter exclude patterns → validate index.html (unless `--no-index-check`) +2. **Content Analysis** (`index.ts`): MIME types → MD5 ETags → extension grouping +3. **Compression** (`index.ts`): Gzip level 9 if >1024 bytes AND >15% reduction +4. **Code Generation** (`cppCode.ts`): Handlebars templates → engine-specific C++ → byte arrays + route handlers + ETags +5. **Output**: Write header with binary data, route handlers, ETag validation, C++ defines ### Supported Engines -- **psychic**: PsychicHttpServer (ESP32 only, fastest performance) -- **psychic2**: PsychicHttpServer V2 -- **async**: ESPAsyncWebServer (ESP32/ESP8266 compatible) -- **espidf**: Native ESP-IDF web server - -### Key Features - -- **Automatic Gzip Compression**: Compresses assets when size reduction >15% and >1024 bytes -- **ETag Support**: HTTP cache validation for reduced network traffic with 304 Not Modified responses across all engines (psychic, psychic2, async, espidf) -- **Cache Control**: Configurable browser caching with `--cachetime` -- **File Exclusion**: Exclude unwanted files using glob patterns with `--exclude` option -- **Multi-Engine Support**: Generate code for different ESP web server libraries -- **File Type Analysis**: Groups files by extension with count statistics -- **Memory Optimization**: Binary data stored as const arrays in program memory -- **Optimized C++ Code**: Generated code uses modern C++ best practices with minimal overhead - -### File Exclusion +- **psychic** - PsychicHttpServer V1 (ESP32 only, fastest) +- **psychic2** - PsychicHttpServer V2 (ESP32 only, modern API) +- **async** - ESPAsyncWebServer (ESP32/ESP8266, uses PROGMEM) +- **espidf** - Native ESP-IDF web server -The tool supports flexible file exclusion using glob patterns to filter out unwanted files from being embedded in the ESP32 firmware. +## Key Features -**Pattern Support:** +- **Single Binary OTA**: All files embedded in firmware (vs SPIFFS/LittleFS requiring separate partition) +- **Automatic Gzip**: Build-time compression (>1024 bytes, >15% reduction) +- **ETag Support**: HTTP 304 Not Modified responses on all engines (MD5 hashes, If-None-Match validation) +- **CI/CD Integration**: npm package, RC files, variable interpolation from package.json +- **File Exclusion**: Glob patterns (`--exclude="*.map,*.md"`) +- **Index.html Validation**: Ensures default entry point exists (skip with `--no-index-check` for APIs) +- **Multi-Engine**: Generates optimized C++ for 4 different web server frameworks +- **C++ Defines**: Build-time validation (`SVELTEESP32_COUNT`, `SVELTEESP32_FILE_INDEX_HTML`, etc.) -- Simple wildcards: `*.map`, `*.txt` -- Directory patterns: `test/**/*.js`, `docs/**/*` -- Specific files: `.DS_Store`, `README.md` +## Configuration -**Multiple Patterns:** +### RC File Support (.svelteesp32rc.json) -- Repeated flag: `--exclude *.map --exclude *.md` -- Comma-separated: `--exclude="*.map,*.md,*.txt"` -- Combined: Both formats can be mixed +All CLI options can be stored in RC files. Searched in: current directory, home directory, or `--config=path`. -**Default Exclusions:** -System and development files are excluded by default: - -- `.DS_Store`, `Thumbs.db` (system files) -- `.git`, `.svn` (version control) -- `*.swp`, `*~` (editor files) -- `.gitignore`, `.gitattributes` (git config) - -**Implementation:** - -- Uses `picomatch` library (transitive dependency via `tinyglobby`) -- Filtering occurs after pre-compressed file detection but before file content reading -- Excluded files are logged with count and list for verification -- Windows path normalization for cross-platform consistency - -**Example Output:** - -``` -Collecting source files - -Excluded 3 file(s): - - assets/index.js.map - - README.md - - test/unit.test.js - -Translation to header file -[index.html] ✓ gzip used (472 -> 308 = 65%) -... -``` +### NPM Variable Interpolation -## Development Environment +RC files support `$npm_package_*` variables from package.json: -### Build System +- `"version": "v$npm_package_version"` → `"v2.1.0"` +- `"define": "$npm_package_name"` → `"my-esp32-app"` +- Nested: `$npm_package_repository_type` → `packageJson.repository.type` +- Implementation: `src/commandLine.ts` lines 125-220 -- **TypeScript**: Compiled to CommonJS in `dist/` directory -- **Target**: ES2020 with strict type checking -- **Incremental**: Disabled to ensure clean builds +### CLI Options -### Code Quality +Key flags: `-s` (source), `-e` (engine), `-o` (output), `--etag` (true/false/compiler), `--gzip` (true/false/compiler), `--exclude` (glob patterns), `--no-index-check`, `--cachetime`, `--version`, `--define`, `--espmethod` -- **ESLint**: Comprehensive rules including TypeScript, Prettier, Unicorn plugins -- **Prettier**: 120 character line width, single quotes, no trailing commas -- **Import Sorting**: Automatic import organization with `simple-import-sort` +## Generated C++ Code -### Testing +### ETag Implementation (All Engines) -The project uses **Vitest** for unit testing with comprehensive coverage: - -**Test Structure:** - -``` -test/ -├── unit/ -│ ├── commandLine.test.ts # CLI argument parsing tests -│ ├── file.test.ts # File operations tests -│ ├── cppCode.test.ts # C++ code generation tests -│ └── consoleColor.test.ts # Console utilities tests -└── fixtures/ - └── sample-files/ # Test fixture files -``` - -**Test Coverage (68.25% overall):** - -- `commandLine.ts`: 84.56% - CLI argument parsing, validation, engine/tri-state validation -- `file.ts`: 100% - File collection, duplicate detection (SHA256), pre-compressed file skipping -- `cppCode.ts`: 96.62% - Template rendering for all 4 engines, etag/gzip combinations, Handlebars helpers -- `cppCodeEspIdf.ts`: 100% - ESP-IDF template (tested via `cppCode.ts`) -- `consoleColor.ts`: 100% - ANSI color code wrapping -- `index.ts`: 0% - Main entry point (has side effects, tested via integration) - -**Key Testing Features:** - -- **Vitest Configuration**: `vitest.config.ts` with TypeScript support, 60% coverage thresholds -- **Mocking Strategy**: Uses `vi.mock()` for file system (`node:fs`), glob (`tinyglobby`), and module dependencies -- **Dynamic Imports**: Tests use dynamic imports for `commandLine.ts` to test different CLI arguments without side effects -- **Test Fixtures**: Small sample files (HTML/CSS/JS) for testing file processing pipeline -- **Coverage Reports**: Generated in `coverage/` directory (ignored by git), viewable HTML reports at `coverage/index.html` - -**Testing Approach:** - -- **commandLine.test.ts**: Tests argument parsing (`--flag=value`, `-f value`, `--flag value`), validation errors, required arguments, directory validation. Uses dynamic imports to avoid module side effects. -- **file.test.ts**: Mocks file system with `memfs`, tests duplicate detection, compressed file skipping, path handling -- **cppCode.test.ts**: Tests template selection by engine, code generation for all etag/gzip combinations, byte array conversion, ETag/cache headers, default route detection -- **consoleColor.test.ts**: Simple tests for ANSI escape code wrapping (quick coverage wins) - -**Running Tests:** - -- `npm run test` - Run all tests once (CI/CD) -- `npm run test:watch` - Watch mode for development -- `npm run test:coverage` - Generate coverage reports - -### Demo Projects - -- **`demo/svelte/`**: Example Svelte application with Vite, TailwindCSS, gallery images -- **`demo/esp32/`**: PlatformIO Arduino framework project with WiFi credentials example -- **`demo/esp32idf/`**: ESP-IDF native project using native web server - -The `package.script` executable generates 36 test header files (9 combinations of etag/gzip × 4 engines) for comprehensive validation. - -## Important Notes - -- The tool processes entire directories recursively and embeds all files as binary data -- Generated header files can be large but compile efficiently -- Memory usage is optimized through const array placement in program memory (ESP32) or PROGMEM (ESP8266) -- The CLI is designed for CI/CD integration with npm packaging workflows -- `index.html` or `index.htm` files are automatically set as the default route for "/" -- Pre-compressed files (.gz, .br, .brottli) in the source directory are skipped if the original file exists -- The build uses `--clean` and `--force` flags to ensure clean builds without incremental compilation -- ESP-IDF engine includes required headers (`string.h`, `stdlib.h`) for ETag validation support -- All engines now fully support HTTP 304 Not Modified responses for efficient caching - -## Template System - -The code generation uses Handlebars with custom helpers: - -- **switch/case helpers**: Enable conditional C++ code generation based on etag/gzip settings -- **Three-state options**: `etag` and `gzip` can be "true", "false", or "compiler" (for C++ directive control) -- **Engine-specific templates**: Each engine (psychic, psychic2, async, espidf) has its own template in `src/cppCode.ts` or `src/cppCodeEspIdf.ts` -- **Data transformation**: Binary content is converted to comma-separated byte arrays in the template data - -## Generated C++ Code Quality - -The generated C++ code follows modern best practices and is optimized for performance and maintainability: - -### ETag Validation Implementation - -All four engines support ETag validation with HTTP 304 Not Modified responses: - -- **ESPAsyncWebServer (`async`)**: Uses `const AsyncWebHeader*` for proper const-correctness, single `getHeader()` call instead of `hasHeader()` + `getHeader()`, inlined lambda handlers -- **PsychicHttpServer (`psychic`, `psychic2`)**: Uses `request->header().equals()` for direct string comparison without temporary objects -- **ESP-IDF (`espidf`)**: Uses `httpd_req_get_hdr_value_len()` and `httpd_req_get_hdr_value_str()` for header validation with proper memory management (malloc/free) - -### Code Optimizations - -- **No intermediate variables**: Lambda handlers are inlined directly in route registration (ESPAsyncWebServer) -- **Optimized header lookups**: Single API call instead of redundant checks -- **No temporary String objects**: Uses `.equals()` method directly instead of `== String()` wrapper -- **Proper const-correctness**: All pointer declarations use `const` where appropriate -- **Minimal overhead**: Generated code has minimal runtime performance impact +- **async**: `const AsyncWebHeader*`, single `getHeader()` call, inlined lambdas +- **psychic/psychic2**: `request->header().equals()`, no temporary String objects +- **espidf**: `httpd_req_get_hdr_value_len()` + `httpd_req_get_hdr_value_str()`, malloc/free ### Memory Management -- **ESP32 (psychic, psychic2, espidf)**: Const arrays automatically placed in program memory (flash) -- **ESP8266 (async)**: PROGMEM directive explicitly used for flash storage -- **ESP-IDF ETag validation**: Temporary header value buffers are properly allocated and freed +- **ESP32** (psychic/psychic2/espidf): Const arrays → automatic program memory (flash) +- **ESP8266** (async): PROGMEM directive → explicit flash storage +- All: Binary data in flash, not RAM -## Generated C++ Defines +### Configuration Comment -The generated header file includes C++ defines for build-time validation: +Generated headers include `//config:` comment showing all settings used (engine, sourcepath, etag, gzip, exclude, etc.) for traceability. -- `{PREFIX}_COUNT`: Total number of files -- `{PREFIX}_SIZE`: Total uncompressed size in bytes -- `{PREFIX}_SIZE_GZIP`: Total gzip compressed size in bytes -- `{PREFIX}_FILE_{FILENAME}`: Define for each file (e.g., `SVELTEESP32_FILE_INDEX_HTML`) -- `{PREFIX}_{EXT}_FILES`: Count of files by extension (e.g., `SVELTEESP32_CSS_FILES`) +## Testing (Vitest) -These allow C++ code to verify expected files are present using `#ifndef` and `#error` directives. +**Coverage**: ~68% overall -## Generated Header Configuration Comment +- `commandLine.ts`: 84.56% - CLI parsing, validation, npm variable interpolation +- `file.ts`: 100% - File ops, duplicate detection, index.html validation +- `cppCode.ts`: 96.62% - Template rendering, all 4 engines, etag/gzip combos +- `consoleColor.ts`: 100% - ANSI colors +- `index.ts`: 0% - Main entry (side effects, tested via integration) -The generated header file includes a `//config:` comment at the top that displays the effective configuration used during code generation: +**Test Files**: -```cpp -//engine: PsychicHttpServer -//config: engine=psychic sourcepath=./dist outputfile=./output.h etag=true gzip=true cachetime=0 espmethod=initSvelteStaticFiles define=SVELTEESP32 exclude=[*.map, *.md] -``` - -**Implementation** (`src/commandLine.ts`, `src/cppCode.ts`): - -- `formatConfiguration()` function creates a formatted string from the `ICopyFilesArguments` object -- Shows all configuration parameters: `engine`, `sourcepath`, `outputfile`, `etag`, `gzip`, `cachetime`, `espmethod`, `define`, and `exclude` patterns -- Works consistently whether configuration comes from RC file, CLI arguments, or both -- Provides complete traceability of the configuration used for code generation - -## NPM Package Variable Interpolation - -RC files support automatic variable interpolation from `package.json`, allowing dynamic configuration based on project metadata. +- `test/unit/commandLine.test.ts` - Dynamic imports, argument parsing, npm variable interpolation (20+ tests) +- `test/unit/file.test.ts` - memfs mocking, duplicate detection, index.html validation +- `test/unit/cppCode.test.ts` - Template selection, code generation, byte arrays +- `test/unit/errorMessages.test.ts` - Error message validation -**Feature:** Variables like `$npm_package_version` and `$npm_package_name` in RC files are automatically replaced with values from `package.json` located in the same directory as the RC file. +**Key Patterns**: -**Supported Fields:** All string fields in RC configuration: +- Mock fs with `vi.mock('node:fs')` and memfs +- Dynamic imports for commandLine tests (avoid side effects) +- Test fixtures in `test/fixtures/sample-files/` -- `version` - e.g., `"v$npm_package_version"` -- `define` - e.g., `"$npm_package_name_STATIC"` -- `sourcepath` - e.g., `"./$npm_package_name/dist"` -- `outputfile` - e.g., `"./output_$npm_package_version.h"` -- `espmethod` - e.g., `"init$npm_package_name"` -- `exclude` patterns - e.g., `["$npm_package_name.map"]` +## Important Implementation Details -**Variable Syntax:** +### File Exclusion -- Simple fields: `$npm_package_version` → `packageJson.version` -- Nested fields: `$npm_package_repository_type` → `packageJson.repository.type` -- Multiple variables: `"$npm_package_name-v$npm_package_version"` → `"myapp-v1.2.3"` +- Default exclusions: `.DS_Store`, `Thumbs.db`, `.git`, `.svn`, `*.swp`, `*~`, `.gitignore`, `.gitattributes` +- Uses `picomatch` (via tinyglobby transitive dependency) +- Filtering after pre-compressed detection, before file reading +- Windows path normalization for cross-platform compatibility -**Implementation** (`src/commandLine.ts` lines 125-220): +### Index.html Validation (`src/file.ts` lines 117-125) -**Core Functions:** +- Checks for `index.html` or `index.htm` in root or subdirectories +- Shows engine-specific hints on error: `server->defaultEndpoint` (psychic), `server.on("/")` (async), etc. +- Skip with `--no-index-check` flag for API-only applications -1. **`findPackageJson(rcFilePath: string)`** - Locates package.json in the same directory as RC file -2. **`parsePackageJson(packageJsonPath: string)`** - Reads and parses package.json with error handling -3. **`getNpmPackageVariable(packageJson, variableName)`** - Extracts values from package.json using underscore-separated path segments (e.g., `$npm_package_repository_type` traverses `packageJson.repository.type`) -4. **`checkStringForNpmVariable(value)`** - Helper to check if a string contains npm package variables -5. **`hasNpmVariables(config)`** - Quick check if RC config contains any npm variables (optimization to skip interpolation when not needed) -6. **`interpolateNpmVariables(config, rcFilePath)`** - Main interpolation function that processes all string fields +### Template System (`src/cppCode.ts`) -**Processing Flow:** +- Handlebars with custom `switch`/`case` helpers +- Tri-state options: `etag`/`gzip` can be "true", "false", or "compiler" (C++ directives) +- Engine-specific templates inline in source file +- Binary data converted to comma-separated byte arrays -1. Load RC file JSON -2. **Check for npm variables** - If present, find and parse package.json -3. **Interpolate variables** - Replace `$npm_package_*` patterns using regex: `/\$npm_package_[\dA-Za-z]+(?:_[a-z][\dA-Za-z]*)*/g` -4. Validate interpolated configuration -5. Merge with CLI arguments +### Compression Thresholds (`src/index.ts`) -**Error Handling:** +```typescript +const GZIP_MIN_SIZE = 1024; +const GZIP_MIN_REDUCTION_RATIO = 0.85; // Use gzip if <85% of original +``` -- If variables are used but package.json not found: Throws error listing affected fields -- If variable doesn't exist in package.json: Left unchanged (not an error) -- If package.json is invalid JSON: Clear error message with file path +### Demo Projects -**Regex Pattern Design:** +- `demo/svelte/` - Example Svelte app (Vite + TailwindCSS) +- `demo/esp32/` - PlatformIO Arduino project +- `demo/esp32idf/` - ESP-IDF native project +- `package.script` generates 36 test headers (9 etag/gzip combos × 4 engines) -- Matches: `$npm_package_version`, `$npm_package_repository_type` -- Stops at: `_STATIC`, `_CONSTANT` (underscore followed by uppercase) -- Example: `"$npm_package_name_STATIC"` → interpolates `name`, keeps `_STATIC` suffix +## Recent Updates -**Testing** (`test/unit/commandLine.test.ts` lines 604-952): +### Comparison Table (README) -- 20+ comprehensive tests covering all edge cases -- Tests for simple fields, nested fields, multiple variables, error scenarios -- 100% coverage of new interpolation functions +README includes comparison table: SvelteESP32 vs Traditional Filesystem (SPIFFS/LittleFS/AsyncStaticWebHandler) -**Example Usage:** +- Highlights: Single Binary OTA, Automatic Gzip, Built-in ETag, CI/CD integration +- Location: README lines 15-29, after "Forget SPIFFS and LittleFS" intro -```json -// .svelteesp32rc.json -{ - "engine": "psychic", - "version": "v$npm_package_version", - "define": "$npm_package_name", - "sourcepath": "./dist" -} +### Error Messages (v1.13.1) -// package.json -{ - "name": "esp32-webui", - "version": "2.1.0" -} +Enhanced error messages with framework-specific hints: -// Result after interpolation: -{ - "version": "v2.1.0", - "define": "esp32-webui" -} -``` +- **Missing index.html**: Engine-specific routing examples, `--no-index-check` flag +- **Invalid engine**: Lists all 4 engines with descriptions +- **Sourcepath not found**: Build tool hints (Vite, Webpack, Rollup) +- **max_uri_handlers**: Console hints after generation (psychic, psychic2, espidf) -**Benefits:** +## Build Configuration -- **Version synchronization**: Header version automatically matches package version -- **Dynamic naming**: C++ defines use actual package name -- **CI/CD friendly**: Reusable RC files across projects -- **Consistency**: Single source of truth for project metadata +- **TypeScript**: Target ES2020, CommonJS output, strict mode +- **Build**: `--clean` and `--force` flags ensure clean builds (no incremental) +- **ESLint**: TypeScript, Prettier, Unicorn plugins +- **Prettier**: 120 char width, single quotes, no trailing commas From e6bdef1f3b19ed84f293766b06aebb952ad2f087 Mon Sep 17 00:00:00 2001 From: BCsabaEngine Date: Thu, 11 Dec 2025 20:42:33 +0100 Subject: [PATCH 4/4] fix: update readme --- README.md | 87 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 59 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 7452d48..68f6e3b 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,23 @@ In order to be able to easily update OTA, it is important - from the users' poin 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**. +**Quick Comparison:** + +| Feature | SvelteESP32 | Traditional Filesystem (SPIFFS/LittleFS) | +| --------------------- | --------------------------------------- | ---------------------------------------- | +| **Single Binary OTA** | ✓ Everything embedded in firmware | ✗ Requires separate partition upload | +| **Gzip Compression** | ✓ Automatic build-time (>15% reduction) | Manual or runtime compression | +| **ETag Support** | ✓ Built-in MD5 ETags with 304 responses | Manual implementation required | +| **CI/CD Integration** | ✓ npm package, simple build step | Complex with upload_fs tools | +| **Memory Efficiency** | Flash only (PROGMEM/const arrays) | Flash partition + filesystem overhead | +| **Performance** | Direct byte array serving | Filesystem read overhead | +| **Setup Complexity** | Include header, call init function | Partition setup, upload tools, handlers | + +**When to use:** + +- **SvelteESP32**: Single-binary OTA updates, CI/CD pipelines, static web content that doesn't change at runtime +- **SPIFFS/LittleFS**: Dynamic content, user-uploadable files, configuration that changes at runtime + > Starting with version v1.13.0, RC files support npm package variable interpolation > Starting with version v1.12.0, you can use RC file for configuration @@ -309,6 +326,18 @@ At the same time, it can be an advantage that the content is cached by the brows Typically, the entry point for web applications is the **index.htm or index.html** file. This does not need to be listed in the browser's address bar because web servers know that this file should be served by default. Svelteesp32 also does this: if there is an index.htm or index.html file, it sets it as the main file to be served. So using `http://esp_xxx.local` or just entering the `http://x.y.w.z/` IP address will serve this main file. +**Validation**: By default, svelteesp32 validates that an `index.html` or `index.htm` file exists in your source directory (in the root or any subdirectory). This ensures users won't get a 404 error when visiting your ESP32's root URL. + +**Skipping Validation**: If you're building an API-only application (REST endpoints without a web UI) or using a different entry point (e.g., `main.html`), you can skip this validation with the `--no-index-check` flag: + +```bash +# API-only application (no web UI) +npx svelteesp32 -e psychic -s ./dist -o ./output.h --no-index-check + +# Custom entry point (users must visit /main.html explicitly) +npx svelteesp32 -e psychic -s ./dist -o ./output.h --no-index-check +``` + ### File Exclusion The `--exclude` option allows you to exclude files from being embedded in the ESP32 firmware using glob patterns. This is useful for excluding source maps, documentation, and test files that shouldn't be part of the deployed application. @@ -418,21 +447,22 @@ You can use the following c++ directives at the project level if you want to con ### Command line options -| Option | Description | default | -| ------------- | ------------------------------------------------------------------------------------ | ----------------------- | -| `-s` | **Source dist folder contains compiled web files** | (required) | -| `-e` | The engine for which the include file is created (psychic/psychic2/async/espidf) | psychic | -| `-o` | Generated output file with path | `svelteesp32.h` | -| `--exclude` | Exclude files matching glob pattern (repeatable or comma-separated) | Default system files | -| `--etag` | Use ETag header for cache (true/false/compiler) | false | -| `--cachetime` | Override no-cache response with a max-age=\ response (seconds) | 0 | -| `--gzip` | Compress content with gzip (true/false/compiler) | true | -| `--created` | Include creation time in generated header | false | -| `--version` | Include a version string in generated header, e.g. `--version=v$npm_package_version` | '' | -| `--espmethod` | Name of generated initialization method | `initSvelteStaticFiles` | -| `--define` | Prefix of c++ defines (e.g., SVELTEESP32_COUNT) | `SVELTEESP32` | -| `--config` | Use custom RC file path | `.svelteesp32rc.json` | -| `-h` | Show help | | +| Option | Description | default | +| ------------------ | ------------------------------------------------------------------------------------ | ----------------------- | +| `-s` | **Source dist folder contains compiled web files** | (required) | +| `-e` | The engine for which the include file is created (psychic/psychic2/async/espidf) | psychic | +| `-o` | Generated output file with path | `svelteesp32.h` | +| `--exclude` | Exclude files matching glob pattern (repeatable or comma-separated) | Default system files | +| `--etag` | Use ETag header for cache (true/false/compiler) | false | +| `--cachetime` | Override no-cache response with a max-age=\ response (seconds) | 0 | +| `--gzip` | Compress content with gzip (true/false/compiler) | true | +| `--created` | Include creation time in generated header | false | +| `--version` | Include a version string in generated header, e.g. `--version=v$npm_package_version` | '' | +| `--espmethod` | Name of generated initialization method | `initSvelteStaticFiles` | +| `--define` | Prefix of c++ defines (e.g., SVELTEESP32_COUNT) | `SVELTEESP32` | +| `--config` | Use custom RC file path | `.svelteesp32rc.json` | +| `--no-index-check` | Skip validation for index.html/index.htm (for API-only or custom entry points) | false | +| `-h` | Show help | | ### Configuration File @@ -479,19 +509,20 @@ npx svelteesp32 --config=.svelteesp32rc.prod.json All CLI options can be specified in the RC file using long-form property names: -| RC Property | CLI Flag | Type | Example | -| ------------ | ------------- | ------- | ------------------------------------------------ | -| `engine` | `-e` | string | `"psychic"`, `"psychic2"`, `"async"`, `"espidf"` | -| `sourcepath` | `-s` | string | `"./dist"` | -| `outputfile` | `-o` | string | `"./output.h"` | -| `etag` | `--etag` | string | `"true"`, `"false"`, `"compiler"` | -| `gzip` | `--gzip` | string | `"true"`, `"false"`, `"compiler"` | -| `cachetime` | `--cachetime` | number | `86400` | -| `created` | `--created` | boolean | `true`, `false` | -| `version` | `--version` | string | `"v1.0.0"` | -| `espmethod` | `--espmethod` | string | `"initSvelteStaticFiles"` | -| `define` | `--define` | string | `"SVELTEESP32"` | -| `exclude` | `--exclude` | array | `["*.map", "*.md"]` | +| RC Property | CLI Flag | Type | Example | +| -------------- | ------------------ | ------- | ------------------------------------------------ | +| `engine` | `-e` | string | `"psychic"`, `"psychic2"`, `"async"`, `"espidf"` | +| `sourcepath` | `-s` | string | `"./dist"` | +| `outputfile` | `-o` | string | `"./output.h"` | +| `etag` | `--etag` | string | `"true"`, `"false"`, `"compiler"` | +| `gzip` | `--gzip` | string | `"true"`, `"false"`, `"compiler"` | +| `cachetime` | `--cachetime` | number | `86400` | +| `created` | `--created` | boolean | `true`, `false` | +| `version` | `--version` | string | `"v1.0.0"` | +| `espmethod` | `--espmethod` | string | `"initSvelteStaticFiles"` | +| `define` | `--define` | string | `"SVELTEESP32"` | +| `exclude` | `--exclude` | array | `["*.map", "*.md"]` | +| `noIndexCheck` | `--no-index-check` | boolean | `true`, `false` | #### CLI Override