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: [ spotPublicAssets(), vue() ], build: { outDir: PUBLIC, assetsDir: 'assets', emptyOutDir: false, manifest: true, sourcemap: isDev, minify: !isDev, cssMinify: !isDev, assetsInlineLimit: 1 * 1024, chunkSizeWarningLimit: 1500, 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 folder = isImage ? 'assets/images' : '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 spotPublicAssets() { return { name: 'spot-public-assets', apply: 'build', buildStart() { cleanGeneratedAssets(); ensurePublicSymlinks(); } }; } function cleanGeneratedAssets() { fs.rmSync(path.resolve(PUBLIC, '.vite'), { recursive: true, force: true }); fs.rmSync(path.resolve(PUBLIC, 'maplibre'), { 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 }); } 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('../files', path.resolve(PUBLIC, 'files')); ensureSymlink('../resources/geo', path.resolve(PUBLIC, 'geo')); 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); }