Single file output.

This commit is contained in:
Timothy Farrell 2026-05-12 17:22:52 +00:00
parent 3f5cd5825e
commit b3697fa61f
7 changed files with 185 additions and 6 deletions

1
.gitignore vendored
View File

@ -8,7 +8,6 @@ pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local

View File

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

16
dist/index.html vendored Normal file

File diff suppressed because one or more lines are too long

99
package-lock.json generated
View File

@ -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",

View File

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

47
scripts/inline-assets.js Normal file
View File

@ -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(
/<link rel="icon"[^>]*href="[^"]*favicon\.svg"[^>]*\/?>/i,
`<link rel="icon" type="image/svg+xml" href="${dataUri}" />`
)
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')

View File

@ -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"]
},