684 lines
25 KiB
JavaScript
684 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',
|
|
generateBundle(_, bundle) {
|
|
const chunks = Object.values(bundle).filter((item) => item.type === 'chunk')
|
|
const mainChunk = chunks.find((chunk) =>
|
|
String(chunk.facadeModuleId || '').replace(/\\/g, '/').endsWith('/src/main.js')
|
|
) || chunks.find((chunk) => chunk.name === 'main' && /^assets\/main-[\w-]+\.js$/.test(chunk.fileName))
|
|
if (!mainChunk) {
|
|
this.warn('Could not find src/main.js chunk for release-entry.json')
|
|
return
|
|
}
|
|
|
|
const css = Array.from(mainChunk.viteMetadata?.importedCss || [])
|
|
this.emitFile({
|
|
type: 'asset',
|
|
fileName: 'release-entry.json',
|
|
source: 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)
|
|
},
|
|
closeBundle() {
|
|
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'
|
|
|
|
// 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 && !isPlaywrightRuntime && 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',
|
|
// Use relative scope when base is relative
|
|
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: {
|
|
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}']
|
|
}
|
|
|
|
}
|
|
})
|