Resolve recommended-profile Critical and High findings, update vulnerable dependencies, restore invoice queue E2E authentication setup, and clear the remaining frontend Qodana findings.
695 lines
25 KiB
JavaScript
695 lines
25 KiB
JavaScript
import path from 'node:path'
|
|
import fs from 'node:fs'
|
|
import crypto from 'node:crypto'
|
|
import { fileURLToPath, URL } from 'node:url'
|
|
import { defineConfig } from 'vite'
|
|
import vue from '@vitejs/plugin-vue'
|
|
import vueDevTools from 'vite-plugin-vue-devtools'
|
|
import { viteSingleFile } from 'vite-plugin-singlefile'
|
|
import { VitePWA } from 'vite-plugin-pwa'
|
|
import VueJsx from '@vitejs/plugin-vue-jsx'
|
|
import { execSync } from 'node:child_process'
|
|
|
|
const BUEFY_CSS_REPLACEMENTS = new Map([
|
|
[
|
|
'@media screen and (max-width: calc(var(--bulma-sidebar-mobile-breakpoint) - 1px))',
|
|
'@media screen and (max-width: 768px)'
|
|
],
|
|
[
|
|
'@media screen and (max-width: var(--bulma-steps-mobile-max-width)-1px)',
|
|
'@media screen and (max-width: 768px)'
|
|
],
|
|
[
|
|
'var(--bulma-$steps-divider-height)',
|
|
'var(--bulma-steps-divider-height)'
|
|
]
|
|
])
|
|
|
|
const projectRoot = fileURLToPath(new URL('.', import.meta.url))
|
|
const fromProjectRoot = (...segments) => path.join(projectRoot, ...segments)
|
|
const PUBLIC_ASSET_ALIASES = [
|
|
['favicons/favicon-96x96.png', 'assets/favicons/favicon-96x96.png'],
|
|
['favicons/favicon.svg', 'assets/favicons/favicon.svg'],
|
|
['favicons/favicon.ico', 'assets/favicons/favicon.ico'],
|
|
['favicons/apple-touch-icon.png', 'assets/favicons/apple-touch-icon.png'],
|
|
['favicons/web-app-manifest-192x192.png', 'assets/favicons/web-app-manifest-192x192.png'],
|
|
['favicons/web-app-manifest-512x512.png', 'assets/favicons/web-app-manifest-512x512.png'],
|
|
]
|
|
const DEFAULT_RELEASE_FRONTEND_BASE_PATH = './'
|
|
|
|
function normalizeReleaseFrontendBasePath(value) {
|
|
const rawValue = String(value || '').trim()
|
|
let basePath = rawValue
|
|
|
|
if (/^https?:\/\//i.test(rawValue)) {
|
|
try {
|
|
basePath = new URL(rawValue).pathname
|
|
} catch {
|
|
basePath = ''
|
|
}
|
|
}
|
|
|
|
if (!basePath || basePath === '/') {
|
|
return '/'
|
|
}
|
|
if (basePath === './') {
|
|
return './'
|
|
}
|
|
|
|
return `/${basePath.replace(/^\/+|\/+$/g, '')}/`
|
|
}
|
|
|
|
function releaseFrontendBasePath() {
|
|
return normalizeReleaseFrontendBasePath(
|
|
process.env.VITE_FRONTEND_BASE_PATH ||
|
|
process.env.VITE_BASE_PATH ||
|
|
process.env.RELEASE_FRONTEND_BASE_PATH ||
|
|
DEFAULT_RELEASE_FRONTEND_BASE_PATH
|
|
)
|
|
}
|
|
|
|
function releaseManifestPath(pathname) {
|
|
const normalizedPathname = pathname.startsWith('/') ? pathname : `/${pathname}`
|
|
const basePath = releaseFrontendBasePath()
|
|
if (basePath === '/' || basePath === './') {
|
|
return normalizedPathname
|
|
}
|
|
|
|
const basePrefix = basePath.replace(/\/+$/g, '')
|
|
if (normalizedPathname === basePrefix) {
|
|
return '/'
|
|
}
|
|
if (normalizedPathname.startsWith(`${basePrefix}/`)) {
|
|
return normalizedPathname.slice(basePrefix.length) || '/'
|
|
}
|
|
|
|
return normalizedPathname
|
|
}
|
|
|
|
function releaseManifestAssetPath(value) {
|
|
return String(value || '').replace(/^\/+/, '')
|
|
}
|
|
|
|
function getGitCommit() {
|
|
const envCommit = firstReleaseCommitEnv()
|
|
if (envCommit) {
|
|
return envCommit.slice(0, 12)
|
|
}
|
|
|
|
try {
|
|
return execSync('git rev-parse --short HEAD').toString().trim()
|
|
} catch {
|
|
return 'unknown'
|
|
}
|
|
}
|
|
|
|
function getGitCommitSha() {
|
|
const envCommit = firstReleaseCommitEnv()
|
|
if (envCommit) {
|
|
return envCommit
|
|
}
|
|
|
|
try {
|
|
return execSync('git rev-parse HEAD').toString().trim()
|
|
} catch {
|
|
return 'unknown'
|
|
}
|
|
}
|
|
|
|
function firstReleaseCommitEnv() {
|
|
for (const key of ['RELEASE_COMMIT_SHA', 'SOURCE_COMMIT', 'COMMIT_SHA', 'GITHUB_SHA', 'VITE_COMMIT_HASH']) {
|
|
const value = String(process.env[key] || '').trim()
|
|
if (/^[0-9a-f]{7,40}$/i.test(value)) {
|
|
return value
|
|
}
|
|
}
|
|
return ''
|
|
}
|
|
|
|
function releaseBuildId(commitSha) {
|
|
const explicitBuildId = process.env.RELEASE_BUILD_ID || process.env.BUILD_ID
|
|
if (explicitBuildId) {
|
|
return explicitBuildId
|
|
}
|
|
|
|
if (process.env.GITHUB_RUN_ID) {
|
|
return [process.env.GITHUB_RUN_ID, process.env.GITHUB_RUN_ATTEMPT].filter(Boolean).join('-')
|
|
}
|
|
|
|
const prefix = commitSha !== 'unknown' ? commitSha.slice(0, 12) : 'local'
|
|
return `${prefix}-${new Date().toISOString().replace(/[-:.TZ]/g, '').slice(0, 14)}`
|
|
}
|
|
|
|
function releaseUrlPath(value, basePath = '/') {
|
|
if (!value || value.startsWith('#') || /^(?:data|mailto|tel|javascript):/i.test(value)) {
|
|
return ''
|
|
}
|
|
|
|
try {
|
|
const normalizedBasePath = basePath.startsWith('/') ? basePath : `/${basePath}`
|
|
const url = new URL(value, `https://release.local${normalizedBasePath}`)
|
|
if (url.origin !== 'https://release.local') {
|
|
return ''
|
|
}
|
|
|
|
return releaseManifestPath(url.pathname)
|
|
} catch {
|
|
return ''
|
|
}
|
|
}
|
|
|
|
function releaseManifestDistPath(outputDirectory, urlPath) {
|
|
try {
|
|
const normalizedPath = decodeURIComponent(
|
|
releaseManifestPath(new URL(urlPath, 'https://release.local').pathname)
|
|
).replace(/^\/+/, '')
|
|
const resolvedPath = path.resolve(outputDirectory, normalizedPath)
|
|
const resolvedOutputDirectory = path.resolve(outputDirectory)
|
|
if (
|
|
resolvedPath !== resolvedOutputDirectory &&
|
|
!resolvedPath.startsWith(`${resolvedOutputDirectory}${path.sep}`)
|
|
) {
|
|
return ''
|
|
}
|
|
|
|
return resolvedPath
|
|
} catch {
|
|
return ''
|
|
}
|
|
}
|
|
|
|
function releaseManifestHash(filePath) {
|
|
const contents = fs.readFileSync(filePath)
|
|
return {
|
|
sha256: crypto.createHash('sha256').update(contents).digest('hex'),
|
|
bytes: contents.length
|
|
}
|
|
}
|
|
|
|
function releaseDistAssetUrls(outputDirectory) {
|
|
const assetUrls = []
|
|
const walk = (directory) => {
|
|
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
|
|
const absolutePath = path.join(directory, entry.name)
|
|
const relativePath = path.relative(outputDirectory, absolutePath).replace(/\\/g, '/')
|
|
|
|
if (entry.isDirectory()) {
|
|
walk(absolutePath)
|
|
continue
|
|
}
|
|
if (!entry.isFile()) {
|
|
continue
|
|
}
|
|
if (relativePath === 'release-manifest.json' || relativePath === '.htaccess') {
|
|
continue
|
|
}
|
|
|
|
assetUrls.push(relativePath)
|
|
}
|
|
}
|
|
|
|
walk(outputDirectory)
|
|
return assetUrls
|
|
}
|
|
|
|
function patchBuefyCssMediaQuery() {
|
|
return {
|
|
name: 'patch-buefy-css-media-query',
|
|
enforce: 'pre',
|
|
transform(code, id) {
|
|
if (!id.includes('buefy/dist/css/buefy.css')) {
|
|
return null
|
|
}
|
|
|
|
let nextCode = code
|
|
let didPatch = false
|
|
|
|
for (const [invalidSnippet, fixedSnippet] of BUEFY_CSS_REPLACEMENTS.entries()) {
|
|
if (!nextCode.includes(invalidSnippet)) {
|
|
continue
|
|
}
|
|
|
|
nextCode = nextCode.replaceAll(invalidSnippet, fixedSnippet)
|
|
didPatch = true
|
|
}
|
|
|
|
if (!didPatch) {
|
|
return null
|
|
}
|
|
|
|
return nextCode
|
|
}
|
|
}
|
|
}
|
|
|
|
function releaseEntryManifest() {
|
|
return {
|
|
name: 'release-entry-manifest',
|
|
writeBundle(options, bundle) {
|
|
const chunks = Object.values(bundle).filter((item) => item.type === 'chunk')
|
|
const isMainModuleId = (moduleId) =>
|
|
String(moduleId || '').replace(/\\/g, '/').endsWith('/src/main.js')
|
|
const mainChunk = chunks.find((chunk) =>
|
|
isMainModuleId(chunk.facadeModuleId) ||
|
|
Array.from(chunk.moduleIds || []).some(isMainModuleId)
|
|
) || chunks.find((chunk) => chunk.isEntry)
|
|
if (!mainChunk) {
|
|
this.warn('Could not find src/main.js chunk for release-entry.json')
|
|
return
|
|
}
|
|
|
|
const css = Array.from(mainChunk.viteMetadata?.importedCss || [])
|
|
const configuredOutputDirectory = options.dir || 'dist'
|
|
const outputDirectory = path.isAbsolute(configuredOutputDirectory)
|
|
? configuredOutputDirectory
|
|
: fromProjectRoot(configuredOutputDirectory)
|
|
fs.writeFileSync(
|
|
path.join(outputDirectory, 'release-entry.json'),
|
|
JSON.stringify(
|
|
{
|
|
entry: mainChunk.fileName,
|
|
css
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
function publicAssetAliases() {
|
|
return {
|
|
name: 'public-asset-aliases',
|
|
generateBundle() {
|
|
for (const [source, fileName] of PUBLIC_ASSET_ALIASES) {
|
|
const sourcePath = fromProjectRoot('public', source)
|
|
if (!fs.existsSync(sourcePath)) {
|
|
this.warn(`Could not find public asset alias source: ${source}`)
|
|
continue
|
|
}
|
|
|
|
this.emitFile({
|
|
type: 'asset',
|
|
fileName,
|
|
source: fs.readFileSync(sourcePath)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function rootPwaManifestAlias() {
|
|
return {
|
|
name: 'root-pwa-manifest-alias',
|
|
writeBundle(options) {
|
|
const configuredOutputDirectory = options.dir || 'dist'
|
|
const outputDirectory = path.isAbsolute(configuredOutputDirectory)
|
|
? configuredOutputDirectory
|
|
: fromProjectRoot(configuredOutputDirectory)
|
|
const sourcePath = path.join(outputDirectory, 'assets', 'manifest.webmanifest')
|
|
const targetPath = path.join(outputDirectory, 'manifest.webmanifest')
|
|
|
|
if (!fs.existsSync(sourcePath)) {
|
|
this.warn('Could not find assets/manifest.webmanifest for root manifest alias')
|
|
return
|
|
}
|
|
|
|
const manifest = JSON.parse(fs.readFileSync(sourcePath, 'utf8'))
|
|
manifest.icons = (manifest.icons || []).map((icon) => {
|
|
if (
|
|
!icon.src ||
|
|
icon.src.startsWith('/') ||
|
|
icon.src.startsWith('assets/') ||
|
|
/^[a-z][a-z0-9+.-]*:/i.test(icon.src)
|
|
) {
|
|
return icon
|
|
}
|
|
|
|
return {
|
|
...icon,
|
|
src: `assets/${icon.src}`
|
|
}
|
|
})
|
|
fs.writeFileSync(targetPath, JSON.stringify(manifest))
|
|
}
|
|
}
|
|
}
|
|
|
|
function releaseMetadataManifest() {
|
|
let outputDirectory = fromProjectRoot('dist')
|
|
|
|
return {
|
|
name: 'release-metadata-manifest',
|
|
enforce: 'post',
|
|
configResolved(config) {
|
|
outputDirectory = path.isAbsolute(config.build.outDir)
|
|
? config.build.outDir
|
|
: path.resolve(config.root, config.build.outDir)
|
|
},
|
|
writeBundle(options) {
|
|
const configuredOutputDirectory = options.dir || outputDirectory
|
|
outputDirectory = path.isAbsolute(configuredOutputDirectory)
|
|
? configuredOutputDirectory
|
|
: fromProjectRoot(configuredOutputDirectory)
|
|
const releaseEntryPath = path.join(outputDirectory, 'release-entry.json')
|
|
const indexPath = path.join(outputDirectory, 'index.html')
|
|
|
|
if (!fs.existsSync(releaseEntryPath)) {
|
|
this.error('Could not find release-entry.json for release-manifest.json')
|
|
return
|
|
}
|
|
if (!fs.existsSync(indexPath)) {
|
|
this.error('Could not find index.html for release-manifest.json')
|
|
return
|
|
}
|
|
|
|
const releaseEntry = JSON.parse(fs.readFileSync(releaseEntryPath, 'utf8'))
|
|
const indexHtml = fs.readFileSync(indexPath, 'utf8')
|
|
const rootFiles = fs.readdirSync(outputDirectory)
|
|
const emittedAssetUrls = releaseDistAssetUrls(outputDirectory)
|
|
const indexAssetUrls = Array.from(indexHtml.matchAll(/\b(?:href|src)=["']([^"']+)["']/g))
|
|
.map((match) => releaseUrlPath(match[1], '/index.html'))
|
|
.map(releaseManifestAssetPath)
|
|
.filter(Boolean)
|
|
const releaseEntryAssetUrls = [
|
|
releaseEntry.entry ? releaseManifestAssetPath(releaseEntry.entry) : '',
|
|
...(Array.isArray(releaseEntry.css) ? releaseEntry.css.map(releaseManifestAssetPath) : [])
|
|
].filter(Boolean)
|
|
const pwaAssetUrls = [
|
|
'manifest.json',
|
|
'manifest.webmanifest',
|
|
'assets/manifest.webmanifest',
|
|
'favicon.ico',
|
|
'favicon_default.ico',
|
|
'pleno-favicon.ico',
|
|
'registerSW.js',
|
|
'sw.js',
|
|
...rootFiles.filter((fileName) => /^workbox-[^/]+\.js$/.test(fileName))
|
|
]
|
|
.filter((fileName) => fs.existsSync(path.join(outputDirectory, fileName)))
|
|
.map((fileName) => releaseManifestAssetPath(fileName.replace(/\\/g, '/')))
|
|
|
|
for (const manifestPath of ['manifest.webmanifest', 'assets/manifest.webmanifest']) {
|
|
const absoluteManifestPath = path.join(outputDirectory, manifestPath)
|
|
if (!fs.existsSync(absoluteManifestPath)) {
|
|
continue
|
|
}
|
|
|
|
try {
|
|
const pwaManifest = JSON.parse(fs.readFileSync(absoluteManifestPath, 'utf8'))
|
|
for (const icon of pwaManifest.icons || []) {
|
|
const iconPath = releaseUrlPath(icon.src || '', `/${manifestPath}`)
|
|
if (iconPath) {
|
|
pwaAssetUrls.push(releaseManifestAssetPath(iconPath))
|
|
}
|
|
}
|
|
} catch {
|
|
this.warn(`Could not parse ${manifestPath} while generating release-manifest.json`)
|
|
}
|
|
}
|
|
|
|
const criticalAssetUrls = Array.from(new Set([
|
|
'index.html',
|
|
'release-entry.json',
|
|
...emittedAssetUrls,
|
|
...indexAssetUrls,
|
|
...releaseEntryAssetUrls,
|
|
...pwaAssetUrls
|
|
]))
|
|
const missingAssets = []
|
|
const assetHashes = {}
|
|
|
|
for (const assetUrl of criticalAssetUrls) {
|
|
const assetPath = releaseManifestDistPath(outputDirectory, assetUrl)
|
|
if (!assetPath || !fs.existsSync(assetPath) || !fs.statSync(assetPath).isFile()) {
|
|
missingAssets.push(assetUrl)
|
|
continue
|
|
}
|
|
|
|
assetHashes[assetUrl] = releaseManifestHash(assetPath)
|
|
}
|
|
|
|
if (missingAssets.length > 0) {
|
|
this.error(`Critical release assets are missing from dist: ${missingAssets.join(', ')}`)
|
|
return
|
|
}
|
|
|
|
const commitSha = getGitCommitSha()
|
|
fs.writeFileSync(
|
|
path.join(outputDirectory, 'release-manifest.json'),
|
|
JSON.stringify(
|
|
{
|
|
schema_version: 1,
|
|
build_id: releaseBuildId(commitSha),
|
|
commit_sha: commitSha,
|
|
created_at: new Date().toISOString(),
|
|
entry: releaseEntry.entry,
|
|
css: Array.isArray(releaseEntry.css) ? releaseEntry.css : [],
|
|
index_asset_urls: Array.from(new Set(indexAssetUrls)),
|
|
pwa_asset_urls: Array.from(new Set(pwaAssetUrls)),
|
|
asset_urls: criticalAssetUrls,
|
|
asset_hashes: assetHashes
|
|
},
|
|
null,
|
|
2
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
const DEFAULT_API_PROXY_TARGET = 'https://api-v2.truckwash.io'
|
|
const DEFAULT_API_PROXY_BASE_PATH = '/master/api'
|
|
const PLAYWRIGHT_OPTIMIZED_DEPS = [
|
|
'@azure/msal-browser',
|
|
'@popperjs/core',
|
|
'@vueuse/core',
|
|
'@vueuse/head',
|
|
'apexcharts',
|
|
'axios',
|
|
'buefy',
|
|
'chart.js',
|
|
'sweetalert2',
|
|
'vue',
|
|
'vue-i18n',
|
|
'vue-router',
|
|
'vue-toast-notification/src/index.js',
|
|
'vue3-apexcharts',
|
|
'vuex'
|
|
]
|
|
|
|
function normalizeProxyBasePath(value) {
|
|
const normalized = String(value || '').trim().replace(/^\/+|\/+$/g, '')
|
|
return normalized ? `/${normalized}` : ''
|
|
}
|
|
|
|
function parseProxySecure(value) {
|
|
return String(value || '').trim().toLowerCase() !== 'false'
|
|
}
|
|
|
|
function rewriteApiProxyPath(requestPath, basePath = '') {
|
|
const pathWithoutApiPrefix = String(requestPath || '/').replace(/^\/api(?=\/|\?|$)/, '') || '/'
|
|
if (!basePath) {
|
|
return pathWithoutApiPrefix.startsWith('?') ? `/${pathWithoutApiPrefix}` : pathWithoutApiPrefix
|
|
}
|
|
if (pathWithoutApiPrefix === '/') {
|
|
return basePath
|
|
}
|
|
return `${basePath}${pathWithoutApiPrefix}`
|
|
}
|
|
|
|
export function createApiProxyOptions(env = process.env) {
|
|
const stripPrefix = env.VITE_API_PROXY_STRIP_PREFIX !== 'false'
|
|
const configuredTarget = String(env.VITE_API_PROXY_TARGET || '').trim()
|
|
const target = configuredTarget || DEFAULT_API_PROXY_TARGET
|
|
const secure = parseProxySecure(env.VITE_API_PROXY_SECURE)
|
|
const basePath = env.VITE_API_PROXY_BASE_PATH !== undefined
|
|
? normalizeProxyBasePath(env.VITE_API_PROXY_BASE_PATH)
|
|
: configuredTarget
|
|
? ''
|
|
: DEFAULT_API_PROXY_BASE_PATH
|
|
|
|
return {
|
|
target,
|
|
changeOrigin: true,
|
|
secure,
|
|
...(stripPrefix
|
|
? {
|
|
rewrite: (requestPath) => rewriteApiProxyPath(requestPath, basePath)
|
|
}
|
|
: {})
|
|
}
|
|
}
|
|
|
|
export default defineConfig(({ mode }) => {
|
|
const isProd = mode === 'production'
|
|
const isPlaywrightRuntime = process.env.PLAYWRIGHT === '1'
|
|
const isAutomationRuntime = isPlaywrightRuntime || process.env.CI === 'true' || process.env.GITHUB_ACTIONS === 'true'
|
|
|
|
// Set COMMIT_HASH env var for use in the app
|
|
const version = process.env.npm_package_version || '0.0.0'
|
|
const commit = getGitCommit()
|
|
console.log(`Building version ${version} (commit: ${commit})`)
|
|
process.env.COMMIT_HASH = commit
|
|
process.env.APP_VERSION = version
|
|
process.env.IS_DEV = !isProd ? 'true' : 'false'
|
|
// Server version updates happen after deploy verification, not during asset builds.
|
|
// Keep production assets host-agnostic. Release channel prefixes are resolved at runtime.
|
|
const base = isProd ? releaseFrontendBasePath() : '/'
|
|
const pwaScope = base === './' ? '/' : base
|
|
|
|
// Enable single-file build only when explicitly requested (disabled by default for PWA)
|
|
const enableSingleFile = process.env.VITE_SINGLE_FILE === 'true' && !isProd
|
|
|
|
return {
|
|
base,
|
|
plugins: [
|
|
patchBuefyCssMediaQuery(),
|
|
vue(),
|
|
VueJsx(),
|
|
releaseEntryManifest(),
|
|
publicAssetAliases(),
|
|
!isProd && !isAutomationRuntime && vueDevTools(),
|
|
enableSingleFile && viteSingleFile(),
|
|
VitePWA({
|
|
registerType: 'autoUpdate',
|
|
injectRegister: isProd ? 'auto' : false,
|
|
manifestFilename: 'assets/manifest.webmanifest',
|
|
strategies: 'generateSW',
|
|
devOptions: {
|
|
enabled: false // keep for local dev
|
|
},
|
|
workbox: {
|
|
maximumFileSizeToCacheInBytes: 12 * 1024 * 1024,
|
|
cleanupOutdatedCaches: true,
|
|
skipWaiting: true,
|
|
clientsClaim: true,
|
|
globPatterns: ['**/*.{js,css,html,ico,svg,png,woff2}'],
|
|
globIgnores: ['**/registerSW.js', '**/sw.js', '**/*.webmanifest'],
|
|
runtimeCaching: [
|
|
{
|
|
// cache API responses
|
|
urlPattern: ({ url }) => url.origin === 'https://api-v2.truckwash.io',
|
|
handler: 'NetworkFirst',
|
|
options: {
|
|
cacheName: 'pleno-api-cache',
|
|
expiration: {
|
|
maxEntries: 100,
|
|
maxAgeSeconds: 5 * 60 // 5 minutes
|
|
},
|
|
networkTimeoutSeconds: 10,
|
|
cacheableResponse: {
|
|
statuses: [200] // avoid opaque caching for API
|
|
}
|
|
}
|
|
},
|
|
{
|
|
// cache same-origin static assets
|
|
urlPattern: ({ sameOrigin, request }) =>
|
|
sameOrigin && ['style', 'script', 'font'].includes(request.destination),
|
|
handler: 'StaleWhileRevalidate',
|
|
options: {
|
|
cacheName: 'pleno-website-cache',
|
|
expiration: {
|
|
maxEntries: 50,
|
|
maxAgeSeconds: 24 * 60 * 60 // 24 hours
|
|
},
|
|
cacheableResponse: {
|
|
statuses: [0, 200]
|
|
}
|
|
}
|
|
},
|
|
{
|
|
// cache images (restrict to same-origin; loosen if you use a trusted CDN)
|
|
urlPattern: ({ sameOrigin, request }) =>
|
|
sameOrigin && request.destination === 'image',
|
|
handler: 'CacheFirst',
|
|
options: {
|
|
cacheName: 'pleno-image-cache',
|
|
expiration: {
|
|
maxEntries: 100,
|
|
maxAgeSeconds: 7 * 24 * 60 * 60 // 7 days
|
|
},
|
|
cacheableResponse: {
|
|
statuses: [0, 200]
|
|
}
|
|
}
|
|
}
|
|
]
|
|
},
|
|
manifest: {
|
|
name: 'Truck Wash Kundeportal',
|
|
short_name: 'Truck Wash',
|
|
description: 'Access your Truck Wash accounts and transactions from anywhere.',
|
|
theme_color: '#063651',
|
|
background_color: '#0787bb',
|
|
start_url: pwaScope,
|
|
scope: pwaScope,
|
|
display: 'standalone',
|
|
orientation: 'portrait',
|
|
launch_handler: { client_mode: 'navigate-existing' },
|
|
icons: [
|
|
{ src: 'favicons/web-app-manifest-192x192.png', sizes: '192x192', type: 'image/png' },
|
|
{ src: 'favicons/web-app-manifest-512x512.png', sizes: '512x512', type: 'image/png' },
|
|
// Optional: add a maskable icon
|
|
// { src: 'icons/icon-512x512-maskable.png', sizes: '512x512', type: 'image/png', purpose: 'any maskable' }
|
|
],
|
|
id: '/'
|
|
},
|
|
useCredentials: true
|
|
}),
|
|
rootPwaManifestAlias(),
|
|
releaseMetadataManifest()
|
|
].filter(Boolean),
|
|
resolve: {
|
|
alias: {
|
|
'@': fromProjectRoot('src'),
|
|
'vue-router': fromProjectRoot('node_modules', 'vue-router', 'dist', 'vue-router.mjs'),
|
|
'vue-i18n': fromProjectRoot('node_modules', 'vue-i18n', 'dist', 'vue-i18n.mjs')
|
|
},
|
|
preserveSymlinks: true,
|
|
dedupe: ['vue', 'vue-router', 'vue-i18n', '@vueuse/core', '@vueuse/head', '@unhead/vue']
|
|
},
|
|
define: {
|
|
'import.meta.env.VITE_BUILD_DATE': JSON.stringify(new Date().toISOString()),
|
|
'import.meta.env.VITE_APP_VERSION': JSON.stringify(process.env.APP_VERSION || '0.0.0'),
|
|
'import.meta.env.VITE_COMMIT_HASH': JSON.stringify(commit),
|
|
'import.meta.env.VITE_IS_PLAYWRIGHT': JSON.stringify(isPlaywrightRuntime),
|
|
// Tip: import.meta.env.DEV/PROD are available at runtime
|
|
'import.meta.env.VITE_IS_DEV': JSON.stringify(!isProd),
|
|
},
|
|
css: {
|
|
preprocessorOptions: {
|
|
scss: {
|
|
api: 'modern-compiler',
|
|
quietDeps: true,
|
|
silenceDeprecations: ['import', 'global-builtin', 'legacy-js-api', 'if-function']
|
|
}
|
|
}
|
|
},
|
|
test: {
|
|
setupFiles: [fromProjectRoot('tests', 'unit', 'setup.js')],
|
|
isolate: true
|
|
},
|
|
server: {
|
|
proxy: {
|
|
'/api': createApiProxyOptions()
|
|
},
|
|
watch: {
|
|
...(isPlaywrightRuntime ? { usePolling: true } : {}),
|
|
ignored: ['**/output/playwright/**', '**/node_modules.codex-backup/**']
|
|
}
|
|
},
|
|
optimizeDeps: isPlaywrightRuntime
|
|
? {
|
|
entries: ['index.html', 'src/**/*.{vue,js,ts,jsx,tsx}'],
|
|
include: PLAYWRIGHT_OPTIMIZED_DEPS
|
|
}
|
|
: {
|
|
entries: ['index.html', 'src/**/*.{vue,js,ts,jsx,tsx}']
|
|
}
|
|
|
|
}
|
|
})
|