import vue from '@vitejs/plugin-vue' import Components from 'unplugin-vue-components/vite' import { defineConfig } from 'vite' import fs from 'node:fs' import path from 'node:path' import { fileURLToPath, URL } from 'node:url' import { VantResolver } from 'unplugin-vue-components/resolvers' const PRODUCTION_LIKE_MODES = new Set(['production', 'production-atu']) const STRIPPABLE_CONSOLE_METHODS = ['log', 'debug', 'info'] const LOCAL_OSS_MIME_TYPES = { '.gif': 'image/gif', '.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.svg': 'image/svg+xml', '.webp': 'image/webp', } function isEscaped(code, index) { let backslashCount = 0 let cursor = index - 1 while (cursor >= 0 && code[cursor] === '\\') { backslashCount += 1 cursor -= 1 } return backslashCount % 2 === 1 } function findCallEnd(code, openParenIndex) { let depth = 0 let quote = null let templateExpressionDepth = 0 let inLineComment = false let inBlockComment = false for (let index = openParenIndex; index < code.length; index += 1) { const current = code[index] const next = code[index + 1] if (inLineComment) { if (current === '\n') { inLineComment = false } continue } if (inBlockComment) { if (current === '*' && next === '/') { inBlockComment = false index += 1 } continue } if (quote) { if (quote === '`') { if (current === '$' && next === '{' && !isEscaped(code, index)) { templateExpressionDepth += 1 index += 1 continue } if (current === '}' && templateExpressionDepth > 0 && !isEscaped(code, index)) { templateExpressionDepth -= 1 continue } if (current === '`' && templateExpressionDepth === 0 && !isEscaped(code, index)) { quote = null } continue } if (current === quote && !isEscaped(code, index)) { quote = null } continue } if (current === '/' && next === '/') { inLineComment = true index += 1 continue } if (current === '/' && next === '*') { inBlockComment = true index += 1 continue } if (current === '"' || current === "'" || current === '`') { quote = current continue } if (current === '(') { depth += 1 continue } if (current === ')') { depth -= 1 if (depth === 0) { let endIndex = index + 1 while (endIndex < code.length && /\s/.test(code[endIndex])) { endIndex += 1 } if (code[endIndex] === ';') { endIndex += 1 } return endIndex } } } return -1 } function findTokenOutsideLiterals(code, token, fromIndex = 0) { let quote = null let templateExpressionDepth = 0 let inLineComment = false let inBlockComment = false for (let index = fromIndex; index < code.length; index += 1) { const current = code[index] const next = code[index + 1] if (inLineComment) { if (current === '\n') { inLineComment = false } continue } if (inBlockComment) { if (current === '*' && next === '/') { inBlockComment = false index += 1 } continue } if (quote) { if (quote === '`') { if (current === '$' && next === '{' && !isEscaped(code, index)) { templateExpressionDepth += 1 index += 1 continue } if (current === '}' && templateExpressionDepth > 0 && !isEscaped(code, index)) { templateExpressionDepth -= 1 continue } if (current === '`' && templateExpressionDepth === 0 && !isEscaped(code, index)) { quote = null } continue } if (current === quote && !isEscaped(code, index)) { quote = null } continue } if (current === '/' && next === '/') { inLineComment = true index += 1 continue } if (current === '/' && next === '*') { inBlockComment = true index += 1 continue } if (current === '"' || current === "'" || current === '`') { quote = current continue } if (code.startsWith(token, index)) { return index } } return -1 } function stripSelectedConsoleCalls(code) { let output = code STRIPPABLE_CONSOLE_METHODS.forEach((method) => { let searchIndex = 0 const token = `console.${method}` while (searchIndex < output.length) { const startIndex = findTokenOutsideLiterals(output, token, searchIndex) if (startIndex === -1) { break } let cursor = startIndex + token.length while (cursor < output.length && /\s/.test(output[cursor])) { cursor += 1 } if (output[cursor] !== '(') { searchIndex = startIndex + token.length continue } const endIndex = findCallEnd(output, cursor) if (endIndex === -1) { searchIndex = cursor + 1 continue } output = `${output.slice(0, startIndex)}void 0;${output.slice(endIndex)}` searchIndex = startIndex + 'void 0;'.length } }) return output } function createBuildLogControlPlugin(stripDebugLogs) { return { name: 'likei-build-log-control', apply: 'build', enforce: 'post', transform(code, id) { if (!stripDebugLogs || id.includes('node_modules')) { return null } if (!/\.(vue|js|ts|jsx|tsx|mjs|cjs)(\?|$)/.test(id)) { return null } const transformedCode = stripSelectedConsoleCalls(code) if (transformedCode === code) { return null } return { code: transformedCode, map: null, } }, } } function createLocalOssAssetPlugin() { const ossRoot = fileURLToPath(new URL('./public/oss', import.meta.url)) return { name: 'likei-local-oss-assets', apply: 'serve', configureServer(server) { server.middlewares.use('/oss', (req, res, next) => { const rawPath = req.url?.split('?')[0] || '/' let decodedPath = '' try { decodedPath = decodeURIComponent(rawPath) } catch { next() return } const normalizedPath = path.normalize(decodedPath).replace(/^(\.\.[/\\])+/, '') const filePath = path.join(ossRoot, normalizedPath) const safeRoot = `${ossRoot}${path.sep}` if (!filePath.startsWith(safeRoot)) { next() return } if (!fs.existsSync(filePath) || !fs.statSync(filePath).isFile()) { next() return } const contentType = LOCAL_OSS_MIME_TYPES[path.extname(filePath).toLowerCase()] if (contentType) { res.setHeader('Content-Type', contentType) } fs.createReadStream(filePath).pipe(res) }) }, } } export default defineConfig(({ mode }) => { const keepBuildLogs = process.env.LIKEI_KEEP_BUILD_LOGS === 'true' const stripDebugLogs = PRODUCTION_LIKE_MODES.has(mode) && !keepBuildLogs return { plugins: [ createLocalOssAssetPlugin(), vue(), Components({ resolvers: [VantResolver()], }), createBuildLogControlPlugin(stripDebugLogs), ], resolve: { alias: { '@': fileURLToPath(new URL('./src', import.meta.url)), }, }, build: { minify: 'esbuild', sourcemap: false, cssMinify: 'esbuild', reportCompressedSize: false, esbuild: { drop: stripDebugLogs ? ['debugger'] : [], charset: 'ascii', legalComments: 'none', minifyIdentifiers: true, minifySyntax: true, minifyWhitespace: true, treeShaking: true, }, rollupOptions: { output: { entryFileNames: 'js/[hash].js', chunkFileNames: 'js/[hash].js', assetFileNames: (assetInfo) => { const info = assetInfo.name.split('.') const ext = info[info.length - 1] if (/\.(css)$/.test(assetInfo.name)) { return 'css/[hash].[ext]' } return 'assets/[hash].[ext]' }, }, }, }, server: { host: '0.0.0.0', port: 5173, strictPort: true, open: false, cors: true, proxy: { '/oss': { target: 'https://asset.global-interaction.com', changeOrigin: true, secure: true, rewrite: (path) => path.replace(/^\/oss/, ''), configure: (proxy) => { proxy.on('proxyRes', (proxyRes) => { delete proxyRes.headers.etag delete proxyRes.headers['last-modified'] delete proxyRes.headers['cache-control'] delete proxyRes.headers.expires }) }, }, }, }, preview: { host: '0.0.0.0', port: 4173, strictPort: true, open: false, }, } })