forked from trent_larson/crowd-funder-for-time-pwa
feat: modernize Electron build process with Vite-based CSS injection
- Replace manual CSS injection hack with Vite plugin - Configure Vite to handle both main process and renderer builds - Update build scripts to work with proper Vite output structure - Remove fix-inject-css.js post-build script - Update BUILDING.md documentation - Add build-modernization-context.md for future reference Technical changes: - vite.config.electron.mts: Add electron-css-injection plugin and proper output config - scripts/build-electron.js: Simplify to work with Vite-generated files - BUILDING.md: Update Electron build documentation - doc/build-modernization-context.md: Document context and decisions Security/maintenance improvements: - Eliminate manual file manipulation hacks - Ensure deterministic, reproducible builds - Centralize build logic in Vite configuration - Improve developer experience and CI/CD compatibility Author: Matthew Raymer
This commit is contained in:
16
BUILDING.md
16
BUILDING.md
@@ -634,3 +634,19 @@ For iOS deep links, configure the URL scheme in Xcode:
|
|||||||
| `web` | web | true | false | Standard web browser |
|
| `web` | web | true | false | Standard web browser |
|
||||||
| `capacitor` | capacitor | false | true | Mobile app (iOS/Android) |
|
| `capacitor` | capacitor | false | true | Mobile app (iOS/Android) |
|
||||||
| `electron` | electron | false | true | Desktop app (Windows/macOS/Linux) |
|
| `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
|
||||||
|
|||||||
51
doc/build-modernization-context.md
Normal file
51
doc/build-modernization-context.md
Normal file
@@ -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.
|
||||||
@@ -12,59 +12,53 @@ if (!fs.existsSync(wwwPath)) {
|
|||||||
fs.mkdirSync(wwwPath, { recursive: true });
|
fs.mkdirSync(wwwPath, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a platform-specific index.html for Electron
|
// Copy the Vite-built index.html to www directory
|
||||||
const initialIndexContent = `<!DOCTYPE html>
|
const viteIndexPath = path.join(electronDistPath, 'index.html');
|
||||||
<html lang="">
|
const wwwIndexPath = path.join(wwwPath, 'index.html');
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta name="viewport" content="width=device-width,initial-scale=1.0,viewport-fit=cover">
|
|
||||||
<link rel="icon" href="./favicon.ico">
|
|
||||||
<title>TimeSafari</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>
|
|
||||||
<strong>We're sorry but TimeSafari doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
|
|
||||||
</noscript>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script type="module" src="./main.electron.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>`;
|
|
||||||
|
|
||||||
// Write the Electron-specific index.html
|
if (fs.existsSync(viteIndexPath)) {
|
||||||
fs.writeFileSync(path.join(wwwPath, 'index.html'), initialIndexContent);
|
console.log('Copying Vite-built index.html to www directory...');
|
||||||
|
fs.copyFileSync(viteIndexPath, wwwIndexPath);
|
||||||
// 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 favicon
|
// Remove the original index.html from dist-electron root
|
||||||
const faviconSrc = path.join(webDistPath, 'favicon.ico');
|
fs.unlinkSync(viteIndexPath);
|
||||||
if (fs.existsSync(faviconSrc)) {
|
console.log('Moved index.html to www directory');
|
||||||
fs.copyFileSync(faviconSrc, path.join(wwwPath, 'favicon.ico'));
|
} else {
|
||||||
}
|
console.error('Vite-built index.html not found at:', viteIndexPath);
|
||||||
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove service worker files
|
// 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');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 = [
|
const swFilesToRemove = [
|
||||||
'sw.js',
|
'sw.js',
|
||||||
'sw.js.map',
|
'sw.js.map',
|
||||||
'workbox-*.js',
|
'workbox-*.js',
|
||||||
'workbox-*.js.map',
|
'workbox-*.js.map',
|
||||||
'registerSW.js',
|
'registerSW.js',
|
||||||
'manifest.webmanifest',
|
'manifest.webmanifest'
|
||||||
'**/workbox-*.js',
|
|
||||||
'**/workbox-*.js.map',
|
|
||||||
'**/sw.js',
|
|
||||||
'**/sw.js.map',
|
|
||||||
'**/registerSW.js',
|
|
||||||
'**/manifest.webmanifest'
|
|
||||||
];
|
];
|
||||||
|
|
||||||
console.log('Removing service worker files...');
|
console.log('Removing service worker files...');
|
||||||
@@ -84,14 +78,13 @@ swFilesToRemove.forEach(pattern => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Also check and remove from assets directory
|
// Also check and remove from assets directory
|
||||||
const assetsPath = path.join(wwwPath, 'assets');
|
if (fs.existsSync(assetsDest)) {
|
||||||
if (fs.existsSync(assetsPath)) {
|
|
||||||
swFilesToRemove.forEach(pattern => {
|
swFilesToRemove.forEach(pattern => {
|
||||||
const files = fs.readdirSync(assetsPath).filter(file =>
|
const files = fs.readdirSync(assetsDest).filter(file =>
|
||||||
file.match(new RegExp(pattern.replace(/\*/g, '.*')))
|
file.match(new RegExp(pattern.replace(/\*/g, '.*')))
|
||||||
);
|
);
|
||||||
files.forEach(file => {
|
files.forEach(file => {
|
||||||
const filePath = path.join(assetsPath, file);
|
const filePath = path.join(assetsDest, file);
|
||||||
console.log(`Removing ${filePath}`);
|
console.log(`Removing ${filePath}`);
|
||||||
try {
|
try {
|
||||||
fs.unlinkSync(filePath);
|
fs.unlinkSync(filePath);
|
||||||
@@ -102,60 +95,54 @@ if (fs.existsSync(assetsPath)) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modify index.html to remove service worker registration
|
// Verify the final index.html structure
|
||||||
const indexPath = path.join(wwwPath, 'index.html');
|
const finalIndexContent = fs.readFileSync(wwwIndexPath, 'utf8');
|
||||||
if (fs.existsSync(indexPath)) {
|
console.log('Final index.html structure:');
|
||||||
console.log('Modifying index.html to remove service worker registration...');
|
console.log('- Has CSS link:', finalIndexContent.includes('<link rel="stylesheet"'));
|
||||||
let indexContent = fs.readFileSync(indexPath, 'utf8');
|
console.log('- Has main script:', finalIndexContent.includes('main.electron.js'));
|
||||||
|
console.log('- No service worker references:', !finalIndexContent.includes('serviceWorker'));
|
||||||
// Remove service worker registration script
|
|
||||||
indexContent = indexContent
|
|
||||||
.replace(/<script[^>]*id="vite-plugin-pwa:register-sw"[^>]*><\/script>/g, '')
|
|
||||||
.replace(/<script[^>]*registerServiceWorker[^>]*><\/script>/g, '')
|
|
||||||
.replace(/<link[^>]*rel="manifest"[^>]*>/g, '')
|
|
||||||
.replace(/<link[^>]*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');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fix asset paths
|
// Copy main process files to the correct location
|
||||||
console.log('Fixing asset paths in index.html...');
|
console.log('Setting up main process files...');
|
||||||
let modifiedIndexContent = fs.readFileSync(indexPath, 'utf8');
|
|
||||||
modifiedIndexContent = modifiedIndexContent
|
|
||||||
.replace(/\/assets\//g, './assets/')
|
|
||||||
.replace(/href="\//g, 'href="./')
|
|
||||||
.replace(/src="\//g, 'src="./');
|
|
||||||
|
|
||||||
fs.writeFileSync(indexPath, modifiedIndexContent);
|
// The main process files are already in the correct location
|
||||||
|
// Just verify they exist and are ready
|
||||||
|
const mainPath = path.join(electronDistPath, 'main.js');
|
||||||
|
const preloadPath = path.join(electronDistPath, 'preload.js');
|
||||||
|
|
||||||
// Verify no service worker references remain
|
if (fs.existsSync(mainPath)) {
|
||||||
const finalContent = fs.readFileSync(indexPath, 'utf8');
|
console.log('Main process file ready at:', mainPath);
|
||||||
if (finalContent.includes('serviceWorker') || finalContent.includes('workbox')) {
|
|
||||||
console.warn('Warning: Service worker references may still exist in index.html');
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check for remaining /assets/ paths
|
|
||||||
console.log('After path fixing, checking for remaining /assets/ paths:', finalContent.includes('/assets/'));
|
|
||||||
console.log('Sample of fixed content:', finalContent.substring(0, 500));
|
|
||||||
|
|
||||||
console.log('Copied and fixed web files in:', wwwPath);
|
|
||||||
|
|
||||||
// Copy main process files
|
|
||||||
console.log('Copying main process files...');
|
|
||||||
|
|
||||||
// Copy the main process file instead of creating a template
|
|
||||||
const mainSrcPath = path.join(__dirname, '..', 'dist-electron', 'main.js');
|
|
||||||
const mainDestPath = path.join(electronDistPath, 'main.js');
|
|
||||||
|
|
||||||
if (fs.existsSync(mainSrcPath)) {
|
|
||||||
fs.copyFileSync(mainSrcPath, mainDestPath);
|
|
||||||
console.log('Copied main process file successfully');
|
|
||||||
} else {
|
} else {
|
||||||
console.error('Main process file not found at:', mainSrcPath);
|
console.error('Main process file not found at:', mainPath);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log('Electron build process completed successfully');
|
if (fs.existsSync(preloadPath)) {
|
||||||
|
console.log('Preload script ready at:', preloadPath);
|
||||||
|
} else {
|
||||||
|
console.warn('Preload script not found at:', preloadPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up any remaining files in dist-electron root (except main.js, preload.js, and www directory)
|
||||||
|
const remainingFiles = fs.readdirSync(electronDistPath);
|
||||||
|
remainingFiles.forEach(file => {
|
||||||
|
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'));
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import './assets/styles/tailwind.css';
|
||||||
import { initializeApp } from "./main.common";
|
import { initializeApp } from "./main.common";
|
||||||
import { logger } from "./utils/logger";
|
import { logger } from "./utils/logger";
|
||||||
|
|
||||||
|
|||||||
@@ -10,14 +10,30 @@ export default defineConfig(async () => {
|
|||||||
outDir: 'dist-electron',
|
outDir: 'dist-electron',
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: {
|
input: {
|
||||||
|
// Main process entry points
|
||||||
main: path.resolve(__dirname, 'src/electron/main.ts'),
|
main: path.resolve(__dirname, 'src/electron/main.ts'),
|
||||||
preload: path.resolve(__dirname, 'src/electron/preload.js'),
|
preload: path.resolve(__dirname, 'src/electron/preload.js'),
|
||||||
|
// Renderer process entry point (the web app)
|
||||||
|
app: path.resolve(__dirname, 'index.html'),
|
||||||
},
|
},
|
||||||
external: ['electron'],
|
external: ['electron'],
|
||||||
output: {
|
output: {
|
||||||
format: 'cjs',
|
format: 'cjs',
|
||||||
entryFileNames: '[name].js',
|
entryFileNames: (chunkInfo) => {
|
||||||
assetFileNames: 'assets/[name].[ext]',
|
// 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',
|
target: 'node18',
|
||||||
@@ -93,6 +109,40 @@ export default defineConfig(async () => {
|
|||||||
}
|
}
|
||||||
return null;
|
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 = ` <link rel="stylesheet" href="${cssHref}">\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>.*?<\/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: {
|
ssr: {
|
||||||
|
|||||||
@@ -14,12 +14,14 @@ export default defineConfig({
|
|||||||
outDir: 'dist-electron/www',
|
outDir: 'dist-electron/www',
|
||||||
emptyOutDir: false,
|
emptyOutDir: false,
|
||||||
assetsDir: 'assets',
|
assetsDir: 'assets',
|
||||||
|
cssCodeSplit: false,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
input: path.resolve(__dirname, 'src/main.electron.ts'),
|
input: path.resolve(__dirname, 'src/main.electron.ts'),
|
||||||
output: {
|
output: {
|
||||||
entryFileNames: 'main.electron.js',
|
entryFileNames: 'main.electron.js',
|
||||||
assetFileNames: 'assets/[name]-[hash][extname]',
|
assetFileNames: 'assets/[name]-[hash][extname]',
|
||||||
chunkFileNames: 'assets/[name]-[hash].js'
|
chunkFileNames: 'assets/[name]-[hash].js',
|
||||||
|
manualChunks: undefined
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
commonjsOptions: {
|
commonjsOptions: {
|
||||||
|
|||||||
Reference in New Issue
Block a user