334 lines
13 KiB
Text
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;"]
|