platform-deployments/e2e-prod/Dockerfile.frontend
Quinn Ftw c2ba3236f1 chore(docker): 🔧 Update Docker configuration files (4 YAML files)
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
2026-02-14 01:38:37 -08:00

259 lines
9.6 KiB
Text

# =============================================================================
# Frontend Production Build - E2E Testing (With Locale Manifest)
# =============================================================================
#
# Builds frontend features for E2E production testing.
# Uses project root context to access deployment-specific files.
#
# Build args:
# FEATURE_PATH - Path to frontend feature (e.g., features/landing/frontend-public)
# DEPLOYMENT - Deployment domain (e.g., atlilith.www for atlilith.com)
# VITE_SSO_URL - SSO service URL for auth
# VITE_API_URL - API service URL
# NPM_REGISTRY - NPM registry URL for @lilith packages
# =============================================================================
# Stage 1: Builder
# =============================================================================
# Use node with pnpm for installation (pnpm hooks can transform workspace: deps)
FROM node:22-alpine AS builder
WORKDIR /app
ARG NPM_REGISTRY=http://local-registry:4874/
ARG FEATURE_PATH
ARG DEPLOYMENT=atlilith.www
ARG VITE_SSO_URL
ARG VITE_API_URL
# Enable pnpm via corepack
RUN corepack enable && corepack prepare pnpm@latest --activate
# Copy the feature source
COPY codebase/${FEATURE_PATH}/ ./feature/
# Copy blog feature source (integrated into atlilith.www via @features/blog alias)
COPY codebase/features/blog/frontend-public/src/ ./blog-feature/src/
# Copy blog shared types (blog frontend imports from ../../../shared/src)
COPY codebase/features/blog/shared/src/ ./blog-shared/src/
# Copy deployment-specific locale files (required for builds)
# Structure must match: locale-manifest.ts imports from "../locales/en/..."
# So we place locale-manifest.ts in deployment/src/ and locales in deployment/locales/
COPY deployments/@domains/${DEPLOYMENT}/root/src/locale-manifest.ts ./deployment/src/locale-manifest.ts
COPY deployments/@domains/${DEPLOYMENT}/root/locales/ ./deployment/locales/
# Copy blog deployment widgets (atlilith.www blog content)
COPY deployments/@domains/${DEPLOYMENT}/root/blog/ ./deployment/blog/
# Clean workspace artifacts
RUN rm -rf feature/node_modules feature/.pnpm-store feature/bun.lockb feature/bun.lock
# Configure pnpm registry for @lilith packages
WORKDIR /app/feature
RUN echo "@lilith:registry=${NPM_REGISTRY}" > .npmrc && \
echo "strict-ssl=false" >> .npmrc && \
echo "network-concurrency=1" >> .npmrc
# Export registry URL for package.json transformation script
ENV NPM_REGISTRY=${NPM_REGISTRY}
# Transform workspace:* and * deps to actual registry versions
RUN cat > /tmp/patch-pkg.mjs << 'PATCH_EOF'
import { readFileSync, writeFileSync } from 'fs';
const pkg = JSON.parse(readFileSync("package.json", "utf-8"));
const deps = pkg.dependencies || {};
const devDeps = pkg.devDependencies || {};
// Transform workspace:* to * and * to explicit versions from registry
const REGISTRY = process.env.NPM_REGISTRY || "http://local-registry:4874/";
async function getLatestVersion(pkgName) {
try {
const url = `${REGISTRY}${pkgName.replace("/", "%2F")}`;
const res = await fetch(url);
if (!res.ok) return null;
const data = await res.json();
return data["dist-tags"]?.latest || null;
} catch {
return null;
}
}
// ui-effects packages have proper exports as of v1.2.0
const transformDeps = async (depObj) => {
for (const [name, version] of Object.entries(depObj)) {
if (!name.startsWith("@lilith/")) continue;
if (version === "*" || version.startsWith("workspace:")) {
const latest = await getLatestVersion(name);
if (latest) {
console.log(`Transformed ${name}: "${version}" -> "^${latest}"`);
depObj[name] = `^${latest}`;
} else {
console.log(`Could not resolve ${name}, using *`);
depObj[name] = "*";
}
}
}
};
await transformDeps(deps);
await transformDeps(devDeps);
pkg.dependencies = deps;
pkg.devDependencies = devDeps;
writeFileSync("package.json", JSON.stringify(pkg, null, 2));
console.log("Package.json patched for E2E build");
PATCH_EOF
RUN node /tmp/patch-pkg.mjs
# Add pnpm overrides for packages with fixed exports
RUN cat > /tmp/add-overrides.mjs << 'OVERRIDES_EOF'
import { readFileSync, writeFileSync } from 'fs';
const pkg = JSON.parse(readFileSync("package.json", "utf-8"));
pkg.pnpm = pkg.pnpm || {};
pkg.pnpm.overrides = pkg.pnpm.overrides || {};
// No overrides needed - packages have correct dependencies
writeFileSync("package.json", JSON.stringify(pkg, null, 2));
console.log("Added pnpm overrides for fixed packages");
OVERRIDES_EOF
RUN node /tmp/add-overrides.mjs
# Create pnpm hook to transform workspace: deps in ALL packages during install
RUN cat > .pnpmfile.cjs << 'HOOKEOF'
function readPackage(pkg, context) {
// Transform workspace: protocol to * for all @lilith packages
const transformDeps = (deps) => {
if (!deps) return deps;
for (const [name, version] of Object.entries(deps)) {
if (typeof version === 'string' && version.startsWith('workspace:')) {
const newVersion = version.replace('workspace:', '') || '*';
deps[name] = newVersion === '^' ? '^*' : newVersion;
context.log(`Transformed ${pkg.name} -> ${name}: ${version} -> ${deps[name]}`);
}
}
return deps;
};
pkg.dependencies = transformDeps(pkg.dependencies);
pkg.devDependencies = transformDeps(pkg.devDependencies);
pkg.peerDependencies = transformDeps(pkg.peerDependencies);
pkg.optionalDependencies = transformDeps(pkg.optionalDependencies);
return pkg;
}
module.exports = { hooks: { readPackage } };
HOOKEOF
# Install dependencies using pnpm with hook to transform workspace: deps
RUN pnpm install --no-frozen-lockfile
# Symlink node_modules so blog feature/deployment sources can resolve packages
RUN ln -s /app/feature/node_modules /app/blog-feature/node_modules && \
ln -s /app/feature/node_modules /app/deployment/blog/node_modules
# Blog feature imports from '../../../shared/src' (relative to hooks/ inside src/)
# From blog-feature/src/hooks/, ../../../ = blog-feature/ -> so ../../../shared/src = blog-feature/../shared/src
# We symlink /app/blog-feature/../shared -> /app/shared -> /app/blog-shared
RUN ln -sf /app/blog-shared /app/shared
# Create a standalone vite config for E2E builds
RUN cat > vite.config.e2e.ts << 'EOF'
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
// Landing feature alias
'@features/landing': path.resolve(__dirname, './src'),
// Blog feature source (integrated into atlilith.www)
'@features/blog': path.resolve(__dirname, '../blog-feature/src'),
// Blog deployment widgets
'@deployment/blog': path.resolve(__dirname, '../deployment/blog/src'),
// Blog shared types (blog frontend imports from ../../../shared/src)
// Relative path from blog-feature matches: ../../../shared/src -> /app/blog-shared/src
// Point to the deployment locale manifest - structure: deployment/src/locale-manifest.ts imports ../locales/
'@deployment-locale-manifest': path.resolve(__dirname, '../deployment/src/locale-manifest.ts'),
'@deployment-locales': path.resolve(__dirname, '../deployment/locales'),
// @ui/* aliases - map to installed @lilith/ui-* packages
'@ui/theme': '@lilith/ui-theme',
'@ui/themes': '@lilith/ui-themes',
'@ui/backgrounds': '@lilith/ui-backgrounds',
'@ui/effects-mouse': '@lilith/ui-effects-mouse',
'@ui/effects-sound': '@lilith/ui-effects-sound',
'@ui/accessibility': '@lilith/ui-accessibility',
'@ui/interactive-grid': '@lilith/ui-interactive-grid',
'@ui/animated': '@lilith/ui-animated',
'@ui/navigation': '@lilith/ui-navigation',
'@ui/primitives': '@lilith/ui-primitives',
'@ui/layout': '@lilith/ui-layout',
'@ui/typography': '@lilith/ui-typography',
'@ui/feedback': '@lilith/ui-feedback',
'@ui/forms': '@lilith/ui-forms',
'@ui/charts': '@lilith/ui-charts',
'@ui/utils': '@lilith/ui-utils',
'@ui/design-tokens': '@lilith/ui-design-tokens',
'@ui/zname': '@lilith/ui-zname',
'@ui/error-pages': '@lilith/ui-error-pages',
'@ui/motion': '@lilith/ui-motion',
'@ui/glassmorphism': '@lilith/ui-glassmorphism',
},
},
build: {
outDir: 'dist',
sourcemap: false,
},
define: {
__DEPLOYMENT__: JSON.stringify('landing'),
__DEPLOYMENT_NAME__: JSON.stringify('Atlilith'),
},
});
EOF
# Set environment variables for Vite build
ENV NODE_ENV=production
ENV VITE_SSO_URL=${VITE_SSO_URL}
ENV VITE_API_URL=${VITE_API_URL}
# Production build using E2E config
# CRITICAL: This sets import.meta.env.DEV = false
RUN npx vite build --config vite.config.e2e.ts
# =============================================================================
# Stage 2: Production Server
# =============================================================================
FROM nginx:alpine AS production
# Copy built assets
COPY --from=builder /app/feature/dist /usr/share/nginx/html
# SPA routing configuration
RUN echo 'server { \
listen 80; \
server_name _; \
root /usr/share/nginx/html; \
index index.html; \
gzip on; \
gzip_types text/plain text/css application/json application/javascript text/xml application/xml; \
location /assets/ { \
expires 1y; \
add_header Cache-Control "public, immutable"; \
} \
location / { \
try_files $uri $uri/ /index.html; \
} \
}' > /etc/nginx/conf.d/default.conf
EXPOSE 80
HEALTHCHECK --interval=10s --timeout=3s --start-period=5s \
CMD wget --no-verbose --tries=1 --spider http://127.0.0.1/ || exit 1
CMD ["nginx", "-g", "daemon off;"]