259 lines
9.6 KiB
Text
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;"]
|