From 72c7d6c9e075e00b1a1b472d184133d9a34f2d54 Mon Sep 17 00:00:00 2001 From: Jose Henrique Date: Fri, 10 Jul 2026 17:06:16 -0300 Subject: [PATCH] new pipeline and build improvements --- .dockerignore | 4 +- .gitea/workflows/deploy.yaml | 22 + .gitea/workflows/main.yaml | 4 +- .gitignore | 2 + 404.html | 7 +- Dockerfile | 15 +- css/main.css | 5 - index.html | 8 +- js/matrix.js | 310 +++++--- js/social.js | 26 +- nginx.conf | 79 +- package-lock.json | 1415 ++++++++++++++++++++++++++++++++++ package.json | 12 + utils/build-production.mjs | 250 ++++++ 14 files changed, 1969 insertions(+), 190 deletions(-) create mode 100644 .gitea/workflows/deploy.yaml create mode 100644 .gitignore create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 utils/build-production.mjs diff --git a/.dockerignore b/.dockerignore index 9f5f879..9397565 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,5 @@ .git .gitea -utils/xor-enc.py \ No newline at end of file +dist +node_modules +utils/xor-enc.py diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml new file mode 100644 index 0000000..7a982f3 --- /dev/null +++ b/.gitea/workflows/deploy.yaml @@ -0,0 +1,22 @@ +name: Homepage Deploy (Production) + +on: + workflow_dispatch: {} + +jobs: + deploy: + name: Deploy Homepage + runs-on: ubuntu-amd64 + needs: build + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Recreate Container + uses: https://git.ivanch.me/ivanch/pipeline-actions/ssh-deploy@main + with: + ssh_host: ${{ secrets.HOST }} + ssh_username: ${{ secrets.USERNAME }} + ssh_key: ${{ secrets.KEY }} + ssh_port: ${{ secrets.PORT }} + remote_dir: ${{ secrets.DIR }} diff --git a/.gitea/workflows/main.yaml b/.gitea/workflows/main.yaml index d6307e3..ecb8f1c 100644 --- a/.gitea/workflows/main.yaml +++ b/.gitea/workflows/main.yaml @@ -1,4 +1,4 @@ -name: Homepage Build and Deploy +name: Homepage Build and Deploy (Test) on: push: @@ -41,4 +41,4 @@ jobs: ssh_username: ${{ secrets.USERNAME }} ssh_key: ${{ secrets.KEY }} ssh_port: ${{ secrets.PORT }} - remote_dir: ${{ secrets.DIR }} + remote_dir: ${{ secrets.TEST_DIR }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1eae0cf --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +dist/ +node_modules/ diff --git a/404.html b/404.html index 6806c49..299dca1 100644 --- a/404.html +++ b/404.html @@ -15,6 +15,11 @@ 404 | ivanch + + + + + @@ -48,4 +53,4 @@ - \ No newline at end of file + diff --git a/Dockerfile b/Dockerfile index 9dc0ec3..5b0f4f8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,19 +1,18 @@ -FROM node:alpine AS build +FROM node:22-alpine AS build WORKDIR /src -COPY . /src +COPY package.json package-lock.json ./ +RUN npm ci -# Install terser to minify JS files -RUN npm install -g terser -RUN if [ -d "js" ]; then find js -type f -name "*.js" -exec sh -c 'for f; do terser "$f" -c -m -o "$f"; done' sh {} +; fi +COPY . . +RUN OUTPUT_DIR=/dist npm run build:production FROM nginx:alpine-slim AS final COPY nginx.conf /etc/nginx/nginx.conf -COPY . /usr/share/nginx/html -COPY --from=build /src/js /usr/share/nginx/html/js +COPY --from=build /dist /usr/share/nginx/html EXPOSE 80 -CMD ["nginx", "-g", "daemon off;"] \ No newline at end of file +CMD ["nginx", "-g", "daemon off;"] diff --git a/css/main.css b/css/main.css index 6498e31..33d8733 100644 --- a/css/main.css +++ b/css/main.css @@ -1,7 +1,3 @@ -@import url('variables.css'); -@import url('https://fonts.googleapis.com/css2?family=Lexend:wght@100..900&display=swap'); -@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400..700&display=swap'); - * { margin: 0; padding: 0; @@ -60,7 +56,6 @@ img { border-top: 1px solid var(--glass-border); border-bottom: 1px solid var(--glass-border); box-shadow: var(--shadow-lg), var(--inset-shadow); - will-change: box-shadow; animation: glass-shadow 10s ease-in-out infinite; } diff --git a/index.html b/index.html index 8482595..1fe721b 100644 --- a/index.html +++ b/index.html @@ -14,6 +14,10 @@ ivanch + + + + @@ -34,7 +38,7 @@
- Avatar + Avatar
@@ -152,4 +156,4 @@ - \ No newline at end of file + diff --git a/js/matrix.js b/js/matrix.js index c70f0ad..246836d 100644 --- a/js/matrix.js +++ b/js/matrix.js @@ -1,13 +1,21 @@ class MatrixBackground { constructor() { this.canvas = document.getElementById('matrixCanvas'); + if (!this.canvas) return; + this.ctx = this.canvas.getContext('2d'); + if (!this.ctx) return; + this.dots = []; this.connections = []; this.mouse = { x: -9999, y: -9999, active: false }; this.rafId = null; - this.isRunning = true; - this.prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; + this.isRunning = false; + this.lastTimestamp = 0; + this.motionQuery = window.matchMedia('(prefers-reduced-motion: reduce)'); + this.prefersReducedMotion = this.motionQuery.matches; + this.dotSpriteCache = new Map(); + this.onAnimationFrame = (timestamp) => this.animate(timestamp); this.config = { dotCount: this.getMobileDotCount(), @@ -18,7 +26,6 @@ class MatrixBackground { dotSize: 3, mouseRadius: 110, mouseForce: 0.6, - // Palette synced to the UI accent tokens colors: { dotDefault: 'rgba(230, 240, 255, 0.78)', dotDefaultGlow: 'rgba(180, 220, 255, 0.40)', @@ -51,15 +58,18 @@ class MatrixBackground { init() { this.handleResize(); this.bindEvents(); - this.animate(); + + if (this.prefersReducedMotion) { + this.renderFrame(0); + } else { + this.start(); + } } bindEvents() { - // Pointer reactivity — push dots away from cursor for a "matrix repel" feel. - // Touch devices are included; the dot pushes happen only while touched. - window.addEventListener('pointermove', (e) => { - this.mouse.x = e.clientX; - this.mouse.y = e.clientY; + window.addEventListener('pointermove', (event) => { + this.mouse.x = event.clientX; + this.mouse.y = event.clientY; this.mouse.active = true; }, { passive: true }); @@ -69,35 +79,59 @@ class MatrixBackground { this.mouse.y = -9999; }); - // Pause the rAF loop when the tab is hidden to save CPU / battery. document.addEventListener('visibilitychange', () => { if (document.hidden) { this.stop(); + } else if (this.prefersReducedMotion) { + this.renderFrame(this.lastTimestamp); } else { this.start(); } }); - // Resize is debounced via rAF; disabled on mobile for perf parity with original. + const updateMotionPreference = (event) => { + this.prefersReducedMotion = event.matches; + + if (this.prefersReducedMotion) { + this.stop(); + this.renderFrame(this.lastTimestamp); + } else if (!document.hidden) { + this.start(); + } + }; + + if (this.motionQuery.addEventListener) { + this.motionQuery.addEventListener('change', updateMotionPreference); + } else { + this.motionQuery.addListener(updateMotionPreference); + } + if (window.innerWidth > 768) { let resizeRaf = null; window.addEventListener('resize', () => { - if (resizeRaf) cancelAnimationFrame(resizeRaf); - resizeRaf = requestAnimationFrame(() => this.handleResize()); + if (resizeRaf !== null) cancelAnimationFrame(resizeRaf); + resizeRaf = requestAnimationFrame(() => { + resizeRaf = null; + this.handleResize(); + }); }); } } start() { - if (this.rafId || !this.isRunning) return; - this.animate(); + if (this.rafId !== null || this.prefersReducedMotion || document.hidden) return; + + this.isRunning = true; + this.rafId = requestAnimationFrame(this.onAnimationFrame); } stop() { - if (this.rafId) { + if (this.rafId !== null) { cancelAnimationFrame(this.rafId); - this.rafId = null; } + + this.rafId = null; + this.isRunning = false; } handleResize() { @@ -105,12 +139,17 @@ class MatrixBackground { this.canvas.height = window.innerHeight; this.config.dotCount = this.getMobileDotCount(); this.config.maxConnections = this.getMobileMaxConnections(); + this.connections = []; this.createDots(); + + if (this.prefersReducedMotion) { + this.renderFrame(this.lastTimestamp); + } } createDots() { this.dots = []; - for (let i = 0; i < this.config.dotCount; i++) { + for (let index = 0; index < this.config.dotCount; index++) { this.createDot(); } } @@ -118,7 +157,6 @@ class MatrixBackground { createDot() { const startX = Math.random() * (this.canvas.width - 20) + 10; const startY = Math.random() * (this.canvas.height - 20) + 10; - const angle = Math.random() * Math.PI * 2; const speed = this.config.dotSpeed * (0.8 + Math.random() * 0.4); const baseVelocityX = Math.cos(angle) * speed; @@ -141,36 +179,32 @@ class MatrixBackground { updateDots() { const radius = this.config.mouseRadius; - const radiusSq = radius * radius; + const radiusSquared = radius * radius; const force = this.config.mouseForce; - this.dots.forEach(dot => { - // Mouse repulsion + for (const dot of this.dots) { if (this.mouse.active) { const dx = dot.x - this.mouse.x; const dy = dot.y - this.mouse.y; - const distSq = dx * dx + dy * dy; - if (distSq < radiusSq && distSq > 0.5) { - const dist = Math.sqrt(distSq); - const strength = (1 - dist / radius) * force; - dot.vx += (dx / dist) * strength; - dot.vy += (dy / dist) * strength; + const distanceSquared = dx * dx + dy * dy; + + if (distanceSquared < radiusSquared && distanceSquared > 0.5) { + const distance = Math.sqrt(distanceSquared); + const strength = (1 - distance / radius) * force; + dot.vx += (dx / distance) * strength; + dot.vy += (dy / distance) * strength; } } dot.x += dot.vx; dot.y += dot.vy; - - // Ease back to a gentle autonomous drift after pointer interaction. dot.wanderPhase += dot.wanderSpeed; + const wanderX = Math.cos(dot.wanderPhase) * 0.03; const wanderY = Math.sin(dot.wanderPhase * 0.8) * 0.03; dot.vx += (dot.baseVx + wanderX - dot.vx) * 0.02; dot.vy += (dot.baseVy + wanderY - dot.vy) * 0.02; - // Reflect at the canvas edge. Keep the base velocity in sync with - // the bounce so the drift easing does not push the dot back into - // the same wall on the next frames. const dotRadius = dot.size / 2; const minX = dotRadius; const minY = dotRadius; @@ -197,69 +231,160 @@ class MatrixBackground { dot.baseVy = -Math.abs(dot.baseVy); } - // A large pointer impulse can overshoot both edges by more than - // one frame, so clamp after reflecting the position. dot.x = Math.max(minX, Math.min(maxX, dot.x)); dot.y = Math.max(minY, Math.min(maxY, dot.y)); - }); + } + } + + getDotSprite(size, isConnected) { + const roundedSize = Math.round(size * 2) / 2; + const state = isConnected ? 'connected' : 'default'; + const cacheKey = state + ':' + roundedSize; + const cachedSprite = this.dotSpriteCache.get(cacheKey); + + if (cachedSprite) { + return cachedSprite; + } + + const padding = 10; + const dimension = Math.ceil(roundedSize + padding * 2); + const sprite = document.createElement('canvas'); + sprite.width = dimension; + sprite.height = dimension; + + const spriteContext = sprite.getContext('2d'); + spriteContext.shadowBlur = 6; + spriteContext.shadowColor = isConnected + ? this.config.colors.dotConnectedGlow + : this.config.colors.dotDefaultGlow; + spriteContext.fillStyle = isConnected + ? this.config.colors.dotConnected + : this.config.colors.dotDefault; + spriteContext.beginPath(); + spriteContext.arc(dimension / 2, dimension / 2, roundedSize / 2, 0, Math.PI * 2); + spriteContext.fill(); + + this.dotSpriteCache.set(cacheKey, sprite); + return sprite; } drawDots() { - this.dots.forEach(dot => { - const isConnected = dot.connectionCount > 0; - this.ctx.beginPath(); - this.ctx.arc(dot.x, dot.y, dot.size / 2, 0, Math.PI * 2); - this.ctx.fillStyle = isConnected ? this.config.colors.dotConnected : this.config.colors.dotDefault; + for (const dot of this.dots) { + const sprite = this.getDotSprite(dot.size, dot.connectionCount > 0); + const halfDimension = sprite.width / 2; + this.ctx.globalAlpha = dot.opacity; - this.ctx.shadowBlur = 6; - this.ctx.shadowColor = isConnected ? this.config.colors.dotConnectedGlow : this.config.colors.dotDefaultGlow; - this.ctx.fill(); - }); + this.ctx.drawImage(sprite, dot.x - halfDimension, dot.y - halfDimension); + } + this.ctx.globalAlpha = 1; - this.ctx.shadowBlur = 0; } - createConnection(dot1, dot2) { + createConnection(dot1, dot2, timestamp) { if (this.connections.length >= this.config.maxConnections) return; dot1.connectionCount++; dot2.connectionCount++; this.connections.push({ - dot1: dot1, - dot2: dot2, - startTime: Date.now(), - duration: Math.random() * (this.config.connectionDuration.max - this.config.connectionDuration.min) + this.config.connectionDuration.min + dot1, + dot2, + startTime: timestamp, + duration: Math.random() * (this.config.connectionDuration.max - this.config.connectionDuration.min) + + this.config.connectionDuration.min }); } - updateConnections() { - this.connections = this.connections.filter(conn => { - const elapsed = Date.now() - conn.startTime; - if (elapsed > conn.duration) { - conn.dot1.connectionCount--; - conn.dot2.connectionCount--; - return false; + updateConnections(timestamp) { + let writeIndex = 0; + + for (let readIndex = 0; readIndex < this.connections.length; readIndex++) { + const connection = this.connections[readIndex]; + + if (timestamp - connection.startTime > connection.duration) { + connection.dot1.connectionCount--; + connection.dot2.connectionCount--; + continue; } - return true; - }); + + this.connections[writeIndex] = connection; + writeIndex++; + } + + this.connections.length = writeIndex; } - drawConnections() { - this.connections.forEach(conn => { - const { dot1, dot2, startTime, duration } = conn; - const elapsed = Date.now() - startTime; - const opacity = Math.min(1, elapsed / 500); + samplePoisson(lambda) { + if (lambda <= 0) return 0; + + const threshold = Math.exp(-lambda); + let product = 1; + let count = 0; + + do { + count++; + product *= Math.random(); + } while (product > threshold); + + return count - 1; + } + + tryCreateConnections(timestamp) { + const dotCount = this.dots.length; + if (dotCount < 2 || this.connections.length >= this.config.maxConnections) return; + + const pairCount = dotCount * (dotCount - 1) / 2; + const attempts = this.samplePoisson(pairCount * this.config.connectionChance); + + for (let attempt = 0; + attempt < attempts && this.connections.length < this.config.maxConnections; + attempt++) { + const firstIndex = Math.floor(Math.random() * dotCount); + let secondIndex = Math.floor(Math.random() * (dotCount - 1)); + + if (secondIndex >= firstIndex) { + secondIndex++; + } + + const dot1 = this.dots[firstIndex]; + const dot2 = this.dots[secondIndex]; + const dx = dot2.x - dot1.x; + const dy = dot2.y - dot1.y; + const distanceSquared = dx * dx + dy * dy; + + if (distanceSquared >= 22500 || distanceSquared <= 900) { + continue; + } + + const alreadyConnected = this.connections.some((connection) => + (connection.dot1 === dot1 && connection.dot2 === dot2) + || (connection.dot1 === dot2 && connection.dot2 === dot1) + ); + + if (!alreadyConnected) { + this.createConnection(dot1, dot2, timestamp); + } + } + } + + drawConnections(timestamp) { + for (const connection of this.connections) { + const elapsed = timestamp - connection.startTime; + const opacity = Math.min(1, elapsed / 500); + const gradient = this.ctx.createLinearGradient( + connection.dot1.x, + connection.dot1.y, + connection.dot2.x, + connection.dot2.y + ); - const gradient = this.ctx.createLinearGradient(dot1.x, dot1.y, dot2.x, dot2.y); gradient.addColorStop(0, this.config.colors.connection.start); gradient.addColorStop(0.5, this.config.colors.connection.middle); gradient.addColorStop(1, this.config.colors.connection.end); this.ctx.beginPath(); - this.ctx.moveTo(dot1.x, dot1.y); - this.ctx.lineTo(dot2.x, dot2.y); - + this.ctx.moveTo(connection.dot1.x, connection.dot1.y); + this.ctx.lineTo(connection.dot2.x, connection.dot2.y); this.ctx.strokeStyle = gradient; this.ctx.lineWidth = 2; this.ctx.globalAlpha = opacity; @@ -267,51 +392,34 @@ class MatrixBackground { const pulse = (Math.sin((elapsed / 2000) * Math.PI * 2) + 1) / 2; this.ctx.shadowBlur = 4 + pulse * 6; this.ctx.shadowColor = 'rgba(118, 75, 162, 0.5)'; - this.ctx.stroke(); - }); + } + this.ctx.globalAlpha = 1; this.ctx.shadowBlur = 0; } - tryCreateConnections() { - for (let i = 0; i < this.dots.length; i++) { - for (let j = i + 1; j < this.dots.length; j++) { - if (Math.random() < this.config.connectionChance) { - const dot1 = this.dots[i]; - const dot2 = this.dots[j]; - - const dx = dot2.x - dot1.x; - const dy = dot2.y - dot1.y; - const distance = Math.sqrt(dx * dx + dy * dy); - - if (distance < 150 && distance > 30) { - const alreadyConnected = this.connections.some(conn => - (conn.dot1 === dot1 && conn.dot2 === dot2) || - (conn.dot1 === dot2 && conn.dot2 === dot1) - ); - if (!alreadyConnected) { - this.createConnection(dot1, dot2); - } - } - } - } - } - } - - animate() { + renderFrame(timestamp) { + this.lastTimestamp = timestamp; this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); if (!this.prefersReducedMotion) { this.updateDots(); - this.updateConnections(); - this.tryCreateConnections(); + this.updateConnections(timestamp); + this.tryCreateConnections(timestamp); } - this.drawConnections(); + this.drawConnections(timestamp); this.drawDots(); + } - this.rafId = requestAnimationFrame(() => this.animate()); + animate(timestamp) { + this.rafId = null; + this.renderFrame(timestamp); + + if (this.isRunning && !this.prefersReducedMotion && !document.hidden) { + this.rafId = requestAnimationFrame(this.onAnimationFrame); + } } } diff --git a/js/social.js b/js/social.js index 0a956cb..a9ac9c9 100644 --- a/js/social.js +++ b/js/social.js @@ -9,13 +9,25 @@ document.addEventListener('DOMContentLoaded', () => { const prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches; if (avatar && avatarImg && !prefersReducedMotion) { - ['r', 'b'].forEach((channel) => { - const clone = avatarImg.cloneNode(false); - clone.classList.add('glitch-layer', channel); - clone.removeAttribute('alt'); - clone.setAttribute('aria-hidden', 'true'); - avatar.appendChild(clone); - }); + const createGlitchLayers = () => { + if (avatar.querySelector('.glitch-layer')) return; + + ['r', 'b'].forEach((channel) => { + const clone = avatarImg.cloneNode(false); + clone.classList.add('glitch-layer', channel); + clone.removeAttribute('alt'); + clone.setAttribute('aria-hidden', 'true'); + avatar.appendChild(clone); + }); + }; + + const removeGlitchLayers = () => { + avatar.querySelectorAll('.glitch-layer').forEach((layer) => layer.remove()); + }; + + avatar.addEventListener('pointerenter', createGlitchLayers); + avatar.addEventListener('pointerleave', removeGlitchLayers); + avatar.addEventListener('pointercancel', removeGlitchLayers); } // ---------------------------------------------------------------- diff --git a/nginx.conf b/nginx.conf index 4fe72e2..6896adc 100644 --- a/nginx.conf +++ b/nginx.conf @@ -1,10 +1,10 @@ user nginx; -worker_processes auto; +worker_processes 1; error_log /var/log/nginx/error.log warn; pid /var/run/nginx.pid; events { - worker_connections 1024; + worker_connections 256; use epoll; } @@ -12,22 +12,16 @@ http { include /etc/nginx/mime.types; default_type application/octet-stream; - log_format main '$remote_addr - $remote_user [$time_local] "$request" ' - '$status $body_bytes_sent "$http_referer" ' - '"$http_user_agent" "$http_x_forwarded_for"'; + access_log off; - access_log /var/log/nginx/access.log main; - - # Performance optimizations sendfile on; tcp_nopush on; tcp_nodelay on; - keepalive_timeout 65; + keepalive_timeout 15; keepalive_requests 100; types_hash_max_size 2048; - client_max_body_size 20M; + client_max_body_size 1m; - # Buffers & Timeouts tuning client_body_buffer_size 16k; client_header_buffer_size 1k; large_client_header_buffers 4 8k; @@ -35,17 +29,18 @@ http { client_header_timeout 12s; send_timeout 10s; - # Open File Cache - open_file_cache max=1000 inactive=20s; - open_file_cache_valid 30s; + open_file_cache max=100 inactive=60s; + open_file_cache_valid 60s; open_file_cache_min_uses 2; - open_file_cache_errors on; + open_file_cache_errors off; - # Gzip compression gzip on; + gzip_static on; gzip_vary on; gzip_proxied any; - gzip_comp_level 6; + gzip_comp_level 4; + gzip_min_length 256; + gzip_disable "msie6"; gzip_types text/plain text/css @@ -62,10 +57,7 @@ http { font/woff2 application/vnd.ms-fontobject application/x-font-ttf; - gzip_min_length 256; - gzip_disable "msie6"; - # Security headers (Global default) add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; @@ -73,67 +65,33 @@ http { add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'; frame-ancestors 'self';" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always; - # HTTP (port 80) Server Block server { listen 80; listen [::]:80; server_name localhost ivanch.me www.ivanch.me; root /usr/share/nginx/html; index index.html; - - # Charset charset utf-8; - # Custom error pages error_page 404 /404.html; - error_page 500 502 503 504 /50x.html; - # Main location location / { try_files $uri $uri/ =404; } - # Cache static assets & redeclare security headers to bypass Nginx header override gotcha - location ~* \.(jpg|jpeg|png|gif|ico|svg|webp)$ { - expires 1y; - add_header Cache-Control "public, immutable"; + location ~* \.(css|js|jpg|jpeg|png|gif|ico|svg|webp|woff|woff2|ttf|otf|eot)$ { + add_header Cache-Control "public, max-age=31536000, immutable" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; add_header Referrer-Policy "strict-origin-when-cross-origin" always; add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'; frame-ancestors 'self';" always; add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always; - access_log off; } - location ~* \.(css|js)$ { - expires 1y; - add_header Cache-Control "public, immutable"; - add_header X-Frame-Options "SAMEORIGIN" always; - add_header X-Content-Type-Options "nosniff" always; - add_header X-XSS-Protection "1; mode=block" always; - add_header Referrer-Policy "strict-origin-when-cross-origin" always; - add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'; frame-ancestors 'self';" always; - add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always; - access_log off; - } - - location ~* \.(woff|woff2|ttf|otf|eot)$ { - expires 1y; - add_header Cache-Control "public, immutable"; - add_header X-Frame-Options "SAMEORIGIN" always; - add_header X-Content-Type-Options "nosniff" always; - add_header X-XSS-Protection "1; mode=block" always; - add_header Referrer-Policy "strict-origin-when-cross-origin" always; - add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'; frame-ancestors 'self';" always; - add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always; - access_log off; - } - - # HTML files - shorter cache location ~* \.html$ { - expires 1h; - add_header Cache-Control "public, must-revalidate"; + expires off; + add_header Cache-Control "no-cache, must-revalidate" always; add_header X-Frame-Options "SAMEORIGIN" always; add_header X-Content-Type-Options "nosniff" always; add_header X-XSS-Protection "1; mode=block" always; @@ -142,19 +100,14 @@ http { add_header Permissions-Policy "camera=(), microphone=(), geolocation=(), payment=()" always; } - # Deny access to hidden files location ~ /\. { deny all; - access_log off; log_not_found off; } - # Deny access to backup files location ~ ~$ { deny all; - access_log off; log_not_found off; } } } - diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..49bb7e7 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1415 @@ +{ + "name": "ivanch-me", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ivanch-me", + "devDependencies": { + "esbuild": "0.28.1", + "html-minifier-terser": "7.2.0", + "sharp": "0.35.3" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz", + "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz", + "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz", + "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz", + "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz", + "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz", + "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz", + "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz", + "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz", + "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz", + "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz", + "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz", + "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz", + "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz", + "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz", + "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz", + "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz", + "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz", + "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.2" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz", + "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz", + "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz", + "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.2" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz", + "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==", + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.1" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz", + "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz", + "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz", + "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz", + "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/acorn": { + "version": "8.17.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.17.0.tgz", + "integrity": "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/clean-css": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/html-minifier-terser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz", + "integrity": "sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "~5.3.2", + "commander": "^10.0.0", + "entities": "^4.4.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.15.1" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": "^14.13.1 || >=16.0.0" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.35.3", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz", + "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.3", + "@img/sharp-darwin-x64": "0.35.3", + "@img/sharp-freebsd-wasm32": "0.35.3", + "@img/sharp-libvips-darwin-arm64": "1.3.2", + "@img/sharp-libvips-darwin-x64": "1.3.2", + "@img/sharp-libvips-linux-arm": "1.3.2", + "@img/sharp-libvips-linux-arm64": "1.3.2", + "@img/sharp-libvips-linux-ppc64": "1.3.2", + "@img/sharp-libvips-linux-riscv64": "1.3.2", + "@img/sharp-libvips-linux-s390x": "1.3.2", + "@img/sharp-libvips-linux-x64": "1.3.2", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.2", + "@img/sharp-libvips-linuxmusl-x64": "1.3.2", + "@img/sharp-linux-arm": "0.35.3", + "@img/sharp-linux-arm64": "0.35.3", + "@img/sharp-linux-ppc64": "0.35.3", + "@img/sharp-linux-riscv64": "0.35.3", + "@img/sharp-linux-s390x": "0.35.3", + "@img/sharp-linux-x64": "0.35.3", + "@img/sharp-linuxmusl-arm64": "0.35.3", + "@img/sharp-linuxmusl-x64": "0.35.3", + "@img/sharp-webcontainers-wasm32": "0.35.3", + "@img/sharp-win32-arm64": "0.35.3", + "@img/sharp-win32-ia32": "0.35.3", + "@img/sharp-win32-x64": "0.35.3" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/terser": { + "version": "5.49.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.49.0.tgz", + "integrity": "sha512-SNiDnXyHSrxVcIOtVbULzcTmniUiwcV7Nwdyj1twVubeTmbjoa8p69KKDpfkdoOavuM4/GRm1+ykI8qqnavHoA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..7dbca29 --- /dev/null +++ b/package.json @@ -0,0 +1,12 @@ +{ + "name": "ivanch-me", + "private": true, + "scripts": { + "build:production": "node utils/build-production.mjs" + }, + "devDependencies": { + "esbuild": "0.28.1", + "html-minifier-terser": "7.2.0", + "sharp": "0.35.3" + } +} diff --git a/utils/build-production.mjs b/utils/build-production.mjs new file mode 100644 index 0000000..e059bbd --- /dev/null +++ b/utils/build-production.mjs @@ -0,0 +1,250 @@ +import { createHash } from 'node:crypto'; +import { promises as fs } from 'node:fs'; +import path from 'node:path'; +import { gzipSync } from 'node:zlib'; +import { transform } from 'esbuild'; +import { minify } from 'html-minifier-terser'; +import sharp from 'sharp'; + +const rootDirectory = path.resolve(process.cwd()); +const outputDirectory = path.resolve(process.env.OUTPUT_DIR || path.join(rootDirectory, 'dist')); + +const cssFiles = [ + 'css/variables.css', + 'css/main.css', + 'css/button.css', + 'css/matrix.css', + 'css/sections/error.css', + 'css/sections/hero.css', + 'css/sections/homelab.css', + 'css/sections/profile.css', + 'css/sections/projects.css' +]; + +const jsFiles = [ + 'js/matrix.js', + 'js/social.js' +]; + +const htmlFiles = [ + 'index.html', + '404.html' +]; + +const versionedFiles = new Map(); + +function absoluteSourcePath(relativePath) { + return path.join(rootDirectory, relativePath); +} + +function absoluteOutputPath(relativePath) { + return path.join(outputDirectory, relativePath); +} + +function versionFor(buffer) { + return createHash('sha256').update(buffer).digest('hex').slice(0, 12); +} + +async function requireSourceFile(relativePath) { + try { + await fs.access(absoluteSourcePath(relativePath)); + } catch { + throw new Error('Required source file is missing: ' + relativePath); + } +} + +async function writeOutput(relativePath, content) { + const targetPath = absoluteOutputPath(relativePath); + await fs.mkdir(path.dirname(targetPath), { recursive: true }); + await fs.writeFile(targetPath, content); +} + +async function minifyCss(relativePath) { + let source = await fs.readFile(absoluteSourcePath(relativePath), 'utf8'); + const transformed = await transform(source, { + loader: 'css', + legalComments: 'none', + minify: true + }); + + await writeOutput(relativePath, transformed.code); + versionedFiles.set(relativePath, versionFor(Buffer.from(transformed.code))); +} + +async function minifyJavaScript(relativePath) { + const source = await fs.readFile(absoluteSourcePath(relativePath), 'utf8'); + const transformed = await transform(source, { + loader: 'js', + legalComments: 'none', + minify: true, + target: 'es2020' + }); + + await writeOutput(relativePath, transformed.code); + versionedFiles.set(relativePath, versionFor(Buffer.from(transformed.code))); +} + +async function copyStaticFile(relativePath) { + const content = await fs.readFile(absoluteSourcePath(relativePath)); + await writeOutput(relativePath, content); + versionedFiles.set(relativePath, versionFor(content)); +} + +function isExternalReference(reference) { + return /^(?:[a-z][a-z0-9+.-]*:|\/\/|#|data:)/i.test(reference); +} + +function versionedReference(reference) { + if (isExternalReference(reference)) { + return reference; + } + + const isAbsolute = reference.startsWith('/'); + const relativePath = (isAbsolute ? reference.slice(1) : reference).split(/[?#]/)[0]; + const version = versionedFiles.get(relativePath); + + if (!version) { + return reference; + } + + return (isAbsolute ? '/' : '') + relativePath + '?v=' + version; +} + +function replaceProductionReferences(html, avatarVersion) { + return html.replace(/\b(?:href|src)=(["'])([^"']+)\1/g, (match, quote, reference) => { + if (reference === '/assets/lain.png') { + return match.replace(reference, '/assets/lain.webp?v=' + avatarVersion); + } + + const replacement = versionedReference(reference); + return replacement === reference ? match : match.replace(reference, replacement); + }); +} + +async function minifyHtml(relativePath, avatarVersion) { + const source = await fs.readFile(absoluteSourcePath(relativePath), 'utf8'); + const withProductionAssets = replaceProductionReferences(source, avatarVersion); + const compactHtml = await minify(withProductionAssets, { + collapseWhitespace: true, + minifyCSS: false, + minifyJS: false, + removeComments: true, + removeRedundantAttributes: true, + sortAttributes: false, + sortClassName: false, + useShortDoctype: true + }); + + await writeOutput(relativePath, compactHtml); +} + +async function assertHtmlReferencesResolve(relativePath) { + const html = await fs.readFile(absoluteOutputPath(relativePath), 'utf8'); + const references = html.matchAll(/\b(?:href|src)=(["'])([^"']+)\1/g); + + for (const referenceMatch of references) { + const reference = referenceMatch[2]; + if (isExternalReference(reference)) { + continue; + } + + const relativePath = (reference.startsWith('/') ? reference.slice(1) : reference).split(/[?#]/)[0]; + const targetPath = path.resolve(outputDirectory, relativePath); + const relativeTarget = path.relative(outputDirectory, targetPath); + + if (relativeTarget.startsWith('..') || path.isAbsolute(relativeTarget)) { + throw new Error('Production reference escapes the output directory: ' + reference); + } + + try { + await fs.access(targetPath); + } catch { + throw new Error('Production reference does not resolve: ' + reference); + } + } +} + +async function gzipFile(relativePath) { + const source = await fs.readFile(absoluteOutputPath(relativePath)); + await fs.writeFile(absoluteOutputPath(relativePath) + '.gz', gzipSync(source, { + level: 9, + mtime: 0 + })); +} + +async function byteSize(relativePath) { + return (await fs.stat(absoluteOutputPath(relativePath))).size; +} + +async function build() { + const requiredFiles = [ + ...cssFiles, + ...jsFiles, + ...htmlFiles, + 'assets/favicon.ico', + 'assets/lain.png' + ]; + + await Promise.all(requiredFiles.map(requireSourceFile)); + await fs.rm(outputDirectory, { force: true, recursive: true }); + await fs.mkdir(outputDirectory, { recursive: true }); + + for (const cssFile of cssFiles) { + await minifyCss(cssFile); + } + + for (const jsFile of jsFiles) { + await minifyJavaScript(jsFile); + } + + await copyStaticFile('assets/favicon.ico'); + + const optimizedAvatar = 'assets/lain.webp'; + await fs.mkdir(path.dirname(absoluteOutputPath(optimizedAvatar)), { recursive: true }); + await sharp(absoluteSourcePath('assets/lain.png')) + .resize(320, 320, { fit: 'cover', position: 'centre' }) + .webp({ effort: 4, quality: 82 }) + .toFile(absoluteOutputPath(optimizedAvatar)); + + const avatarVersion = versionFor(await fs.readFile(absoluteOutputPath(optimizedAvatar))); + + for (const htmlFile of htmlFiles) { + await minifyHtml(htmlFile, avatarVersion); + } + + await Promise.all(htmlFiles.map(assertHtmlReferencesResolve)); + + const compressedFiles = [ + ...cssFiles, + ...jsFiles, + ...htmlFiles + ]; + await Promise.all(compressedFiles.map(gzipFile)); + + const avatarBytes = await byteSize(optimizedAvatar); + if (avatarBytes > 100 * 1024) { + throw new Error('Optimized avatar exceeds the 100 KiB budget: ' + avatarBytes + ' bytes'); + } + + const firstPartyFiles = [ + 'index.html.gz', + ...cssFiles.map((file) => file + '.gz'), + ...jsFiles.map((file) => file + '.gz'), + optimizedAvatar, + 'assets/favicon.ico' + ]; + const firstPartyBytes = (await Promise.all(firstPartyFiles.map(byteSize))) + .reduce((total, size) => total + size, 0); + + if (firstPartyBytes > 200 * 1024) { + throw new Error('First-party transfer exceeds the 200 KiB budget: ' + firstPartyBytes + ' bytes'); + } + + console.log(JSON.stringify({ + avatarBytes, + firstPartyBytes, + outputDirectory + })); +} + +await build();