399 lines
9.3 KiB
JavaScript
399 lines
9.3 KiB
JavaScript
import vue from '@vitejs/plugin-vue'
|
||
import legacy from '@vitejs/plugin-legacy'
|
||
import Components from 'unplugin-vue-components/vite'
|
||
|
||
import { defineConfig } from 'vite'
|
||
import fs from 'node:fs'
|
||
import path from 'node:path'
|
||
import process from 'node:process'
|
||
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']
|
||
// Android 9 首发设备可能仍使用 Chrome 69 级别的 System WebView。
|
||
// 显式锁定最低目标,避免 Vite 大版本升级后默认 target 再次抬高。
|
||
const ANDROID_9_WEBVIEW_TARGETS = ['Chrome >= 69', 'ChromeAndroid >= 69']
|
||
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()],
|
||
}),
|
||
legacy({
|
||
// Chrome 69 已支持 ESM,只需要降级现代模块并补齐实际使用的 ES API。
|
||
modernTargets: ANDROID_9_WEBVIEW_TARGETS,
|
||
modernPolyfills: true,
|
||
renderLegacyChunks: false,
|
||
}),
|
||
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) => {
|
||
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,
|
||
},
|
||
}
|
||
})
|