Single file output.
This commit is contained in:
parent
3f5cd5825e
commit
b3697fa61f
1
.gitignore
vendored
1
.gitignore
vendored
@ -8,7 +8,6 @@ pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
|
||||
12
README.md
12
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
|
||||
|
||||
16
dist/index.html
vendored
Normal file
16
dist/index.html
vendored
Normal file
File diff suppressed because one or more lines are too long
99
package-lock.json
generated
99
package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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
47
scripts/inline-assets.js
Normal 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')
|
||||
@ -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"]
|
||||
},
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user