diff --git a/BUILDING.md b/BUILDING.md index 5f7dcd85..996d6573 100644 --- a/BUILDING.md +++ b/BUILDING.md @@ -634,3 +634,19 @@ For iOS deep links, configure the URL scheme in Xcode: | `web` | web | true | false | Standard web browser | | `capacitor` | capacitor | false | true | Mobile app (iOS/Android) | | `electron` | electron | false | true | Desktop app (Windows/macOS/Linux) | + +## Electron Build: CSS Injection + +The Electron build now uses Vite's built-in CSS handling with a custom plugin (`electron-css-injection`) that automatically injects CSS links into the generated `index.html` file. This replaces the previous manual CSS injection script. + +**Plugin:** `vite.config.electron.mts` - `electron-css-injection` plugin + +**Features:** +- Automatically detects and injects CSS files generated by Vite +- Ensures proper relative paths for Electron builds +- Handles multiple CSS files if present +- Provides detailed logging during build process + +**No manual intervention required** - CSS injection is handled automatically during the Vite build process. + +**Author:** Matthew Raymer diff --git a/doc/build-modernization-context.md b/doc/build-modernization-context.md new file mode 100644 index 00000000..133c2ad6 --- /dev/null +++ b/doc/build-modernization-context.md @@ -0,0 +1,51 @@ +# TimeSafari Build Modernization Context + +**Author:** Matthew Raymer + +## Motivation +- Eliminate manual hacks and post-build scripts for Electron builds +- Ensure maintainability, reproducibility, and security of build outputs +- Unify build, test, and deployment scripts for developer experience and CI/CD + +## Key Technical Decisions +- **Vite is the single source of truth for build output** + - All Electron build output (main process, preload, renderer HTML/CSS/JS) is managed by `vite.config.electron.mts` +- **CSS injection for Electron is handled by a Vite plugin** + - No more manual HTML/CSS edits or post-build scripts +- **Build scripts are unified and robust** + - Use `./scripts/build-electron.sh` for all Electron builds + - No more `fix-inject-css.js` or similar hacks +- **Output structure is deterministic and ASAR-friendly** + - Main process: `dist-electron/main.js` + - Preload: `dist-electron/preload.js` + - Renderer assets: `dist-electron/www/` (HTML, CSS, JS) + +## Security & Maintenance Checklist +- [x] All scripts and configs are committed and documented +- [x] No manual file hacks remain +- [x] All build output is deterministic and reproducible +- [x] No sensitive data is exposed in the build process +- [x] Documentation (`BUILDING.md`) is up to date + +## How to Build Electron +1. Run: + ```bash + ./scripts/build-electron.sh + ``` +2. Output will be in `dist-electron/`: + - `main.js`, `preload.js` in root + - `www/` contains all renderer assets +3. No manual post-processing is required + +## Customization +- **Vite config:** All build output and asset handling is controlled in `vite.config.electron.mts` +- **CSS/HTML injection:** Use Vite plugins (see `electron-css-injection` in the config) for further customization +- **Build scripts:** All orchestration is in `scripts/` and documented in `BUILDING.md` + +## For Future Developers +- Always use Vite plugins/config for build output changes +- Never manually edit built files or inject assets post-build +- Keep documentation and scripts in sync with the build process + +--- +This file documents the context and rationale for the build modernization and should be included in the repository for onboarding and future reference. \ No newline at end of file diff --git a/scripts/build-electron.js b/scripts/build-electron.js index 6f7a7a3e..74f172ea 100644 --- a/scripts/build-electron.js +++ b/scripts/build-electron.js @@ -12,59 +12,53 @@ if (!fs.existsSync(wwwPath)) { fs.mkdirSync(wwwPath, { recursive: true }); } -// Create a platform-specific index.html for Electron -const initialIndexContent = ` - - - - - - - TimeSafari - - - -
- - -`; - -// Write the Electron-specific index.html -fs.writeFileSync(path.join(wwwPath, 'index.html'), initialIndexContent); - -// Copy only necessary assets from web build -const webDistPath = path.join(__dirname, '..', 'dist'); -if (fs.existsSync(webDistPath)) { - // Copy assets directory - const assetsSrc = path.join(webDistPath, 'assets'); - const assetsDest = path.join(wwwPath, 'assets'); - if (fs.existsSync(assetsSrc)) { - fs.cpSync(assetsSrc, assetsDest, { recursive: true }); - } +// Copy the Vite-built index.html to www directory +const viteIndexPath = path.join(electronDistPath, 'index.html'); +const wwwIndexPath = path.join(wwwPath, 'index.html'); + +if (fs.existsSync(viteIndexPath)) { + console.log('Copying Vite-built index.html to www directory...'); + fs.copyFileSync(viteIndexPath, wwwIndexPath); - // Copy favicon - const faviconSrc = path.join(webDistPath, 'favicon.ico'); - if (fs.existsSync(faviconSrc)) { - fs.copyFileSync(faviconSrc, path.join(wwwPath, 'favicon.ico')); + // Remove the original index.html from dist-electron root + fs.unlinkSync(viteIndexPath); + console.log('Moved index.html to www directory'); +} else { + console.error('Vite-built index.html not found at:', viteIndexPath); + process.exit(1); +} + +// Copy assets directory if it exists in dist-electron +const assetsSrc = path.join(electronDistPath, 'assets'); +const assetsDest = path.join(wwwPath, 'assets'); + +if (fs.existsSync(assetsSrc)) { + console.log('Moving assets directory to www...'); + if (fs.existsSync(assetsDest)) { + fs.rmSync(assetsDest, { recursive: true, force: true }); } + fs.renameSync(assetsSrc, assetsDest); + console.log('Moved assets directory to www'); } -// Remove service worker files +// Copy favicon if it exists +const faviconSrc = path.join(electronDistPath, 'favicon.ico'); +const faviconDest = path.join(wwwPath, 'favicon.ico'); + +if (fs.existsSync(faviconSrc)) { + console.log('Moving favicon to www...'); + fs.renameSync(faviconSrc, faviconDest); + console.log('Moved favicon to www'); +} + +// Remove service worker files from www directory const swFilesToRemove = [ 'sw.js', 'sw.js.map', 'workbox-*.js', 'workbox-*.js.map', 'registerSW.js', - 'manifest.webmanifest', - '**/workbox-*.js', - '**/workbox-*.js.map', - '**/sw.js', - '**/sw.js.map', - '**/registerSW.js', - '**/manifest.webmanifest' + 'manifest.webmanifest' ]; console.log('Removing service worker files...'); @@ -84,14 +78,13 @@ swFilesToRemove.forEach(pattern => { }); // Also check and remove from assets directory -const assetsPath = path.join(wwwPath, 'assets'); -if (fs.existsSync(assetsPath)) { +if (fs.existsSync(assetsDest)) { swFilesToRemove.forEach(pattern => { - const files = fs.readdirSync(assetsPath).filter(file => + const files = fs.readdirSync(assetsDest).filter(file => file.match(new RegExp(pattern.replace(/\*/g, '.*'))) ); files.forEach(file => { - const filePath = path.join(assetsPath, file); + const filePath = path.join(assetsDest, file); console.log(`Removing ${filePath}`); try { fs.unlinkSync(filePath); @@ -102,60 +95,54 @@ if (fs.existsSync(assetsPath)) { }); } -// Modify index.html to remove service worker registration -const indexPath = path.join(wwwPath, 'index.html'); -if (fs.existsSync(indexPath)) { - console.log('Modifying index.html to remove service worker registration...'); - let indexContent = fs.readFileSync(indexPath, 'utf8'); - - // Remove service worker registration script - indexContent = indexContent - .replace(/]*id="vite-plugin-pwa:register-sw"[^>]*><\/script>/g, '') - .replace(/]*registerServiceWorker[^>]*><\/script>/g, '') - .replace(/]*rel="manifest"[^>]*>/g, '') - .replace(/]*rel="serviceworker"[^>]*>/g, '') - .replace(/navigator\.serviceWorker\.register\([^)]*\)/g, '') - .replace(/if\s*\(\s*['"]serviceWorker['"]\s*in\s*navigator\s*\)\s*{[^}]*}/g, ''); - - fs.writeFileSync(indexPath, indexContent); - console.log('Successfully modified index.html'); -} +// Verify the final index.html structure +const finalIndexContent = fs.readFileSync(wwwIndexPath, 'utf8'); +console.log('Final index.html structure:'); +console.log('- Has CSS link:', finalIndexContent.includes(' { + if (file !== 'main.js' && file !== 'preload.js' && file !== 'www') { + const filePath = path.join(electronDistPath, file); + console.log(`Removing remaining file: ${file}`); + try { + if (fs.statSync(filePath).isDirectory()) { + fs.rmSync(filePath, { recursive: true, force: true }); + } else { + fs.unlinkSync(filePath); + } + } catch (err) { + console.warn(`Could not remove ${filePath}:`, err.message); + } + } +}); + +console.log('Electron build process completed successfully'); +console.log('Final structure:'); +console.log('- Main process:', path.join(electronDistPath, 'main.js')); +console.log('- Preload script:', path.join(electronDistPath, 'preload.js')); +console.log('- Web assets:', path.join(electronDistPath, 'www')); \ No newline at end of file diff --git a/src/main.electron.ts b/src/main.electron.ts index c71807af..0a20d8f8 100644 --- a/src/main.electron.ts +++ b/src/main.electron.ts @@ -1,3 +1,4 @@ +import './assets/styles/tailwind.css'; import { initializeApp } from "./main.common"; import { logger } from "./utils/logger"; diff --git a/vite.config.electron.mts b/vite.config.electron.mts index 07188939..b4d22c68 100644 --- a/vite.config.electron.mts +++ b/vite.config.electron.mts @@ -10,14 +10,30 @@ export default defineConfig(async () => { outDir: 'dist-electron', rollupOptions: { input: { + // Main process entry points main: path.resolve(__dirname, 'src/electron/main.ts'), preload: path.resolve(__dirname, 'src/electron/preload.js'), + // Renderer process entry point (the web app) + app: path.resolve(__dirname, 'index.html'), }, external: ['electron'], output: { format: 'cjs', - entryFileNames: '[name].js', - assetFileNames: 'assets/[name].[ext]', + entryFileNames: (chunkInfo) => { + // Use different formats for main process vs renderer + if (chunkInfo.name === 'main' || chunkInfo.name === 'preload') { + return '[name].js'; + } + return 'assets/[name].[hash].js'; + }, + assetFileNames: (assetInfo) => { + // Keep main process files in root, others in assets + if (assetInfo.name === 'main.js' || assetInfo.name === 'preload.js') { + return '[name].[ext]'; + } + return 'assets/[name].[hash].[ext]'; + }, + chunkFileNames: 'assets/[name].[hash].js', }, }, target: 'node18', @@ -93,6 +109,40 @@ export default defineConfig(async () => { } return null; } + }, + { + name: 'electron-css-injection', + enforce: 'post', + generateBundle(options, bundle) { + // Find the main CSS file + const cssAsset = Object.values(bundle).find( + (asset: any) => asset.type === 'asset' && asset.fileName?.endsWith('.css') + ) as any; + + if (cssAsset) { + // Find the HTML file and inject CSS link + const htmlAsset = Object.values(bundle).find( + (asset: any) => asset.type === 'asset' && asset.fileName?.endsWith('.html') + ) as any; + + if (htmlAsset) { + const cssHref = `./${cssAsset.fileName}`; + const cssLink = ` \n`; + + // Check if CSS link already exists + if (!htmlAsset.source.includes(cssHref)) { + // Inject CSS link after the title tag + htmlAsset.source = htmlAsset.source.replace( + /(.*?<\/title>)/, + `$1\n${cssLink}` + ); + console.log(`[electron-css-injection] Injected CSS link: ${cssHref}`); + } else { + console.log(`[electron-css-injection] CSS link already present: ${cssHref}`); + } + } + } + } } ], ssr: { diff --git a/vite.config.electron.renderer.mts b/vite.config.electron.renderer.mts index dcdb489d..9e0f7b36 100644 --- a/vite.config.electron.renderer.mts +++ b/vite.config.electron.renderer.mts @@ -14,12 +14,14 @@ export default defineConfig({ outDir: 'dist-electron/www', emptyOutDir: false, assetsDir: 'assets', + cssCodeSplit: false, rollupOptions: { input: path.resolve(__dirname, 'src/main.electron.ts'), output: { entryFileNames: 'main.electron.js', assetFileNames: 'assets/[name]-[hash][extname]', - chunkFileNames: 'assets/[name]-[hash].js' + chunkFileNames: 'assets/[name]-[hash].js', + manualChunks: undefined } }, commonjsOptions: {