diff --git a/Dockerfile b/Dockerfile index dd2a048..a51ac5d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,8 +7,10 @@ COPY . . RUN npx ng build --configuration production # ── Serve stage ──────────────────────────────────────────────────────── -FROM docker.io/library/nginx:alpine +FROM docker.io/library/node:22-alpine +RUN apk add --no-cache nginx gettext COPY --from=build /app/dist/radio-station/browser /usr/share/nginx/html +COPY --from=build /app/dist/radio-station/server /app/server COPY nginx.conf.template /etc/nginx/conf.d/nginx.conf.template COPY config.json.template /usr/share/nginx/html/config.json.template COPY docker-entrypoint.sh /docker-entrypoint.sh diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh index c2f7d6e..79b85f9 100644 --- a/docker-entrypoint.sh +++ b/docker-entrypoint.sh @@ -1,11 +1,36 @@ #!/bin/sh -# Render config templates with environment variables, then start nginx. +# Start the Angular SSR Node process, then nginx. set -e # Ensure nginx can write to the shared logs directory mkdir -p /logs && chown nginx:nginx /logs -sed "s|__API_UPSTREAM__|${API_UPSTREAM}|g" /etc/nginx/conf.d/nginx.conf.template > /etc/nginx/conf.d/default.conf +# Render nginx config from template to /etc/nginx/nginx.conf +# (full config, not conf.d/ snippet — avoids Alpine include nesting issues) +sed "s|__API_UPSTREAM__|${API_UPSTREAM}|g" /etc/nginx/conf.d/nginx.conf.template > /etc/nginx/nginx.conf + +# Render client-side config.json for fallback use envsubst '${API_PUBLIC_URL}' < /usr/share/nginx/html/config.json.template > /usr/share/nginx/html/config.json +# Start the Angular SSR server (Node process) in the background +# Point at nginx (port 80) so SSR API calls go through the nginx proxy chain +# to the upstream backend. Nginx is started after SSR so the chain is complete +# by the time the first SSR request actually needs API data. +export SSR_API_BASE_URL="http://localhost:80" +echo "[entrypoint] Starting Angular SSR server on port 4200..." +node /app/server/server.mjs & +SSR_PID=$! + +# Wait for the SSR server to be ready (up to 10 seconds) +echo "[entrypoint] Waiting for SSR server to be ready..." +for i in $(seq 1 20); do + if node -e "require('http').get('http://localhost:4200/', (r) => { process.exit(r.statusCode === 200 ? 0 : 1) }).on('error', () => process.exit(1))" 2>/dev/null; then + echo "[entrypoint] SSR server is ready." + break + fi + sleep 0.5 +done + +# Start nginx in the foreground (primary process) +echo "[entrypoint] Starting nginx..." exec nginx -g 'daemon off;' diff --git a/nginx.conf.template b/nginx.conf.template index 742c83d..0d61aba 100644 --- a/nginx.conf.template +++ b/nginx.conf.template @@ -1,30 +1,47 @@ -# Map ISO timestamp to date-only string for daily log rotation -map $time_iso8601 $log_date { - "~^(?\d{4}-\d{2}-\d{2})" $date; - default "unknown"; +# Full nginx configuration — rendered to /etc/nginx/nginx.conf +# (NOT placed in conf.d/ to avoid include nesting issues with Alpine nginx) + +user nginx; +worker_processes auto; +error_log /var/log/nginx/error.log warn; +pid /var/run/nginx.pid; + +events { + worker_connections 1024; } -log_format kmtn_json escape=json - '{' - '"time":"$time_iso8601",' - '"remote_addr":"$remote_addr",' - '"x_forwarded_for":"$http_x_forwarded_for"' - '}'; +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; -access_log /logs/access-$log_date.log kmtn_json; + log_format kmtn_json escape=json + '{' + '"time":"$time_iso8601",' + '"remote_addr":"$remote_addr",' + '"x_forwarded_for":"$http_x_forwarded_for"' + '}'; -server { + access_log /logs/access.log kmtn_json; + sendfile on; + + upstream ssr_backend { + server 127.0.0.1:4200; + } + + server { listen 80; server_name _; root /usr/share/nginx/html; index index.html; - # SPA fallback — Angular router handles its own paths - location / { - try_files $uri $uri/ /index.html; + # Serve static assets directly from disk (hashed filenames are immutable) + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|map)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + access_log off; } - # Dynamic sitemap.xml + robots.txt — proxied to the backend so each + # Dynamic sitemap.xml + robots.txt - proxied to the backend so each # white-label tenant gets their KMTN_WEBSITE_URL in the output. location = /sitemap.xml { proxy_pass __API_UPSTREAM__/api/sitemap/sitemap.xml; @@ -53,12 +70,28 @@ server { proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; proxy_set_header X-Forwarded-Host $host; - # Disable caching through the double-proxy chain (NPM → frontend nginx → API) - # Without this, browsers cache responses keyed by the HTTPS origin, then send - # conditional requests (If-None-Match) that get lost or mismatched in the proxy chain. + # Disable caching through the double-proxy chain proxy_cache off; add_header Cache-Control "no-store, no-cache, must-revalidate" always; add_header Pragma "no-cache" always; add_header Vary "*" always; } + + # Proxy all page requests to the Angular SSR server. + # The SSR server (AngularNodeAppEngine) handles both RenderMode.Server + # routes (full on-demand rendering with fresh API data) and serves + # prerendered static HTML for RenderMode.Prerender routes. + location / { + proxy_pass http://ssr_backend; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $http_x_forwarded_proto; + proxy_set_header X-Forwarded-Host $host; + + # SSR responses should not be cached by intermediate proxies + proxy_cache off; + add_header X-Powered-By "Angular SSR" always; + } +} } diff --git a/server.spec.ts b/server.spec.ts index 0f8d0d7..0a6fd09 100644 --- a/server.spec.ts +++ b/server.spec.ts @@ -4,7 +4,7 @@ import '@angular/compiler'; import { describe, it, expect, vi, beforeEach } from 'vitest'; // Mock the @angular/ssr/node imports that pull in Node.js http server code -// — we only want to test PRERENDERED_ROUTES and isPrerenderedRoute here. +// — we only want to test the request handler logic here. vi.mock('@angular/ssr/node', () => ({ AngularNodeAppEngine: class {}, createNodeRequestHandler: (fn: Function) => fn, @@ -16,75 +16,160 @@ vi.mock('node:http', () => ({ default: { createServer: vi.fn() }, })); -import { PRERENDERED_ROUTES, isPrerenderedRoute } from './server'; - describe('SSR server — server.ts', () => { - describe('PRERENDERED_ROUTES', () => { - it('should contain exactly 5 routes', () => { - expect(PRERENDERED_ROUTES).toHaveLength(5); - }); - - it('should include the home route', () => { - expect(PRERENDERED_ROUTES).toContain('/'); - }); - - it('should include /about', () => { - expect(PRERENDERED_ROUTES).toContain('/about'); - }); - - it('should include /schedule', () => { - expect(PRERENDERED_ROUTES).toContain('/schedule'); - }); - - it('should include /events', () => { - expect(PRERENDERED_ROUTES).toContain('/events'); - }); - - it('should include /contact', () => { - expect(PRERENDERED_ROUTES).toContain('/contact'); - }); + it('should export a default request handler', async () => { + const mod = await import('./server'); + expect(mod.default).toBeDefined(); }); - describe('isPrerenderedRoute()', () => { - it('should match exact prerendered paths', () => { - expect(isPrerenderedRoute('/')).toBe(true); - expect(isPrerenderedRoute('/about')).toBe(true); - expect(isPrerenderedRoute('/schedule')).toBe(true); - expect(isPrerenderedRoute('/events')).toBe(true); - expect(isPrerenderedRoute('/contact')).toBe(true); - }); + it('should not export PRERENDERED_ROUTES (all routes now use RenderMode.Server)', async () => { + const mod = await import('./server'); + // The server.ts file no longer exports PRERENDERED_ROUTES or isPrerenderedRoute + // because all routes use on-demand SSR. Verify the file doesn't reference them. + const fs = require('fs'); + const content = fs.readFileSync('./server.ts', 'utf-8'); + expect(content).not.toContain('PRERENDERED_ROUTES'); + expect(content).not.toContain('isPrerenderedRoute'); + expect(content).not.toContain('AngularAppEngine'); + }); - it('should match paths with trailing slashes', () => { - expect(isPrerenderedRoute('/about/')).toBe(true); - expect(isPrerenderedRoute('/schedule/')).toBe(true); - expect(isPrerenderedRoute('/events/')).toBe(true); - expect(isPrerenderedRoute('/contact/')).toBe(true); - }); - - it('should match paths with query strings', () => { - expect(isPrerenderedRoute('/about?foo=bar')).toBe(true); - expect(isPrerenderedRoute('/schedule?day=monday')).toBe(true); - }); - - it('should match paths with fragments', () => { - expect(isPrerenderedRoute('/events#upcoming')).toBe(true); - }); - - it('should reject non-prerendered routes', () => { - expect(isPrerenderedRoute('/login')).toBe(false); - expect(isPrerenderedRoute('/admin')).toBe(false); - expect(isPrerenderedRoute('/nonexistent')).toBe(false); - expect(isPrerenderedRoute('/admin/dashboard')).toBe(false); - }); - - it('should handle full URLs', () => { - expect(isPrerenderedRoute('https://kmtnflower.org/about')).toBe(true); - expect(isPrerenderedRoute('http://localhost:4200/events')).toBe(true); - expect(isPrerenderedRoute('https://kmtnflower.org/login')).toBe(false); - }); - - it('should normalise root path — "/" stays "/"', () => { - expect(isPrerenderedRoute('/')).toBe(true); - }); + it('should have a clean server with lazy engine bootstrapping', async () => { + const fs = require('fs'); + const content = fs.readFileSync('./server.ts', 'utf-8'); + expect(content).toContain('getEngine'); + expect(content).toContain('AngularNodeAppEngine'); + expect(content).toContain('listen(4200'); + }); +}); + +describe('app.server.routes.ts — RenderMode.Server for all routes', () => { + it('should use RenderMode.Server for all public routes (fresh data on every request)', async () => { + const fs = require('fs'); + const content = fs.readFileSync('./src/app/app.server.routes.ts', 'utf-8'); + + // Every route should use RenderMode.Server + const prerenderMatches = content.match(/RenderMode\.Prerender/g); + expect(prerenderMatches).toBeNull(); + + // Should have RenderMode.Server entries + const serverMatches = content.match(/RenderMode\.Server/g); + expect(serverMatches).not.toBeNull(); + expect(serverMatches.length).toBeGreaterThanOrEqual(8); + }); + + it('should have no Prerender routes (eliminates stale baked-in DEFAULT_CONFIG)', async () => { + const fs = require('fs'); + const content = fs.readFileSync('./src/app/app.server.routes.ts', 'utf-8'); + expect(content).not.toContain('RenderMode.Prerender'); + }); +}); + +describe('docker-entrypoint.sh — SSR + nginx lifecycle', () => { + it('should be executable', () => { + const fs = require('fs'); + const stat = fs.statSync('./docker-entrypoint.sh'); + expect(stat.isFile()).toBe(true); + }); + + it('should start the SSR node process before nginx', () => { + const fs = require('fs'); + const content = fs.readFileSync('./docker-entrypoint.sh', 'utf-8'); + // SSR must start before nginx + const ssrStart = content.indexOf('node /app/server/server.mjs'); + const nginxStart = content.indexOf('exec nginx'); + expect(ssrStart).toBeGreaterThan(-1); + expect(nginxStart).toBeGreaterThan(-1); + expect(ssrStart).toBeLessThan(nginxStart); + }); + + it('should set SSR_API_BASE_URL for the SSR server', () => { + const fs = require('fs'); + const content = fs.readFileSync('./docker-entrypoint.sh', 'utf-8'); + expect(content).toContain('SSR_API_BASE_URL'); + expect(content).toContain('http://localhost:80'); + }); + + it('should wait for SSR server readiness before starting nginx', () => { + const fs = require('fs'); + const content = fs.readFileSync('./docker-entrypoint.sh', 'utf-8'); + expect(content).toContain('Waiting for SSR server to be ready'); + expect(content).toContain('localhost:4200'); + }); +}); + +describe('Dockerfile — multi-stage build with SSR server', () => { + it('should copy the server bundle in addition to browser output', () => { + const fs = require('fs'); + const content = fs.readFileSync('./Dockerfile', 'utf-8'); + expect(content).toMatch(/COPY.*server.*\/app\/server/); + }); + + it('should use a Node.js base image for the serve stage', () => { + const fs = require('fs'); + const content = fs.readFileSync('./Dockerfile', 'utf-8'); + // Second FROM should be Node-based (not plain nginx) + const fromLines = content.split('\n').filter(l => l.startsWith('FROM ')); + expect(fromLines.length).toBeGreaterThanOrEqual(2); + expect(fromLines[1]).toContain('node'); + }); + + it('should install nginx in the serve stage', () => { + const fs = require('fs'); + const content = fs.readFileSync('./Dockerfile', 'utf-8'); + expect(content).toContain('apk add --no-cache nginx'); + }); + + it('should expose port 80', () => { + const fs = require('fs'); + const content = fs.readFileSync('./Dockerfile', 'utf-8'); + expect(content).toContain('EXPOSE 80'); + }); +}); + +describe('nginx.conf.template — SSR proxy chain', () => { + it('should define an upstream pointing to SSR server on port 4200', () => { + const fs = require('fs'); + const content = fs.readFileSync('./nginx.conf.template', 'utf-8'); + expect(content).toContain('upstream ssr_backend'); + expect(content).toContain('127.0.0.1:4200'); + }); + + it('should proxy page requests to the SSR backend', () => { + const fs = require('fs'); + const content = fs.readFileSync('./nginx.conf.template', 'utf-8'); + expect(content).toContain('proxy_pass http://ssr_backend'); + }); + + it('should serve static assets directly from disk with long cache', () => { + const fs = require('fs'); + const content = fs.readFileSync('./nginx.conf.template', 'utf-8'); + expect(content).toContain('location ~*'); + expect(content).toContain('js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot|map'); + expect(content).toContain('expires 1y'); + expect(content).toContain('public, immutable'); + }); + + it('should still proxy /api/ to the upstream backend', () => { + const fs = require('fs'); + const content = fs.readFileSync('./nginx.conf.template', 'utf-8'); + expect(content).toContain('location /api/'); + expect(content).toContain('__API_UPSTREAM__'); + }); +}); + +describe('main.server.ts — SSR API base URL injection', () => { + it('should override APP_CONFIG with SSR-aware apiBaseUrl', () => { + const fs = require('fs'); + const content = fs.readFileSync('./src/main.server.ts', 'utf-8'); + expect(content).toContain('SSR_API_BASE_URL'); + expect(content).toContain('ssrAppConfig'); + expect(content).toContain('provide: APP_CONFIG'); + }); + + it('should read SSR_API_BASE_URL from environment with fallback', () => { + const fs = require('fs'); + const content = fs.readFileSync('./src/main.server.ts', 'utf-8'); + expect(content).toContain("process.env['SSR_API_BASE_URL']"); + expect(content).toContain("http://localhost:80"); }); }); diff --git a/server.ts b/server.ts index 78cba80..f4e84c7 100644 --- a/server.ts +++ b/server.ts @@ -4,72 +4,25 @@ import { isMainModule, writeResponseToNodeResponse, } from '@angular/ssr/node'; -import { AngularAppEngine } from '@angular/ssr'; import * as http from 'node:http'; -// Enable serving prerendered static HTML from disk. Without this flag the -// engine falls back to SSR for every route, ignoring the prerendered output. -AngularAppEngine.ɵallowStaticRouteRender = true; - /** - * Public routes prerendered at build time (SSG — Static Site Generation). - * These five routes are compiled to static HTML during `ng build` and - * served directly from disk at runtime for optimal performance and SEO. - * The list here mirrors `prerender-routes.txt` and the Prerender entries - * in `src/app/app.server.routes.ts`. + * Manually import and register the Angular app engine manifest. + * The Angular 21 builder generates angular-app-engine-manifest.mjs at build time + * but does NOT automatically inject the import into server.mjs. + * Without this, AngularNodeAppEngine fails with "Angular app engine manifest is not set" + * and every SSR request returns 404. */ -export const PRERENDERED_ROUTES: readonly string[] = [ - '/', - '/about', - '/schedule', - '/events', - '/contact', -]; +// @ts-ignore — generated at build time by @angular/build +import engineManifest from './angular-app-engine-manifest.mjs'; +import { ɵsetAngularAppEngineManifest } from '@angular/ssr'; +ɵsetAngularAppEngineManifest(engineManifest); -/** - * Normalise a request pathname and check against prerendered routes. - * Handles both bare paths (`/about`) and trailing-slash variants (`/about/`). - */ -export function isPrerenderedRoute(url: string): boolean { - const pathname = new URL(url, 'http://localhost').pathname; - const normalised = pathname.endsWith('/') && pathname !== '/' - ? pathname.slice(0, -1) - : pathname; - return PRERENDERED_ROUTES.includes(normalised); -} +const appEngine = new AngularNodeAppEngine(); -/** - * Lazily instantiate AngularNodeAppEngine so the engine boots only when - * the first request arrives. The engine handles both prerendered route - * lookups and on-demand SSR bootstrapping internally. - */ -let appEngine: AngularNodeAppEngine | null = null; -function getEngine(): AngularNodeAppEngine { - if (!appEngine) { - appEngine = new AngularNodeAppEngine(); - } - return appEngine; -} - -/** - * Handle incoming requests using the Angular SSR engine. - * - * Prerendered routes (/ , /about, /schedule, /events, /contact) are served - * as pre-generated static HTML from the build output. All other routes - * fall back to server-side rendering (SSR) or the fallback 404 handler. - */ const requestHandler = createNodeRequestHandler(async (req, res, next) => { try { - const prerendered = isPrerenderedRoute(req.url || '/'); - if (prerendered) { - console.log(`[SSR] Prerendered route: ${req.url}`); - } - - // AngularNodeAppEngine handles manifest registration internally — - // no manual bootstrap import needed. For prerendered routes it - // returns pre-generated static HTML; for SSR routes it bootstraps - // the Angular app on demand. - const response = await getEngine().handle(req); + const response = await appEngine.handle(req); if (response) { await writeResponseToNodeResponse(response, res); @@ -81,17 +34,24 @@ const requestHandler = createNodeRequestHandler(async (req, res, next) => { } }); -export default requestHandler; - if (isMainModule(import.meta.url)) { - const httpServer = http.createServer(); - httpServer.on('request', async (req: http.IncomingMessage, res: http.ServerResponse) => { - requestHandler(req, res, () => { - res.statusCode = 404; - res.end('Not Found'); - }); + console.log('Node.js SSR server ready'); + const server = http.createServer(); + server.on('request', async (req, res) => { + try { + await requestHandler(req, res, () => { + res.statusCode = 404; + res.end('Not Found'); + }); + } catch (error) { + console.error('SSR error:', error); + res.statusCode = 500; + res.end('Internal server error'); + } }); - httpServer.listen(4200, () => { - console.log(`Node SSR server listening on http://localhost:4200`); + server.listen(4200, () => { + console.log('SSR server listening on http://localhost:4200'); }); } + +export { requestHandler }; diff --git a/src/app/app.server.routes.spec.ts b/src/app/app.server.routes.spec.ts index 2550460..be8c041 100644 --- a/src/app/app.server.routes.spec.ts +++ b/src/app/app.server.routes.spec.ts @@ -10,34 +10,34 @@ describe('SSR server routes — app.server.routes.ts', () => { expect(serverRoutes).toHaveLength(8); }); - it('should prerender the home route', () => { + it('should use SSR mode for the home route', () => { const home = serverRoutes.find((r) => r.path === ''); expect(home).toBeDefined(); - expect(home!.renderMode).toBe(RenderMode.Prerender); + expect(home!.renderMode).toBe(RenderMode.Server); }); - it('should prerender /about', () => { + it('should use SSR mode for /about', () => { const about = serverRoutes.find((r) => r.path === 'about'); expect(about).toBeDefined(); - expect(about!.renderMode).toBe(RenderMode.Prerender); + expect(about!.renderMode).toBe(RenderMode.Server); }); - it('should prerender /schedule', () => { + it('should use SSR mode for /schedule', () => { const schedule = serverRoutes.find((r) => r.path === 'schedule'); expect(schedule).toBeDefined(); - expect(schedule!.renderMode).toBe(RenderMode.Prerender); + expect(schedule!.renderMode).toBe(RenderMode.Server); }); - it('should prerender /events', () => { + it('should use SSR mode for /events', () => { const events = serverRoutes.find((r) => r.path === 'events'); expect(events).toBeDefined(); - expect(events!.renderMode).toBe(RenderMode.Prerender); + expect(events!.renderMode).toBe(RenderMode.Server); }); - it('should prerender /contact', () => { + it('should use SSR mode for /contact', () => { const contact = serverRoutes.find((r) => r.path === 'contact'); expect(contact).toBeDefined(); - expect(contact!.renderMode).toBe(RenderMode.Prerender); + expect(contact!.renderMode).toBe(RenderMode.Server); }); it('should use SSR mode for /login', () => { @@ -58,13 +58,13 @@ describe('SSR server routes — app.server.routes.ts', () => { expect(wildcard!.renderMode).toBe(RenderMode.Server); }); - it('should have exactly 5 prerendered routes', () => { + it('should have no prerendered routes (eliminates stale DEFAULT_CONFIG)', () => { const prerendered = serverRoutes.filter((r) => r.renderMode === RenderMode.Prerender); - expect(prerendered).toHaveLength(5); + expect(prerendered).toHaveLength(0); }); - it('should have exactly 3 SSR routes', () => { + it('should have exactly 8 SSR routes (all routes)', () => { const ssr = serverRoutes.filter((r) => r.renderMode === RenderMode.Server); - expect(ssr).toHaveLength(3); + expect(ssr).toHaveLength(8); }); }); diff --git a/src/app/app.server.routes.ts b/src/app/app.server.routes.ts index 0a729c4..6b91f1b 100644 --- a/src/app/app.server.routes.ts +++ b/src/app/app.server.routes.ts @@ -3,16 +3,18 @@ import { RenderMode, ServerRoute } from '@angular/ssr'; /** * Server routes configuration for Angular SSR. * - * The five public routes are prerendered at build time (SSG) for optimal SEO. - * Authenticated routes (/login, /admin) use SSR for dynamic rendering. - * All other routes fall back to SSR. + * ALL routes use RenderMode.Server — every request hits the SSR server, + * which fetches fresh station config from the API before rendering HTML. + * This eliminates the stale-asset flash that occurred with prerendered HTML + * (prerender runs at build time when the API is unavailable, baking in + * DEFAULT_CONFIG values). */ export const serverRoutes: ServerRoute[] = [ - { path: '', renderMode: RenderMode.Prerender }, - { path: 'about', renderMode: RenderMode.Prerender }, - { path: 'schedule', renderMode: RenderMode.Prerender }, - { path: 'events', renderMode: RenderMode.Prerender }, - { path: 'contact', renderMode: RenderMode.Prerender }, + { path: '', renderMode: RenderMode.Server }, + { path: 'about', renderMode: RenderMode.Server }, + { path: 'schedule', renderMode: RenderMode.Server }, + { path: 'events', renderMode: RenderMode.Server }, + { path: 'contact', renderMode: RenderMode.Server }, { path: 'login', renderMode: RenderMode.Server }, { path: 'admin', renderMode: RenderMode.Server }, { path: '**', renderMode: RenderMode.Server }, diff --git a/src/main.server.ts b/src/main.server.ts index ee77f70..3b69a4d 100644 --- a/src/main.server.ts +++ b/src/main.server.ts @@ -4,13 +4,20 @@ import { provideServerRendering, withRoutes } from '@angular/ssr'; import { appConfig } from './app/app.config'; import { App } from './app/app'; +import { APP_CONFIG, AppConfig } from './app/services/app-config.service'; import { serverRoutes } from './app/app.server.routes'; +// During SSR, the API base URL must be absolute (HttpClient runs in Node.js). +// nginx proxies /api/ to the upstream, so point at nginx (port 80) locally. +const ssrApiBaseUrl = process.env['SSR_API_BASE_URL'] || 'http://localhost:80'; +const ssrAppConfig: AppConfig = { apiBaseUrl: ssrApiBaseUrl }; + const serverAppConfig = { ...appConfig, providers: [ ...appConfig.providers, { provide: APP_ID, useValue: 'server' }, + { provide: APP_CONFIG, useValue: ssrAppConfig }, provideServerRendering(withRoutes(serverRoutes)), ], }; diff --git a/vitest.config.ts b/vitest.config.ts index a0708a6..18925f9 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -3,7 +3,7 @@ export default { test: { globals: true, environment: 'jsdom', - include: ['src/**/*.spec.ts'], + include: ['src/**/*.spec.ts', 'server.spec.ts'], setupFiles: ['src/test-setup.ts'], reporters: ['default'], coverage: {