import vue from '@vitejs/plugin-vue'; import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { defineConfig } from 'vite'; const ROOT = fileURLToPath(new URL('.', import.meta.url)); const SRC = path.resolve(ROOT, 'src'); const PUBLIC = path.resolve(ROOT, 'public'); export default defineConfig(({ mode }) => { const isDev = (mode === 'development'); return { base: './', publicDir: false, plugins: [ myThoughtsPublicAssets(), myThoughtsLint(isDev), vue() ], build: { outDir: PUBLIC, assetsDir: 'assets', emptyOutDir: false, manifest: true, sourcemap: isDev, minify: !isDev, cssMinify: !isDev, assetsInlineLimit: 1 * 1024, rolldownOptions: { input: { app: path.resolve(SRC, 'app.js') }, output: { entryFileNames: isDev ? 'assets/[name].js' : 'assets/[name].[hash].js', chunkFileNames: isDev ? 'assets/[name].js' : 'assets/[name].[hash].js', assetFileNames: (assetInfo) => { const sourceName = assetInfo.names?.[0] || assetInfo.name || 'asset'; const extension = path.extname(sourceName).toLowerCase(); const isImage = ['.png', '.svg', '.jpg', '.jpeg', '.gif', '.webp'].includes(extension); const isFont = ['.woff', '.woff2', '.ttf', '.otf'].includes(extension); const folder = isImage ? 'assets/images' : (isFont ? 'assets/fonts' : 'assets'); return isDev ? `${folder}/[name][extname]` : `${folder}/[name].[hash][extname]`; } } } }, resolve: { extensions: ['.mjs', '.js', '.vue', '.scss', '.json'], alias: { '@components': path.resolve(SRC, 'components'), '@images': path.resolve(SRC, 'images'), '@scripts': path.resolve(SRC, 'scripts'), '@styles': path.resolve(SRC, 'styles') } }, define: { __VUE_OPTIONS_API__: 'true', __VUE_PROD_DEVTOOLS__: 'false', __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: 'false' } }; }); function myThoughtsLint(isDev) { return { name: 'mythoughts-lint', apply: 'build', //In `--watch` mode Vite re-runs the whole plugin pipeline (including //buildStart) on every rebuild it triggers from a file change, so this //alone gives re-linting on every save with no separate watchChange //wiring needed. async buildStart() { await runLint(isDev); } }; } async function runLint(isDev) { const { ESLint } = await import('eslint'); const eslint = new ESLint({ cwd: ROOT }); const results = await eslint.lintFiles(['src']); const formatter = await eslint.loadFormatter('stylish'); const output = await formatter.format(results); if(output) process.stdout.write(output + '\n'); //Dev keeps watching regardless - the point is fast feedback, not a gate. //Prod aborts the build so bad code can't ship. const hasErrors = results.some((result) => result.errorCount > 0); if(hasErrors && !isDev) { throw new Error('ESLint found errors in src/ - aborting production build.'); } } function myThoughtsPublicAssets() { return { name: 'mythoughts-public-assets', apply: 'build', buildStart() { cleanGeneratedAssets(); ensurePublicSymlinks(); } }; } //Everything under public/ except index.php is build output, so a rebuild //starts from a clean slate and never leaves an orphaned hashed file behind. function cleanGeneratedAssets() { fs.rmSync(path.resolve(PUBLIC, '.vite'), { recursive: true, force: true }); const assetsDir = path.resolve(PUBLIC, 'assets'); if(fs.existsSync(assetsDir)) { for(const entry of fs.readdirSync(assetsDir, { withFileTypes: true })) { const entryPath = path.resolve(assetsDir, entry.name); if(entry.name === 'images' && entry.isDirectory()) cleanImagesDir(entryPath); else fs.rmSync(entryPath, { recursive: true, force: true }); } } fs.mkdirSync(path.resolve(PUBLIC, 'assets', 'images'), { recursive: true }); } //icons/ is a symlink to source files the mask references by stable path //(favicon, webmanifest) - it must survive the sweep. function cleanImagesDir(imagesDir) { for(const entry of fs.readdirSync(imagesDir, { withFileTypes: true })) { if(entry.name === 'icons') continue; fs.rmSync(path.resolve(imagesDir, entry.name), { recursive: true, force: true }); } } function ensurePublicSymlinks() { ensureSymlink('../../../src/images/icons', path.resolve(PUBLIC, 'assets', 'images', 'icons')); } function ensureSymlink(target, linkPath) { fs.mkdirSync(path.dirname(linkPath), { recursive: true }); try { const stats = fs.lstatSync(linkPath); if(stats.isSymbolicLink()) { if(fs.readlinkSync(linkPath) === target) return; fs.unlinkSync(linkPath); } else throw new Error(`Refusing to replace non-symlink public path: ${linkPath}`); } catch (error) { if(error.code !== 'ENOENT') throw error; } fs.symlinkSync(target, linkPath); }