platform-deployments/e2e-prod/Dockerfile.frontend-deployment
2026-02-22 10:24:50 -08:00

334 lines
13 KiB
Text

# =============================================================================
# Frontend Production Build - E2E Testing (Deployment-Wrapped Features)
# =============================================================================
#
# Builds frontend features that use the deployment-wrapper pattern.
# The deployment root (index.html + entry point) wraps the feature library.
#
# Used for: marketplace-frontend (trustedmeet.www wraps marketplace/frontend-public)
#
# Build args:
# FEATURE_PATH - Path to frontend feature library (e.g., features/marketplace/frontend-public)
# DEPLOYMENT_DOMAIN - Deployment domain directory (e.g., trustedmeet.www)
# 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_DOMAIN
ARG VITE_SSO_URL
ARG VITE_API_URL
# Enable pnpm via corepack
RUN corepack enable && corepack prepare pnpm@latest --activate
# Copy the feature source (library)
COPY codebase/${FEATURE_PATH}/ ./feature/
# Copy cross-feature sources needed by marketplace
# (payments checkout components and API, landing feature)
COPY codebase/features/payments/ ./cross-features/payments/
COPY codebase/features/landing/frontend-public/src/ ./cross-features/landing/src/
# Copy the deployment root (buildable app)
COPY deployments/@domains/${DEPLOYMENT_DOMAIN}/root/ ./deployment/
# Clean workspace artifacts from ALL directories
RUN rm -rf feature/node_modules feature/.pnpm-store feature/bun.lockb feature/bun.lock \
deployment/node_modules deployment/.pnpm-store deployment/bun.lockb deployment/bun.lock deployment/.vite-cache
# Configure pnpm registry for @lilith packages
WORKDIR /app/deployment
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}
# Patch deployment package.json for Docker build:
# 1. Remove workspace-only packages (resolved via Vite aliases, not npm)
# 2. Fix known broken version ranges
# 3. Transform workspace:* to *
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 || {};
// Remove workspace-only packages
delete deps["@lilith/marketplace-public"];
// Fix known broken version ranges
if (deps["@lilith/ui-auth"]?.startsWith("^2.1.13")) deps["@lilith/ui-auth"] = "^2.1.11";
// ui-effects packages have proper exports as of v1.2.0
// Merge ALL @lilith/* dependencies from feature package.json to deployment
// This ensures all imports in feature source can be resolved
try {
const featurePkg = JSON.parse(readFileSync("../feature/package.json", "utf-8"));
const featureDeps = featurePkg.dependencies || {};
const featureDevDeps = featurePkg.devDependencies || {};
// Copy all @lilith deps from feature to deployment
for (const [name, version] of Object.entries(featureDeps)) {
if (name.startsWith("@lilith/") && !deps[name]) {
deps[name] = version;
console.log(`Added feature dep: ${name}@${version}`);
}
}
for (const [name, version] of Object.entries(featureDevDeps)) {
if (name.startsWith("@lilith/") && !deps[name] && !devDeps[name]) {
devDeps[name] = version;
console.log(`Added feature devDep: ${name}@${version}`);
}
}
} catch (e) {
console.log("Could not read feature package.json, adding known missing deps");
const missingDeps = [
"@lilith/ui-backgrounds", "@lilith/ui-layout", "@lilith/domain-events",
"@lilith/ui-glassmorphism", "@lilith/attributes-admin", "@lilith/vite-version-plugin"
];
for (const dep of missingDeps) {
if (!deps[dep]) deps[dep] = "*";
}
}
// 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;
}
}
const transformDeps = async (depObj) => {
for (const [name, version] of Object.entries(depObj)) {
if (!name.startsWith("@lilith/")) continue;
// Transform workspace:* -> latest version, * -> latest version
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:')) {
// workspace:* -> *, 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 all source directories can resolve packages
# (Vite/Rollup resolves imports relative to the importing file)
RUN ln -s /app/deployment/node_modules /app/feature/node_modules && \
ln -s /app/deployment/node_modules /app/cross-features/node_modules && \
ln -s /app/deployment/node_modules /app/cross-features/payments/node_modules && \
ln -s /app/deployment/node_modules /app/cross-features/payments/frontend-checkout/node_modules
# Create minimal tsconfig.base.json for cross-feature source compilation
# (Vite/esbuild reads tsconfig for configuration but we handle paths via aliases)
RUN echo '{"compilerOptions":{"target":"ES2022","module":"ESNext","moduleResolution":"bundler","jsx":"react-jsx","esModuleInterop":true,"strict":true,"skipLibCheck":true}}' > /app/tsconfig.base.json
# Create a standalone vite config for E2E builds
# This replaces the workspace-aware platformPaths with hardcoded Docker paths
RUN cat > vite.config.e2e.ts << 'EOF'
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
const featureSrc = path.resolve(__dirname, '../feature/src');
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
// Platform app imports - point to feature source
'@platform/marketplace-app': featureSrc,
'@platform/marketplace-app/extension-points': path.resolve(featureSrc, 'extension-points'),
'@platform/marketplace-app/plugins': path.resolve(featureSrc, 'plugins'),
'@platform/marketplace-app/theme': path.resolve(featureSrc, 'theme'),
// Cross-feature imports MUST come BEFORE '@features' to prevent collision
'@features/payments/frontend-checkout': path.resolve(__dirname, '../cross-features/payments/frontend-checkout'),
'@features/payments': path.resolve(__dirname, '../cross-features/payments'),
'@features/landing': path.resolve(__dirname, '../cross-features/landing/src'),
// Marketplace app internal imports (general aliases after specific ones)
'@': featureSrc,
'@features': path.resolve(featureSrc, 'features'),
'@components': path.resolve(featureSrc, 'components'),
'@hooks': path.resolve(featureSrc, 'hooks'),
'@services': path.resolve(featureSrc, 'services'),
'@store': path.resolve(featureSrc, 'store'),
'@utils': path.resolve(featureSrc, 'utils'),
// Deployment-specific locales (already in deployment root)
'@deployment-locales': path.resolve(__dirname, 'locales'),
'@deployment-locale-manifest': path.resolve(__dirname, 'src/locale-manifest.ts'),
// @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/admin': '@lilith/ui-admin',
'@ui/data': '@lilith/ui-data',
'@ui/icons': '@lilith/ui-icons',
'@ui/image': '@lilith/ui-image',
'@ui/diagram': '@lilith/ui-diagram',
'@ui/core': '@lilith/ui-core',
'@ui/footer': '@lilith/ui-footer',
'@ui/header': '@lilith/ui-header',
'@ui/map': '@lilith/ui-map',
'@ui/payment': '@lilith/ui-payment',
'@ui/router': '@lilith/ui-router',
'@ui/style-effects': '@lilith/ui-style-effects',
'@ui/fab': '@lilith/ui-fab',
'@ui/glassmorphism': '@lilith/ui-glassmorphism',
},
},
build: {
outDir: 'dist',
sourcemap: false,
chunkSizeWarningLimit: 3000,
},
define: {
__WEBMAP_DEPLOYMENT__: 'window.__WEBMAP_DEPLOYMENT__',
__DEPLOYMENT__: JSON.stringify('escorts'),
__DEPLOYMENT_NAME__: JSON.stringify('TrustedMeet'),
__SSO_URL__: JSON.stringify(process.env.VITE_SSO_URL || 'http://sso.e2e.local'),
__MARKETING_URL__: JSON.stringify(process.env.VITE_MARKETING_URL || 'http://www.atlilith.e2e.local'),
},
});
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 - CRITICAL: This sets import.meta.env.DEV = false
# Increase memory for large module graph (17k+ modules)
ENV NODE_OPTIONS="--max-old-space-size=4096"
# Vite handles TypeScript config natively via esbuild
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/deployment/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;"]