From cecba33ebb828ae6a44845d474944a6125bd9832 Mon Sep 17 00:00:00 2001 From: david Date: Mon, 3 Nov 2025 20:45:03 +0100 Subject: [PATCH] Dockers --- .gitignore | 2 +- Dockerfile | 94 ++++++++ apps/server/.env.production | 4 +- apps/server/Dockerfile | 13 -- apps/server/package.json | 7 +- apps/server/src/index.ts | 32 ++- apps/server/tsconfig.json | 10 +- apps/server/tsup.config.ts | 65 +++++- apps/web/package.json | 4 +- docker-compose.yml | 31 --- modules/auth/package.json | 2 +- modules/core/package.json | 2 +- modules/customer-invoices/package.json | 2 +- .../value-objects/customer-invoice-status.ts | 5 +- modules/customers/package.json | 2 +- modules/doc-numbering/package.json | 2 +- modules/verifactu/package.json | 2 +- packages/rdx-criteria/package.json | 2 +- packages/rdx-ddd/package.json | 2 +- packages/rdx-logger/package.json | 2 +- packages/rdx-utils/package.json | 2 +- scripts/build-api.sh | 219 ++++++++++++++++++ tsconfig.json.bak | 4 - 23 files changed, 418 insertions(+), 92 deletions(-) create mode 100644 Dockerfile delete mode 100644 apps/server/Dockerfile delete mode 100644 docker-compose.yml create mode 100755 scripts/build-api.sh delete mode 100644 tsconfig.json.bak diff --git a/.gitignore b/.gitignore index 1761ed6e..54dcc97e 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,7 @@ dist-ssr .cache server/dist public/dist - +out apps/**/false/* false diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..64f0a6bd --- /dev/null +++ b/Dockerfile @@ -0,0 +1,94 @@ +# syntax=docker/dockerfile:1.7-labs + +ARG NODE_IMAGE=node:22-alpine +ARG PNPM_VERSION=10.20.0 + +######################## +# 0) Base común +######################## +FROM ${NODE_IMAGE} AS base +ENV CI=1 \ + NODE_ENV=production +RUN corepack enable && corepack prepare pnpm@${PNPM_VERSION} --activate + +WORKDIR /repo + +######################## +# 1) Turborepo prune (reduce contexto) +######################## +FROM base AS pruner + +# Copiar archivos raíz necesarios +COPY turbo.json package.json pnpm-workspace.yaml pnpm-lock.yaml ./ + +# Copiar los directorios relevantes del monorepo +COPY packages ./packages +COPY apps ./apps +COPY modules ./modules + +# Ejecuta prune para @erp/factuges-server +RUN npx --yes turbo@2.5.8 prune @erp/factuges-server --docker + +######################## +# 2) Instalar deps de la parte pruned (usando pnpm fetch para cache) +######################## +#FROM base AS installer + +# Traemos lockfile y pnpm-store desde la etapa prune +#COPY --from=pruner /repo/out/json/ ./ +#COPY --from=pruner /repo/out/pnpm-lock.yaml ./pnpm-lock.yaml + +# Prefetch a store global (rápido y cacheable) +#RUN pnpm fetch +#RUN pnpm install --frozen-lockfile + +######################## +# 3) Builder: código + link deps y build +######################## +FROM base AS builder + +# Copiamos todo lo “pruned” (json, lock, full) +COPY --from=pruner /repo/out/full/ ./ +COPY --from=pruner /repo/out/pnpm-lock.yaml ./pnpm-lock.yaml + +# Reutilizamos la store prefetch +#COPY --from=installer /root/.local/share/pnpm/store /root/.local/share/pnpm/store + +# Instalamos solo lo necesario para prod de los workspaces +RUN pnpm install --frozen-lockfile --prefer-offline + +# IMPORTANTE: build de paquetes buildables (utils, logger, etc.) primero +# Turborepo asegura orden con "dependsOn". Un único comando: +RUN pnpm -w turbo run build --filter=@erp/factuges-server... + +######################## +# 4) Runner minimal +######################## +#FROM ${NODE_IMAGE} AS runner +FROM base AS runner +ENV NODE_ENV=production TZ=UTC + +# Don't run production as root +RUN addgroup --system --gid 1001 expressjs +RUN adduser --system --uid 1001 expressjs +USER expressjs + +#COPY --from=builder /repo . +COPY --from=builder /repo/node_modules ./node_modules +COPY --from=builder /repo/packages ./packages +COPY --from=builder /repo/package.json ./package.json +COPY --from=builder /repo/pnpm-lock.yaml ./pnpm-lock.yaml +COPY --from=builder /repo/pnpm-workspace.yaml ./pnpm-workspace.yaml + +COPY --from=builder /repo/apps/server/dist ./apps/server/dist +COPY --from=builder /repo/apps/server/.env.production ./apps/server/dist/.env +COPY --from=builder /repo/apps/server/node_modules ./apps/server/node_modules +COPY --from=builder /repo/apps/server/package.json ./apps/server/package.json + + +# Salud del contenedor (ajusta puerto/endpoint) +#HEALTHCHECK --interval=20s --timeout=3s --retries=5 \ +# CMD node -e "fetch('http://127.0.0.1:'+(process.env.PORT||3002)+'/health').then(r=>process.exit(r.ok?0:1)).catch(()=>process.exit(1))" + +EXPOSE 3002 +CMD ["pnpm","exec", "node", "--env-file=apps/server/dist/.env", "apps/server/dist/index.js"] diff --git a/apps/server/.env.production b/apps/server/.env.production index 26633961..baad0aca 100644 --- a/apps/server/.env.production +++ b/apps/server/.env.production @@ -1,7 +1,7 @@ -NODE_ENV=development +NODE_ENV=production HOST=0.0.0.0 PORT=3002 -FRONTEND_URL=http://localhost:5173 +FRONTEND_URL=http://factuges.rodax-software.local DB_DIALECT=mysql diff --git a/apps/server/Dockerfile b/apps/server/Dockerfile deleted file mode 100644 index 64c9fc45..00000000 --- a/apps/server/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -# server/Dockerfile -FROM node:22.13.1 - -WORKDIR /usr/src/app - -COPY package*.json ./ -RUN npm install --production - -COPY . . - -EXPOSE 5000 - -CMD ["npm", "start"] diff --git a/apps/server/package.json b/apps/server/package.json index 2da36adc..23bf12f9 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -1,10 +1,7 @@ { - "name": "@erp/server", - "version": "0.0.1", + "name": "@erp/factuges-server", + "version": "0.0.3", "private": true, - "type": "module", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", "scripts": { "build": "tsup src/index.ts --config tsup.config.ts", "dev": "tsx watch src/index.ts", diff --git a/apps/server/src/index.ts b/apps/server/src/index.ts index 3f5850a6..006874aa 100644 --- a/apps/server/src/index.ts +++ b/apps/server/src/index.ts @@ -2,13 +2,13 @@ import { DateTime } from "luxon"; import http from "node:http"; import os from "node:os"; import { z } from "zod/v4"; -import { createApp } from "./app"; -import { ENV } from "./config"; -import { tryConnectToDatabase } from "./config/database"; -import { registerHealthRoutes } from "./health"; -import { listRoutes, logger } from "./lib"; -import { initModules } from "./lib/modules"; -import { registerModules } from "./register-modules"; +import { createApp } from "./app.ts"; +import { tryConnectToDatabase } from "./config/database.ts"; +import { ENV } from "./config/index.ts"; +import { registerHealthRoutes } from "./health.ts"; +import { listRoutes, logger } from "./lib/index.ts"; +import { initModules } from "./lib/modules/index.ts"; +import { registerModules } from "./register-modules.ts"; const API_BASE_PATH = "/api/v1"; @@ -208,9 +208,23 @@ process.on("uncaughtException", async (error: Error) => { const now = DateTime.now(); logger.info(`Time: ${now.toLocaleString(DateTime.DATETIME_FULL)} ${now.zoneName}`); logger.info(`Launched in: ${now.diff(currentState.launchedAt).toMillis()} ms`); - logger.info(`Environment: ${currentState.environment}`); logger.info(`Process PID: ${process.pid}`); + // Mostrar variables de entorno + logger.info(`Environment: ${currentState.environment}`); + logger.info(`HOST: ${ENV.HOST}`); + logger.info(`PORT: ${ENV.PORT}`); + logger.info(`API_BASE_PATH: ${API_BASE_PATH}`); + + logger.info(`FRONTEND_URL: ${ENV.FRONTEND_URL}`); + + logger.info(`DB_HOST: ${ENV.DB_HOST}`); + logger.info(`DB_PORT: ${ENV.DB_PORT}`); + logger.info(`DB_NAME: ${ENV.DB_NAME}`); + logger.info(`DB_USER: ${ENV.DB_USER}`); + logger.info(`DB_LOGGING: ${ENV.DB_LOGGING}`); + logger.info(`DB_SYNC_MODE: ${ENV.DB_SYNC_MODE}`); + const database = await tryConnectToDatabase(); // Lógica de inicialización de DB, si procede: @@ -219,7 +233,7 @@ process.on("uncaughtException", async (error: Error) => { await initModules({ app, database, baseRoutePath: API_BASE_PATH, logger }); - // ✅ El servidor ya está listo para recibir tráfico + // El servidor ya está listo para recibir tráfico isReady = true; logger.info("✅ Server is READY (readiness=true)"); logger.info(`startup_duration_ms=${DateTime.now().diff(currentState.launchedAt).toMillis()}`); diff --git a/apps/server/tsconfig.json b/apps/server/tsconfig.json index 29c7698e..350a4fd7 100644 --- a/apps/server/tsconfig.json +++ b/apps/server/tsconfig.json @@ -2,13 +2,19 @@ "extends": "@repo/typescript-config/express.json", "compilerOptions": { + "noEmit": true, + "allowImportingTsExtensions": true, + "lib": ["ES2022"], "baseUrl": "./src", "paths": { "@/*": ["*"] }, - "outDir": "dist" + "outDir": "./dist", + "rootDir": "./src", + + "removeComments": true }, //"files": ["src/index.ts"], // Esta opción compila sólo los archivos listados (y sus dependencias importadas). "include": ["src"], - "exclude": ["src/**/__tests__/*", "src/**/*.mock.*", "src/**/*.test.*", "node_modules", "dist"] + "exclude": ["node_modules", "dist"] } diff --git a/apps/server/tsup.config.ts b/apps/server/tsup.config.ts index f7576c77..e145e53d 100644 --- a/apps/server/tsup.config.ts +++ b/apps/server/tsup.config.ts @@ -1,3 +1,5 @@ +import fs from "node:fs"; +import path from "node:path"; import { defineConfig } from "tsup"; /** @@ -10,23 +12,62 @@ import { defineConfig } from "tsup"; export default defineConfig({ entry: ["src/index.ts"], // punto de entrada principal format: ["esm"], // ESM nativo (Node 18+) - target: "node18", // objetivo de compilación - sourcemap: true, + target: "node22", // objetivo de compilación + bundle: true, // genera un único bundle, evita imports rotos + sourcemap: false, clean: true, + minify: false, treeshake: true, dts: false, // opcional, genera .d.ts outDir: "dist", + platform: "node", - // Paquetes de npm a mantener como externos (no se incluyen) - external: ["express", "zod", "react", "react-dom", "date-fns"], + // Paquetes de npm a mantener como externos (no se incluyen en el "bundle") + external: [], - // Paquetes internos buildless que se deben incluir en el bundle - noExternal: [ - "@erp/auth", - "@erp/core", - "@erp/customers", - "@erp/customer-invoices", - "@erp/verifactu", - "@repo/rdx-logger", + noExternal: ["@repo", "@erp"], + + esbuildOptions(options) { + // Permite resolver imports sin extensión (.ts, .js, .mjs, etc.) + options.resolveExtensions = [".ts", ".js", ".mjs", ".json"]; + + // Corrige la extensión de salida + options.outExtension = { ".js": ".js" }; + + // Permite usar require en contexto ESM (Node >=18) + options.banner = { + js: ` + import { createRequire } from "module"; + const require = createRequire(import.meta.url); + `, + }; + }, + + // Plugin: fuerza a que los imports relativos se traten como locales, no "external" + esbuildPlugins: [ + { + name: "fix-local-imports", + setup(build) { + build.onResolve({ filter: /^\.{1,2}\// }, (args) => { + const abs = path.resolve(args.resolveDir, args.path); + + // Si es un directorio, intenta resolver a index.(ts|js) + if (fs.existsSync(abs) && fs.statSync(abs).isDirectory()) { + const indexTs = path.join(abs, "index.ts"); + const indexJs = path.join(abs, "index.js"); + if (fs.existsSync(indexTs)) return { path: indexTs, external: false }; + if (fs.existsSync(indexJs)) return { path: indexJs, external: false }; + } + + // Si es un fichero con extensión válida + const withExts = [".ts", ".js", ".mjs"]; + for (const ext of withExts) { + if (fs.existsSync(abs + ext)) return { path: abs + ext, external: false }; + } + + return { path: abs, external: false }; + }); + }, + }, ], }); diff --git a/apps/web/package.json b/apps/web/package.json index abf3d067..fed4e9b2 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,7 +1,7 @@ { - "name": "@erp/web", + "name": "@erp/factuges-web", "private": true, - "version": "0.0.1", + "version": "0.0.3", "type": "module", "scripts": { "dev": "vite --host --clearScreen false", diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 8d8d10ed..00000000 --- a/docker-compose.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: factuges - -services: - mariadb: - image: mariadb:latest - container_name: mariadb - restart: always - environment: - MYSQL_ROOT_PASSWORD: rootpass - MYSQL_DATABASE: factuges_db - MYSQL_USER: factuges_usr - MYSQL_PASSWORD: factuges_pass - volumes: - - mariadb_data:/var/lib/mysql - ports: - - "3306:3306" - - phpmyadmin: - image: phpmyadmin/phpmyadmin - container_name: phpmyadmin - restart: always - environment: - PMA_HOST: mariadb - MYSQL_ROOT_PASSWORD: rootpass - ports: - - "8080:80" - depends_on: - - mariadb - -volumes: - mariadb_data: diff --git a/modules/auth/package.json b/modules/auth/package.json index beef397e..cb6692bb 100644 --- a/modules/auth/package.json +++ b/modules/auth/package.json @@ -1,6 +1,6 @@ { "name": "@erp/auth", - "version": "0.0.1", + "version": "0.0.3", "private": true, "type": "module", "sideEffects": false, diff --git a/modules/core/package.json b/modules/core/package.json index 663ab73a..3c04d49c 100644 --- a/modules/core/package.json +++ b/modules/core/package.json @@ -1,6 +1,6 @@ { "name": "@erp/core", - "version": "0.0.1", + "version": "0.0.3", "private": true, "type": "module", "sideEffects": false, diff --git a/modules/customer-invoices/package.json b/modules/customer-invoices/package.json index 853c3332..5eab095a 100644 --- a/modules/customer-invoices/package.json +++ b/modules/customer-invoices/package.json @@ -1,6 +1,6 @@ { "name": "@erp/customer-invoices", - "version": "0.0.1", + "version": "0.0.3", "private": true, "type": "module", "sideEffects": false, diff --git a/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-status.ts b/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-status.ts index 0ab197cd..13b9f208 100644 --- a/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-status.ts +++ b/modules/customer-invoices/src/api/domain/value-objects/customer-invoice-status.ts @@ -10,7 +10,10 @@ export enum INVOICE_STATUS { SENT = "sent", // <- Proforma APPROVED = "approved", // <- Proforma REJECTED = "rejected", // <- Proforma - ISSUED = "issued", // <- Factura y enviada a Veri*Factu + + // issued <- (si is_proforma === true) => Es una proforma (histórica) + // issued <- (si is_proforma === false) => Factura y enviada a Veri*Factu + ISSUED = "issued", } export class CustomerInvoiceStatus extends ValueObject { private static readonly ALLOWED_STATUSES = ["draft", "sent", "approved", "rejected", "issued"]; diff --git a/modules/customers/package.json b/modules/customers/package.json index 55320081..846be965 100644 --- a/modules/customers/package.json +++ b/modules/customers/package.json @@ -1,6 +1,6 @@ { "name": "@erp/customers", - "version": "0.0.1", + "version": "0.0.3", "private": true, "type": "module", "sideEffects": false, diff --git a/modules/doc-numbering/package.json b/modules/doc-numbering/package.json index f662c6b7..7f2462c0 100644 --- a/modules/doc-numbering/package.json +++ b/modules/doc-numbering/package.json @@ -1,6 +1,6 @@ { "name": "@erp/doc-numbering", - "version": "0.0.1", + "version": "0.0.3", "private": true, "type": "module", "sideEffects": false, diff --git a/modules/verifactu/package.json b/modules/verifactu/package.json index ff80f056..51e59db2 100644 --- a/modules/verifactu/package.json +++ b/modules/verifactu/package.json @@ -1,6 +1,6 @@ { "name": "@erp/verifactu", - "version": "0.0.1", + "version": "0.0.3", "private": true, "type": "module", "sideEffects": false, diff --git a/packages/rdx-criteria/package.json b/packages/rdx-criteria/package.json index 354ec77e..bcf8c303 100644 --- a/packages/rdx-criteria/package.json +++ b/packages/rdx-criteria/package.json @@ -1,6 +1,6 @@ { "name": "@repo/rdx-criteria", - "version": "0.0.1", + "version": "0.0.3", "private": true, "type": "module", "sideEffects": false, diff --git a/packages/rdx-ddd/package.json b/packages/rdx-ddd/package.json index b847980d..7ed7856c 100644 --- a/packages/rdx-ddd/package.json +++ b/packages/rdx-ddd/package.json @@ -1,6 +1,6 @@ { "name": "@repo/rdx-ddd", - "version": "0.0.1", + "version": "0.0.3", "private": true, "type": "module", "sideEffects": false, diff --git a/packages/rdx-logger/package.json b/packages/rdx-logger/package.json index d2a36e0c..bd9a0ec2 100644 --- a/packages/rdx-logger/package.json +++ b/packages/rdx-logger/package.json @@ -1,6 +1,6 @@ { "name": "@repo/rdx-logger", - "version": "0.0.1", + "version": "0.0.3", "private": true, "type": "module", "sideEffects": false, diff --git a/packages/rdx-utils/package.json b/packages/rdx-utils/package.json index 4ae72068..e55ac3f0 100644 --- a/packages/rdx-utils/package.json +++ b/packages/rdx-utils/package.json @@ -1,6 +1,6 @@ { "name": "@repo/rdx-utils", - "version": "0.0.1", + "version": "0.0.3", "private": true, "type": "module", "sideEffects": false, diff --git a/scripts/build-api.sh b/scripts/build-api.sh new file mode 100755 index 00000000..1c49e9a4 --- /dev/null +++ b/scripts/build-api.sh @@ -0,0 +1,219 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_VERSION="1.0.3" + +# ===================================================== +# FACTUGES Build Script +# ----------------------------------------------------- +# Build + Export de la API y/o +# compilación de la Web (por compañía) +# ===================================================== +# Uso: +# ./scripts/build-api.sh [--api|web|all] [--load] +# +# Ejemplos: +# ./scripts/build-api.sh acme --api --load # solo API + carga en Docker local +# ./scripts/build-api.sh acme --web # solo web +# ./scripts/build-api.sh acme # API + web (por defecto) +# +# Funcionalidades: +# - Detecta automáticamente el nombre, versión y puerto de la API +# - Compila la web React (Vite) +# - Copia los estáticos a out//web/dist +# - Genera imagen Docker etiquetada por compañía + versión + latest +# - Guarda la imagen .tar en out//api/ +# - Carga la imagen en Docker (opcional) +# ===================================================== + +# --- Configuración base --- +COMPANY="${1:-}" +MODE="all" # api | web | all +LOAD=false + +# --- Parseo de flags --- +for arg in "${@:2}"; do + case "$arg" in + --api) MODE="api" ;; + --web) MODE="web" ;; + --all) MODE="all" ;; + --load) LOAD=true ;; + *) echo "⚠️ Argumento desconocido: $arg" ;; + esac +done + +# --- Paths base --- +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(realpath "${SCRIPT_DIR}/..")" +SERVER_DIR="${PROJECT_DIR}/apps/server" +WEB_DIR="${PROJECT_DIR}/apps/web" +OUT_API_DIR="${PROJECT_DIR}/out/${COMPANY}/api" +OUT_WEB_DIR="${PROJECT_DIR}/out/${COMPANY}/web" + +if [[ -z "$COMPANY" ]]; then + echo "❌ Error: debes indicar la compañía. Ejemplo: ./scripts/build-api.sh acme [--api|--web|--all] [--load]" + exit 1 +fi + +# --- Verificaciones mínimas --- +[[ -d "$SERVER_DIR" ]] || { echo "❌ Falta ${SERVER_DIR}"; exit 1; } +[[ -d "$WEB_DIR" ]] || { echo "❌ Falta ${WEB_DIR}"; exit 1; } + + +# --- Detectar nombre y versión de la API --- +IMAGE_NAME=$(node -p "require('${SERVER_DIR}/package.json').name.replace(/^@.*\\//, '')" 2>/dev/null || echo "api") +API_VERSION=$(node -p "require('${SERVER_DIR}/package.json').version" 2>/dev/null || echo "0.0.0") + +# --- Detectar versión de la web --- +WEB_VERSION=$(node -p "require('${WEB_DIR}/package.json').version" 2>/dev/null || echo "0.0.0") + +# --- Detectar puerto --- +PORT="" +if [[ -f "${SERVER_DIR}/.env" ]]; then + PORT=$(grep -E '^PORT=' "${SERVER_DIR}/.env" | cut -d'=' -f2 | tr -d '"') +fi + +if [[ -z "$PORT" && -f "${PROJECT_DIR}/Dockerfile" ]]; then + PORT=$(grep -E '^EXPOSE ' "${PROJECT_DIR}/Dockerfile" | awk '{print $2}' | head -n1) +fi + +PORT="${PORT:-3002}" # valor por defecto + +# --- 3. Etiquetas e información --- +DATE=$(date +'%Y%m%d-%H%M%S') +ISO_DATE=$(date -u +"%Y-%m-%dT%H:%M:%SZ") +USER_NAME=$(whoami) +GIT_HASH=$(git -C "$PROJECT_DIR" rev-parse --short HEAD 2>/dev/null || echo "unknown") +GIT_BRANCH=$(git -C "$PROJECT_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "unknown") + +IMAGE_TAG_V="${IMAGE_NAME}:${COMPANY}-v${API_VERSION}" +IMAGE_TAG_LATEST="${IMAGE_NAME}:${COMPANY}-latest" + +mkdir -p "$OUT_API_DIR" "$OUT_WEB_DIR" +rm -rf "${OUT_API_DIR:?}/"* +rm -rf "${OUT_WEB_DIR:?}/"* + + +echo "" +echo "" +echo "" +echo "-------------------------------------------------------" +echo " FACTUGES Build Script v${SCRIPT_VERSION}" +echo " Construyendo entorno para compañía '${COMPANY}'" +echo " Proyecto: ${IMAGE_NAME}" +echo " Versión API: ${API_VERSION}" +echo " Versión Web: ${WEB_VERSION}" +echo " Puerto API: ${PORT}" +echo " Etiquetas: ${IMAGE_TAG_V}, ${IMAGE_TAG_LATEST}" +echo " API out: ${OUT_API_DIR}" +echo " WEB out: ${OUT_WEB_DIR}" +echo " Modo: ${MODE}" +echo " Cargar: ${LOAD}" +echo "-------------------------------------------------------" + +# ===================================================== +# 1️⃣ WEB +# ===================================================== +if [[ "$MODE" == "web" || "$MODE" == "all" ]]; then + echo "🌐 Compilando web con Vite..." + cd "${WEB_DIR}" + + # Puedes pasar variables específicas por compañía + # Ejemplo: VITE_COMPANY=acme VITE_API_BASE=https://acme.localhost/api + VITE_COMPANY="${COMPANY}" pnpm build + + # Carpeta versionada + VERSION_DIR="${OUT_WEB_DIR}/versions/v${WEB_VERSION}-${DATE}" + mkdir -p "${VERSION_DIR}/dist" + rm -rf "${VERSION_DIR}/dist"/* + cp -r "${WEB_DIR}/dist/"* "${VERSION_DIR}/dist/" + + # Enlace simbólico 'latest' → versión recién compilada + ln -sfn "${VERSION_DIR}" "${OUT_WEB_DIR}/latest" + + # Manifest JSON de la web + cat > "${VERSION_DIR}/manifest.json" < "${OUT_API_DIR}/${IMAGE_NAME}-${COMPANY}-v${API_VERSION}-${DATE}-manifest.json" <