import { RequestResponse } from '@dooboostore/simple-boot-http-server/models/RequestResponse'; import { HttpHeaders } from '@dooboostore/simple-boot-http-server/codes/HttpHeaders'; import { Filter } from '@dooboostore/simple-boot-http-server/filters/Filter'; import { Mimes } from '@dooboostore/simple-boot-http-server/codes/Mimes'; import { HttpStatus } from '@dooboostore/simple-boot-http-server/codes/HttpStatus'; import { SimpleBootHttpServer } from '@dooboostore/simple-boot-http-server/SimpleBootHttpServer'; import path from 'path'; import fs from 'fs'; import {HttpMethod} from "@dooboostore/simple-boot-http-server"; export type SWCSSRConfig = { frontDistPath: string; frontDistIndexFileName?: string; welcomUrl?: string; ssrExcludeFilter?: (rr: RequestResponse) => boolean; /** * (Deprecated for Playwright) * Function to register components for each request. * Playwright evaluates real JS files loaded from your index.html, * so component registration happens automatically in the browser context. */ registerComponents?: (window: any) => Promise | void; playwright?: { waitUntil?: 'load' | 'domcontentloaded' | 'networkidle' | 'commit'; waitForSelector?: string; waitForTimeout?: number; timeout?: number; ignoreTags?: string[]; }; }; /** * SSR Filter specifically for Simple Web Component (SWC). * It utilizes Playwright to render the page robustly and perfectly matching browser specs. * * ### Playwright Setup & Requirements * * To use this filter, Playwright must be installed in your server environment: * 1. Install playwright package: `npm install playwright` or `pnpm add playwright` * 2. Install browsers: `npx playwright install chromium` * * If running inside a Docker container (e.g., Alpine Linux), you must use an image that supports Playwright * or manually install OS dependencies. Playwright provides official Docker images: `mcr.microsoft.com/playwright:v1.x.x-focal`. */ export class SSRSimpleWebComponentFilter implements Filter { private welcomUrl = 'http://localhost'; private browser?: any; constructor(public config: SWCSSRConfig) { this.welcomUrl = config.welcomUrl || this.welcomUrl; } async onInit(app: SimpleBootHttpServer) { // 1. Initialize Playwright browser when the server starts const { chromium } = await import('playwright'); this.browser = await chromium.launch({ headless: true, args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-web-security'] // Allow CORS for local dev }); console.log('๐Ÿš€ Playwright Browser initialized for SSR Filter'); } async onDestroy() { if (this.browser) { await this.browser.close(); console.log('๐Ÿ›‘ Playwright Browser closed'); } } async proceedBefore({ rr, app, carrier }: { rr: RequestResponse; app: SimpleBootHttpServer; carrier: Map }) { if (this.config.ssrExcludeFilter?.(rr)) { return true; // Bypass SSR and continue server chain } console.log('playwright2222222', rr.reqHeader('x-ssr-browser')); // 2. Prevent Infinite Loop: // If the request comes from Playwright itself, bypass SSR and serve the original static SPA file. if (rr.reqHeader('x-ssr-browser') === 'playwright') { return true; } if (rr.reqIsMethod(HttpMethod.GET) && rr.reqHasContentTypeHeader(Mimes.TextHtml)) { // Build target URL for Playwright to visit // Use the server's actual listening address and port if available const protocol = app.option.protocol || 'http'; const host = app.option.hostname || '127.0.0.1'; const port = app.option.port || 80; const url = rr.reqUrlObj({ host: `${host}:${port}` }); url.protocol = protocol; const targetUrl = url.toString() ?? this.welcomUrl; if (!this.browser) { console.error('Playwright browser is not initialized. Bypassing SSR.'); return true; } // Create a fresh context for isolation // IMPORTANT: We do NOT use `extraHTTPHeaders` globally because it attaches the header to EVERY request // Playwright makes (including CORS requests to FontAwesome or API requests). // Instead, we will set the header ONLY for the initial navigation request to our server. const context = await this.browser.newContext(); const page = await context.newPage(); try { // Intercept ONLY requests going to our local server and append the bypass header await page.route('**/*', route => { const request = route.request(); // Only append the x-ssr-browser header for requests to the local server that are NOT API requests const isLocal = request.url().startsWith(targetUrl) || request.url().includes('localhost') || request.url().includes('127.0.0.1'); const isApi = request.url().includes('/api/'); if (isLocal && !isApi) { const headers = { ...request.headers(), 'x-ssr-browser': 'playwright' }; route.continue({ headers }).catch(() => {}); } else { // For external requests (like font-awesome CORS) or API calls, do not add the custom header route.continue().catch(() => {}); } }); // 3. Navigate to the page and wait for the framework to finish rendering // but normally Playwright just navigates to the targetUrl and loads the real SPA resources. // ๐Ÿ’ก Playwright โ†” Framework ํ†ต์‹  ์„ค์ • // ํ”„๋ก ํŠธ์—”๋“œ์˜ `bootfactory.ts` ์•ˆ์˜ `onRouteChanged` ๋“ฑ์—์„œ // `if (window.onPlaywrightReady) window.onPlaywrightReady()` ๋ผ๊ณ  ํ˜ธ์ถœํ•ด์ฃผ๋ฉด, // ์•„๋ž˜์˜ Promise๊ฐ€ ์ฆ‰์‹œ resolve ๋˜๋„๋ก ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค. let resolvePlaywrightReady: () => void; const playwrightReadyPromise = new Promise(resolve => { resolvePlaywrightReady = resolve; }); await page.exposeFunction('onPlaywrightReady', () => { console.log('โšก๏ธ [SSR:Playwright] Received "onPlaywrightReady" signal from framework!'); resolvePlaywrightReady(); }); // 3. Navigate to the page and wait for the framework to finish rendering // Since the server will return the `index.html` (due to the bypass header), Playwright will download // your main.js / bundle.js, evaluate it, and the Custom Elements will register themselves naturally! await page.goto(targetUrl, { waitUntil: this.config.playwright?.waitUntil ?? 'networkidle', timeout: this.config.playwright?.timeout ?? 15000 }); // ๐Ÿ’ก Framework Load Wait (Crucial for SWC) // Wait for EITHER the framework to call `window.onPlaywrightReady()` OR a specific innerHTML condition await Promise.race([ playwrightReadyPromise, // ์ง์ ‘ exposeFunction์—์„œ ํŠธ๋ฆฌ๊ฑฐ๋œ Promise page.waitForFunction( () => { // Fallback 1: If explicit signal variable is set return (window as any)._playwrightReadySignalFired === true; }, { timeout: this.config.playwright?.timeout ?? 15000 } ), page.waitForFunction( () => { // Fallback 2: If no signal, just wait until body has content return document.body.innerHTML.trim().length > 100; }, { timeout: this.config.playwright?.timeout ?? 15000 } ) ]).catch(() => console.log('Wait for app mount timeout... proceeding anyway')); // ๊ฐ•์ œ๋กœ ์ปดํฌ๋„ŒํŠธ ๋ Œ๋”๋ง ๋ฐ ํฐํŠธ์–ด์ธ ๋“ฑ ๋ฆฌ์†Œ์Šค ๋กœ๋”ฉ ๋Œ€๊ธฐ ์‹œ๊ฐ„ ์ถ”๊ฐ€ (์•ˆ์ „ ์žฅ์น˜) // onPlaywrightReady ์‹ ํ˜ธ๊ฐ€ ๋–จ์–ด์ ธ๋„, ๋น„๋™๊ธฐ ๋ Œ๋”๋ง(SwcLoop ๋“ฑ)์ด DOM์— ์™„์ „ํžˆ ๋ฐ˜์˜๋˜๋Š” ๋ฐ // ์•ฝ๊ฐ„์˜ Microtask ์ง€์—ฐ์ด ์žˆ์„ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์•„์ฃผ ์งง๊ฒŒ๋งŒ ๊ธฐ๋‹ค๋ ค ์ค๋‹ˆ๋‹ค. // await new Promise(resolve => setTimeout(resolve, 300)); // Optional: Wait for a specific component to be visible if (this.config.playwright?.waitForSelector) { await page.waitForSelector(this.config.playwright.waitForSelector, { timeout: this.config.playwright?.timeout ?? 15000 }); } // Optional: Hardcoded wait time if explicitly set (fallback) if (this.config.playwright?.waitForTimeout) { await page.waitForTimeout(this.config.playwright.waitForTimeout); } // 4. Set SSR Attribute before extracting HTML await page.evaluate(() => { document.body.setAttribute('ssr-use', 'true'); }); // 5. Generate Final HTML const html = await page.evaluate(ignoreTags => { const skipTags = new Set(ignoreTags || []); function serializeNode(node: Node, inBody: boolean = false): string { if (node.nodeType === Node.TEXT_NODE) { const text = node.textContent || ''; return text.replace(/&/g, '&').replace(//g, '>'); } if (node.nodeType === Node.COMMENT_NODE) { return ``; } if (node.nodeType !== Node.ELEMENT_NODE) { return ''; } const el = node as Element; const tag = el.tagName.toLowerCase(); const isBody = tag === 'body'; const isInsideBody = inBody || isBody; // ๐Ÿšซ Skip ignored tags (like