diff --git a/.gitignore b/.gitignore index a547bf3..251ce6d 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,6 @@ pnpm-debug.log* lerna-debug.log* node_modules -dist dist-ssr *.local diff --git a/README.md b/README.md index 1b6b114..44b72cc 100644 --- a/README.md +++ b/README.md @@ -24,13 +24,16 @@ npm run dev # http://localhost:5173 ## Production Build ```bash -npm run build # → dist/ (static files) +npm run build # → dist/index.html (single self-contained file) npm run preview # test the production build locally ``` -The `dist/` folder contains fully static HTML/CSS/JS that works from: +The build produces a **single `dist/index.html`** file with all JavaScript, CSS, and assets (including favicon) inlined as data URIs. No external files, no network requests — it works from: - `file://` protocol (open `dist/index.html` directly) - Any static web server (nginx, Apache, GitHub Pages, etc.) +- USB stick, email attachment, or any offline medium + +The single-file output is handled by [`vite-plugin-singlefile`](https://www.npmjs.com/package/vite-plugin-singlefile) for JS/CSS inlining, plus a post-build script that inlines the favicon SVG and removes leftover asset files. ## Architecture @@ -57,6 +60,8 @@ src/ │ └── security.svelte.js # Auto-lock timer, visibility detection └── styles/ └── main.css # Dark theme, CSS variables, responsive +scripts/ + └── inline-assets.js # Post-build: inlines favicon, removes leftover files ``` ### Encryption Flow @@ -107,8 +112,9 @@ npm run preview # Preview production build ### Stack -- **Svelte 5** — Runes-based reactivity (`$state`, `$derived`, `$effect`) +- **Svelte 5** — Runes-based reactivity (`$state`, `$derived`, `$effect`), props-based event passing - **Vite 8** — Build tool and dev server +- **vite-plugin-singlefile** — Inlines all JS/CSS into a single HTML file - **idb** — Promise-based IndexedDB wrapper - **Web Crypto API** — Native browser cryptography (no external crypto libraries) - **Vanilla CSS** — Dark theme with CSS custom properties, no preprocessors diff --git a/dist/index.html b/dist/index.html new file mode 100644 index 0000000..641a962 --- /dev/null +++ b/dist/index.html @@ -0,0 +1,16 @@ + + + + + + + Password Vault + + + + +
+ + diff --git a/package-lock.json b/package-lock.json index 3cf1730..0311ca0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "jsdom": "^29.1.1", "svelte": "^5.55.5", "vite": "^8.0.12", + "vite-plugin-singlefile": "^2.3.3", "vitest": "^4.1.6" } }, @@ -1030,6 +1031,19 @@ "require-from-string": "^2.0.2" } }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -1236,6 +1250,19 @@ "dev": true, "license": "MIT" }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/flatted": { "version": "3.4.2", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", @@ -1277,6 +1304,16 @@ "integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==", "license": "ISC" }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -1647,6 +1684,33 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -1990,6 +2054,19 @@ "dev": true, "license": "MIT" }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, "node_modules/totalist": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", @@ -2122,6 +2199,28 @@ } } }, + "node_modules/vite-plugin-singlefile": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/vite-plugin-singlefile/-/vite-plugin-singlefile-2.3.3.tgz", + "integrity": "sha512-XVnGH0QzbOa8fxRSsHdCarVN1BSBXNi7uLMQYlrGRN5apdHkk62XQWRJhVever0lnfuyBkwn+kvVChdm/OoOUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">18.0.0" + }, + "peerDependencies": { + "rollup": "^4.59.0", + "vite": "^5.4.21 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, "node_modules/vitefu": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", diff --git a/package.json b/package.json index 76393c9..2063e1c 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "dev": "vite", - "build": "vite build", + "build": "vite build && node scripts/inline-assets.js", "preview": "vite preview --host 0.0.0.0", "test": "vitest", "test:run": "vitest run", @@ -19,6 +19,7 @@ "jsdom": "^29.1.1", "svelte": "^5.55.5", "vite": "^8.0.12", + "vite-plugin-singlefile": "^2.3.3", "vitest": "^4.1.6" }, "dependencies": { diff --git a/scripts/inline-assets.js b/scripts/inline-assets.js new file mode 100644 index 0000000..5c7dfd5 --- /dev/null +++ b/scripts/inline-assets.js @@ -0,0 +1,47 @@ +/** + * Post-build script: inline remaining external assets (favicon) into index.html + * and remove leftover files so only a single HTML file remains. + */ +import { readFileSync, writeFileSync, rmSync, existsSync } from 'fs' +import { join, dirname } from 'path' +import { fileURLToPath } from 'url' + +const __dirname = dirname(fileURLToPath(import.meta.url)) +const distDir = join(__dirname, '..', 'dist') + +// Read favicon SVG and encode as data URI +const faviconPath = join(distDir, 'favicon.svg') +if (existsSync(faviconPath)) { + const svgContent = readFileSync(faviconPath, 'utf8') + const encoded = Buffer.from(svgContent).toString('base64') + const dataUri = `data:image/svg+xml;base64,${encoded}` + + // Replace the favicon link in index.html + const indexPath = join(distDir, 'index.html') + let html = readFileSync(indexPath, 'utf8') + html = html.replace( + /]*href="[^"]*favicon\.svg"[^>]*\/?>/i, + `` + ) + writeFileSync(indexPath, html) + + // Remove the standalone SVG + rmSync(faviconPath) + console.log('[inline-assets] Inlined favicon.svg into index.html') +} + +// Remove any other leftover asset files (e.g. icons.svg from Svelte compiler) +const iconsPath = join(distDir, 'icons.svg') +if (existsSync(iconsPath)) { + rmSync(iconsPath) + console.log('[inline-assets] Removed icons.svg') +} + +// Remove assets directory if it exists +const assetsDir = join(distDir, 'assets') +if (existsSync(assetsDir)) { + rmSync(assetsDir, { recursive: true }) + console.log('[inline-assets] Removed assets/ directory') +} + +console.log('[inline-assets] Done — dist/ contains only index.html') diff --git a/vite.config.js b/vite.config.js index 14de4c8..379fcf4 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,9 +1,20 @@ import { defineConfig } from 'vite' import { svelte } from '@sveltejs/vite-plugin-svelte' +import { viteSingleFile } from 'vite-plugin-singlefile' // https://vite.dev/config/ export default defineConfig({ - plugins: [svelte()], + plugins: [ + svelte(), + viteSingleFile({ + inlineStyles: true, + inlineScripts: true, + removeUnusedCss: true, + }), + ], + build: { + target: 'esnext', + }, preview: { allowedHosts: ["dev.thecookiejar.me"] },