feat: add BASE_PATH support for deployment and update environment variables (#165)
This commit is contained in:
parent
30ede17f80
commit
2fe35fafcc
13
.env.example
13
.env.example
@ -1,2 +1,13 @@
|
||||
# Environment variables for MiniQR
|
||||
|
||||
# Crowdin configuration for translations
|
||||
CROWDIN_PERSONAL_TOKEN="your_personal_token"
|
||||
CROWDIN_PROJECT_ID="your_project_id"
|
||||
CROWDIN_PROJECT_ID="your_project_id"
|
||||
|
||||
# Base path for deployment (e.g., /mini-qr for deploying at domain.com/mini-qr)
|
||||
# Default: /
|
||||
BASE_PATH=/
|
||||
|
||||
# Hide credits in the footer
|
||||
# Default: false
|
||||
VITE_HIDE_CREDITS=false
|
18
Dockerfile
18
Dockerfile
@ -3,16 +3,22 @@
|
||||
# Build stage
|
||||
FROM node:lts-alpine AS builder
|
||||
WORKDIR /app
|
||||
|
||||
# Accept BASE_PATH as build argument
|
||||
ARG BASE_PATH=/
|
||||
ENV BASE_PATH=${BASE_PATH}
|
||||
|
||||
COPY package*.json ./
|
||||
RUN npm install --frozen-lockfile
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
|
||||
# Production stage
|
||||
FROM nginx:alpine AS production
|
||||
LABEL org.opencontainers.image.source https://github.com/lyqht/mini-qr
|
||||
COPY --from=builder /app/dist /usr/share/nginx/html
|
||||
COPY --from=builder /app/public /usr/share/nginx/html/public
|
||||
COPY --from=builder /app/nginx.conf /etc/nginx/nginx.conf
|
||||
FROM node:lts-alpine AS production
|
||||
WORKDIR /app
|
||||
COPY --from=builder /app/dist ./dist
|
||||
COPY --from=builder /app/public ./public
|
||||
COPY --from=builder /app/package.json ./
|
||||
RUN npm install -g serve
|
||||
EXPOSE 8080
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
CMD ["serve", "-s", "dist", "-l", "8080"]
|
26
README.md
26
README.md
@ -142,6 +142,32 @@ docker run -d -p 8081:8080 mini-qr
|
||||
- The production image uses Nginx for optimal static file serving.
|
||||
- The `.dockerignore` file is included for smaller, faster builds.
|
||||
- Set `HIDE_CREDITS=1` to remove the maintainer credit from the footer.
|
||||
- Set `BASE_PATH=/your-path` to deploy the app under a subdirectory (e.g., for hosting at `domain.com/your-path`).
|
||||
|
||||
#### Environment Variables
|
||||
|
||||
| Variable | Description | Default |
|
||||
|----------|-------------|---------|
|
||||
| `HIDE_CREDITS` | Hide credits in the footer | `false` |
|
||||
| `BASE_PATH` | Base path for deployment | `/` |
|
||||
|
||||
#### Examples
|
||||
|
||||
Deploy at root path (default):
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Deploy at subdirectory `/mini-qr`:
|
||||
```bash
|
||||
BASE_PATH=/mini-qr docker compose up -d
|
||||
```
|
||||
|
||||
For custom builds with specific BASE_PATH:
|
||||
```bash
|
||||
docker build --build-arg BASE_PATH=/mini-qr -t mini-qr .
|
||||
docker run -d -p 8081:8080 mini-qr
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
|
@ -1,17 +1,37 @@
|
||||
---
|
||||
services:
|
||||
mini-qr:
|
||||
image: ghcr.io/lyqht/mini-qr:latest
|
||||
# image: ghcr.io/lyqht/mini-qr:latest
|
||||
container_name: mini-qr
|
||||
ports:
|
||||
- 8081:8080
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- VITE_HIDE_CREDITS=${HIDE_CREDITS:-false}
|
||||
- BASE_PATH=${BASE_PATH:-/}
|
||||
# Uncomment the following lines to build locally instead of pulling from ghcr.io
|
||||
# build:
|
||||
# context: .
|
||||
# dockerfile: Dockerfile
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
args:
|
||||
- BASE_PATH=${BASE_PATH:-/}
|
||||
# volumes:
|
||||
# - ./public:/usr/share/nginx/html/public:ro
|
||||
# - ./nginx.conf:/etc/nginx/nginx.conf:ro
|
||||
networks:
|
||||
- mini-qr-network
|
||||
|
||||
nginx-proxy:
|
||||
image: nginx:alpine
|
||||
container_name: nginx-proxy
|
||||
ports:
|
||||
- 80:80
|
||||
restart: unless-stopped
|
||||
volumes:
|
||||
- ./nginx-proxy.conf:/etc/nginx/nginx.conf:ro
|
||||
depends_on:
|
||||
- mini-qr
|
||||
networks:
|
||||
- mini-qr-network
|
||||
|
||||
networks:
|
||||
mini-qr-network:
|
||||
driver: bridge
|
||||
|
1
env.d.ts
vendored
1
env.d.ts
vendored
@ -2,6 +2,7 @@
|
||||
|
||||
interface ImportMetaEnv {
|
||||
readonly VITE_HIDE_CREDITS?: string
|
||||
readonly BASE_PATH?: string
|
||||
}
|
||||
|
||||
interface ImportMeta {
|
||||
|
68
nginx-proxy.conf
Normal file
68
nginx-proxy.conf
Normal file
@ -0,0 +1,68 @@
|
||||
events {
|
||||
worker_connections 1024;
|
||||
}
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
sendfile on;
|
||||
keepalive_timeout 65;
|
||||
|
||||
# Upstream for mini-qr service
|
||||
upstream mini-qr-backend {
|
||||
server mini-qr:8080;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
# Root path - redirect to mini-qr
|
||||
location = / {
|
||||
return 301 /mini-qr/;
|
||||
}
|
||||
|
||||
# Mini QR application with subpath
|
||||
location /mini-qr/ {
|
||||
# Remove /mini-qr prefix before forwarding to backend
|
||||
rewrite ^/mini-qr/(.*) /$1 break;
|
||||
|
||||
proxy_pass http://mini-qr-backend;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header X-Forwarded-Host $host;
|
||||
proxy_set_header X-Forwarded-Port $server_port;
|
||||
|
||||
# Handle assets specifically
|
||||
location ~* ^/mini-qr/assets/ {
|
||||
rewrite ^/mini-qr/(.*) /$1 break;
|
||||
proxy_pass http://mini-qr-backend;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# Handle static files
|
||||
location ~* ^/mini-qr/.*\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
rewrite ^/mini-qr/(.*) /$1 break;
|
||||
proxy_pass http://mini-qr-backend;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
}
|
||||
|
||||
# Health check endpoint
|
||||
location /health {
|
||||
access_log off;
|
||||
return 200 "healthy\n";
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
|
||||
# Error pages
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
}
|
||||
}
|
||||
}
|
31
nginx.conf
31
nginx.conf
@ -10,20 +10,45 @@ pid /var/run/nginx.pid;
|
||||
|
||||
http {
|
||||
include /etc/nginx/mime.types;
|
||||
default_type application/octet-stream;
|
||||
sendfile on;
|
||||
default_type application/octet-stream; sendfile on;
|
||||
keepalive_timeout 65;
|
||||
server {
|
||||
server {
|
||||
listen 8080;
|
||||
server_name localhost;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
# Handle JavaScript modules with correct MIME type for any base path
|
||||
location ~* ^/.*/assets/.*\.(js|mjs)$ {
|
||||
add_header Content-Type application/javascript;
|
||||
try_files $uri =404;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# Handle CSS files with correct MIME type for any base path
|
||||
location ~* ^/.*/assets/.*\.css$ {
|
||||
add_header Content-Type text/css;
|
||||
try_files $uri =404;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
# Handle other static assets
|
||||
location ~* \.(png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
try_files $uri =404;
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /public/ {
|
||||
alias /usr/share/nginx/html/public/;
|
||||
}
|
||||
|
||||
error_page 500 502 503 504 /50x.html;
|
||||
location = /50x.html {
|
||||
root /usr/share/nginx/html;
|
||||
|
@ -2,6 +2,7 @@
|
||||
import { marked } from 'marked'
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { fetchWithBasePath } from '@/utils/basePath'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@ -17,15 +18,13 @@ const { t } = useI18n()
|
||||
const version = ref('...')
|
||||
const changelogContent = ref<string | null>(null)
|
||||
const isLoading = ref(true)
|
||||
const hideCredits = ['1', 'true'].includes(
|
||||
(import.meta.env.VITE_HIDE_CREDITS ?? '').toLowerCase()
|
||||
)
|
||||
const hideCredits = ['1', 'true'].includes((import.meta.env.VITE_HIDE_CREDITS ?? '').toLowerCase())
|
||||
|
||||
async function fetchAndProcessChangelog() {
|
||||
if (changelogContent.value === null) {
|
||||
isLoading.value = true
|
||||
try {
|
||||
const response = await fetch('/CHANGELOG.md')
|
||||
const response = await fetchWithBasePath('/CHANGELOG.md')
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
@ -69,33 +68,33 @@ onMounted(() => {
|
||||
>
|
||||
<span>|</span>
|
||||
<Dialog>
|
||||
<DialogTrigger as-child>
|
||||
<button class="secondary-button" :aria-label="t('View changelog')" :disabled="isLoading">
|
||||
{{ isLoading ? '...' : version }}
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
<DialogContent class="flex max-h-[80vh] flex-col sm:max-w-md" @open-auto-focus.prevent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t('Changelog') }}</DialogTitle>
|
||||
<DialogClose
|
||||
class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
|
||||
>
|
||||
<X class="size-4" />
|
||||
<span class="sr-only">{{ t('Close') }}</span>
|
||||
</DialogClose>
|
||||
</DialogHeader>
|
||||
<DialogTrigger as-child>
|
||||
<button class="secondary-button" :aria-label="t('View changelog')" :disabled="isLoading">
|
||||
{{ isLoading ? '...' : version }}
|
||||
</button>
|
||||
</DialogTrigger>
|
||||
<DialogContent class="flex max-h-[80vh] flex-col sm:max-w-md" @open-auto-focus.prevent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{{ t('Changelog') }}</DialogTitle>
|
||||
<DialogClose
|
||||
class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
|
||||
>
|
||||
<X class="size-4" />
|
||||
<span class="sr-only">{{ t('Close') }}</span>
|
||||
</DialogClose>
|
||||
</DialogHeader>
|
||||
|
||||
<div class="flex-1 overflow-y-auto pr-2">
|
||||
<DialogDescription
|
||||
as="div"
|
||||
class="prose prose-sm max-w-none text-start dark:prose-invert prose-li:my-1"
|
||||
>
|
||||
<div v-if="isLoading">Loading...</div>
|
||||
<div v-else-if="changelogContent" v-html="changelogContent"></div>
|
||||
<div v-else>{{ t('Failed to load changelog') }}</div>
|
||||
</DialogDescription>
|
||||
</div>
|
||||
</DialogContent>
|
||||
<div class="flex-1 overflow-y-auto pr-2">
|
||||
<DialogDescription
|
||||
as="div"
|
||||
class="prose prose-sm max-w-none text-start dark:prose-invert prose-li:my-1"
|
||||
>
|
||||
<div v-if="isLoading">Loading...</div>
|
||||
<div v-else-if="changelogContent" v-html="changelogContent"></div>
|
||||
<div v-else>{{ t('Failed to load changelog') }}</div>
|
||||
</DialogDescription>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</footer>
|
||||
|
@ -4,6 +4,7 @@ import { useFloating, offset, flip, shift, autoUpdate } from '@floating-ui/vue'
|
||||
import LanguageSelector from '@/components/LanguageSelector.vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { marked } from 'marked'
|
||||
import { fetchWithBasePath } from '@/utils/basePath'
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
@ -33,15 +34,13 @@ const floating = ref<HTMLElement | null>(null)
|
||||
const version = ref('...')
|
||||
const changelogContent = ref<string | null>(null)
|
||||
const isLoadingChangelog = ref(true)
|
||||
const hideCredits = ['1', 'true'].includes(
|
||||
(import.meta.env.VITE_HIDE_CREDITS ?? '').toLowerCase()
|
||||
)
|
||||
const hideCredits = ['1', 'true'].includes((import.meta.env.VITE_HIDE_CREDITS ?? '').toLowerCase())
|
||||
|
||||
async function fetchAndProcessChangelog() {
|
||||
if (changelogContent.value === null) {
|
||||
isLoadingChangelog.value = true
|
||||
try {
|
||||
const response = await fetch('/CHANGELOG.md')
|
||||
const response = await fetchWithBasePath('/CHANGELOG.md')
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`)
|
||||
}
|
||||
|
40
src/utils/basePath.ts
Normal file
40
src/utils/basePath.ts
Normal file
@ -0,0 +1,40 @@
|
||||
// Base path utility for handling runtime paths
|
||||
// Uses Vite's environment variable system to get BASE_PATH at build time
|
||||
|
||||
/**
|
||||
* Get the base path for the application
|
||||
* This reads from Vite's environment variables which are set during build time
|
||||
*/
|
||||
export function getBasePath(): string {
|
||||
// Vite makes BASE_PATH available at build time through import.meta.env
|
||||
// If BASE_PATH is not set, default to '/'
|
||||
return import.meta.env.BASE_PATH || '/'
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a relative path with the application base path
|
||||
* @param path - The path to resolve (should start with /)
|
||||
* @returns The resolved path with base path prepended
|
||||
*/
|
||||
export function resolvePath(path: string): string {
|
||||
const basePath = getBasePath()
|
||||
|
||||
// Remove trailing slash from base path (except for root)
|
||||
const cleanBasePath = basePath === '/' ? '' : basePath.replace(/\/$/, '')
|
||||
|
||||
// Ensure path starts with /
|
||||
const cleanPath = path.startsWith('/') ? path : `/${path}`
|
||||
|
||||
return `${cleanBasePath}${cleanPath}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a fetch function that respects the base path
|
||||
* @param path - The path to fetch (should start with /)
|
||||
* @param options - Fetch options
|
||||
* @returns Promise<Response>
|
||||
*/
|
||||
export function fetchWithBasePath(path: string, options?: RequestInit): Promise<Response> {
|
||||
const resolvedPath = resolvePath(path)
|
||||
return fetch(resolvedPath, options)
|
||||
}
|
182
vite.config.js
182
vite.config.js
@ -1,82 +1,120 @@
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import { defineConfig, loadEnv } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||
import { VitePWA } from 'vite-plugin-pwa'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueJsx(),
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
includeAssets: [
|
||||
'app_icons/web/favicon.ico',
|
||||
'app_icons/web/splash-750x1334@2x.png',
|
||||
'app_icons/web/splash-1170x2532@3x.png',
|
||||
'app_icons/web/splash-1290x2796@3x.png',
|
||||
'app_icons/web/splash-2048x2732@2x.png'
|
||||
],
|
||||
manifest: {
|
||||
name: 'MiniQR',
|
||||
short_name: 'MiniQR',
|
||||
description: 'A minimal QR code generator and scanner',
|
||||
theme_color: '#ffffff',
|
||||
background_color: '#ffffff',
|
||||
display: 'standalone',
|
||||
orientation: 'portrait',
|
||||
icons: [
|
||||
{
|
||||
src: 'app_icons/web/icon-192.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png'
|
||||
},
|
||||
{
|
||||
src: 'app_icons/web/icon-192-maskable.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png',
|
||||
purpose: 'maskable'
|
||||
},
|
||||
{
|
||||
src: 'app_icons/web/icon-512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png'
|
||||
},
|
||||
{
|
||||
src: 'app_icons/web/icon-512-maskable.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
purpose: 'maskable'
|
||||
}
|
||||
export default defineConfig(({ mode }) => {
|
||||
// Load environment variables
|
||||
const env = loadEnv(mode, '.', '')
|
||||
// Get BASE_PATH from environment variable, default to '/'
|
||||
// Ensure base path ends with slash for proper URL construction
|
||||
let base = env.BASE_PATH || '/'
|
||||
if (base !== '/' && !base.endsWith('/')) {
|
||||
base = base + '/'
|
||||
}
|
||||
|
||||
return {
|
||||
base,
|
||||
define: {
|
||||
// Make BASE_PATH available to client-side code through import.meta.env
|
||||
'import.meta.env.BASE_PATH': JSON.stringify(base)
|
||||
},
|
||||
plugins: [
|
||||
vue(),
|
||||
vueJsx(),
|
||||
VitePWA({
|
||||
registerType: 'autoUpdate',
|
||||
base: base, // Make sure PWA respects the base path
|
||||
includeAssets: [
|
||||
'app_icons/web/favicon.ico',
|
||||
'app_icons/web/splash-750x1334@2x.png',
|
||||
'app_icons/web/splash-1170x2532@3x.png',
|
||||
'app_icons/web/splash-1290x2796@3x.png',
|
||||
'app_icons/web/splash-2048x2732@2x.png'
|
||||
],
|
||||
screenshots: [
|
||||
{
|
||||
src: 'app_icons/web/screenshot-narrow.png',
|
||||
sizes: '3510x7596',
|
||||
type: 'image/png',
|
||||
form_factor: 'narrow'
|
||||
},
|
||||
{
|
||||
src: 'app_icons/web/screenshot-wide.png',
|
||||
sizes: '7596x3510',
|
||||
type: 'image/png',
|
||||
form_factor: 'wide'
|
||||
}
|
||||
]
|
||||
},
|
||||
workbox: {
|
||||
navigateFallback: 'index.html'
|
||||
},
|
||||
devOptions: {
|
||||
// enabled: true,
|
||||
type: 'module'
|
||||
manifest: {
|
||||
name: 'MiniQR',
|
||||
short_name: 'MiniQR',
|
||||
description: 'A minimal QR code generator and scanner',
|
||||
theme_color: '#ffffff',
|
||||
background_color: '#ffffff',
|
||||
display: 'standalone',
|
||||
orientation: 'portrait',
|
||||
start_url: base, // Use the base path as start URL
|
||||
icons: [
|
||||
{
|
||||
src: 'app_icons/web/icon-192.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png'
|
||||
},
|
||||
{
|
||||
src: 'app_icons/web/icon-192-maskable.png',
|
||||
sizes: '192x192',
|
||||
type: 'image/png',
|
||||
purpose: 'maskable'
|
||||
},
|
||||
{
|
||||
src: 'app_icons/web/icon-512.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png'
|
||||
},
|
||||
{
|
||||
src: 'app_icons/web/icon-512-maskable.png',
|
||||
sizes: '512x512',
|
||||
type: 'image/png',
|
||||
purpose: 'maskable'
|
||||
}
|
||||
],
|
||||
screenshots: [
|
||||
{
|
||||
src: 'app_icons/web/screenshot-narrow.png',
|
||||
sizes: '3510x7596',
|
||||
type: 'image/png',
|
||||
form_factor: 'narrow'
|
||||
},
|
||||
{
|
||||
src: 'app_icons/web/screenshot-wide.png',
|
||||
sizes: '7596x3510',
|
||||
type: 'image/png',
|
||||
form_factor: 'wide'
|
||||
}
|
||||
]
|
||||
},
|
||||
workbox: {
|
||||
globPatterns: ['**/*.{js,css,svg,png,jpg,jpeg,gif,ico,woff,woff2}'], // Removed html from patterns
|
||||
// Exclude large files from precaching and HTML files to avoid base path issues
|
||||
globIgnores: ['**/app_preview.*', '**/presets/*.svg', '**/*.html'],
|
||||
maximumFileSizeToCacheInBytes: 5 * 1024 * 1024, // 5MB limit
|
||||
// Don't precache index.html to avoid base path issues
|
||||
dontCacheBustURLsMatching: /\.\w{8}\./,
|
||||
navigateFallback: null, // Disable navigate fallback to avoid issues
|
||||
navigateFallbackDenylist: [/^\/_/, /\/[^/?]+\.[^/]+$/],
|
||||
// Remove modifyURLPrefix as it's causing conflicts with the base path
|
||||
runtimeCaching: [
|
||||
{
|
||||
urlPattern: ({ request }) => request.destination === 'document',
|
||||
handler: 'NetworkFirst',
|
||||
options: {
|
||||
cacheName: 'pages',
|
||||
expiration: {
|
||||
maxEntries: 10,
|
||||
maxAgeSeconds: 86400 // 1 day
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
devOptions: {
|
||||
// enabled: true,
|
||||
type: 'module'
|
||||
}
|
||||
})
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
})
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url))
|
||||
}
|
||||
}
|
||||
})
|
||||
|
Loading…
x
Reference in New Issue
Block a user