| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146 |
- import { spawn, ChildProcess } from "child_process";
- import { execSync } from "child_process";
- import path from "path";
- import fs from "fs";
- import http from "http";
- import { createAssetsSymlink, generateAdConfig } from "./configGenerator";
- const TEMPLATE_DIR = path.resolve(__dirname, "../../../../templates/coloring");
- const PREVIEW_PORT = 5199;
- let viteProcess: ChildProcess | null = null;
- let currentCreativeId: string | null = null;
- /**
- * 等待 HTTP 服务就绪
- */
- function waitForReady(url: string, maxRetries = 15): Promise<void> {
- return new Promise((resolve, reject) => {
- let tries = 0;
- function check() {
- http.get(url, (res) => {
- if (res.statusCode === 200) resolve();
- else retry();
- }).on("error", retry);
- }
- function retry() {
- if (++tries >= maxRetries) {
- reject(new Error(`Preview server did not start within ${maxRetries}s`));
- return;
- }
- setTimeout(check, 1000);
- }
- check();
- });
- }
- /**
- * 启动实时预览。等待 Vite dev server 就绪后才返回。
- */
- export async function startPreview(
- creativeId: string,
- theme: Record<string, string>,
- storageDir: string
- ): Promise<{ url: string }> {
- // 1. 停止旧的预览(如果有)
- stopPreview();
- // 2. 创建 symlink
- createAssetsSymlink(creativeId, storageDir);
- // 3. 生成配置
- const configContent = generateAdConfig({ creativeId, theme, storageDir });
- const configPath = path.join(TEMPLATE_DIR, "src", "filler", "_ad_config_.ts");
- fs.writeFileSync(configPath, configContent, "utf-8");
- // 4. 启动 Vite dev server
- // base 路径通过 PREVIEW_BASE_PATH 环境变量传入 vite.config.js
- // 注意:nginx proxy_pass 不能有尾部斜杠,以保留 /ads-preview/ 前缀
- const previewBase = process.env.PREVIEW_BASE_PATH || "/";
- console.log(`[preview] Starting Vite dev server on port ${PREVIEW_PORT} (base: ${previewBase})...`);
- viteProcess = spawn(path.join(TEMPLATE_DIR, "node_modules", ".bin", "vite"), [
- "--port", String(PREVIEW_PORT),
- "--strictPort",
- ], {
- cwd: TEMPLATE_DIR,
- env: {
- ...process.env,
- AD_CONFIG_PATH: "src/filler/_ad_config_.ts",
- },
- stdio: ["ignore", "pipe", "pipe"],
- });
- viteProcess.stdout?.on("data", (data: Buffer) => {
- console.log(`[preview:vite] ${data.toString().trim()}`);
- });
- viteProcess.stderr?.on("data", (data: Buffer) => {
- console.log(`[preview:vite] ${data.toString().trim()}`);
- });
- viteProcess.on("exit", (code) => {
- console.log(`[preview] Vite dev server exited (code ${code})`);
- viteProcess = null;
- currentCreativeId = null;
- });
- currentCreativeId = creativeId;
- // 5. 等待 Vite 就绪
- // Vite 有 base 路径时,根路径可能 404,需要用带 prefix 的 URL 检查
- const checkUrl = previewBase !== "/"
- ? `http://localhost:${PREVIEW_PORT}${previewBase}`
- : `http://localhost:${PREVIEW_PORT}`;
- console.log(`[preview] Waiting for Vite to be ready (checking ${checkUrl})...`);
- await waitForReady(checkUrl);
- console.log("[preview] Vite is ready.");
- // 生产环境通过 nginx 代理暴露公网 URL,本地开发直接用 localhost
- const localUrl = `http://localhost:${PREVIEW_PORT}`;
- const publicUrl = process.env.PREVIEW_PUBLIC_URL || localUrl;
- return { url: publicUrl };
- }
- /**
- * 更新预览配置(主题变更时调用)。Vite HMR 会自动检测并刷新页面。
- */
- export function updatePreviewConfig(
- creativeId: string,
- theme: Record<string, string>,
- storageDir: string
- ): void {
- if (currentCreativeId !== creativeId) return;
- const configContent = generateAdConfig({ creativeId, theme, storageDir });
- const configPath = path.join(TEMPLATE_DIR, "src", "filler", "_ad_config_.ts");
- fs.writeFileSync(configPath, configContent, "utf-8");
- console.log(`[preview] Config updated for creative ${creativeId}`);
- }
- /**
- * 停止预览
- */
- export function stopPreview(): void {
- if (viteProcess) {
- console.log("[preview] Stopping Vite dev server...");
- try {
- viteProcess.kill("SIGTERM");
- } catch {
- // ignore
- }
- viteProcess = null;
- }
- currentCreativeId = null;
- // 确保端口释放
- try {
- execSync(`lsof -ti :${PREVIEW_PORT} | xargs kill -9 2>/dev/null`, { stdio: "ignore" });
- } catch {}
- }
- export function getPreviewStatus(): { active: boolean; creativeId: string | null; url: string | null } {
- return {
- active: viteProcess !== null && currentCreativeId !== null,
- creativeId: currentCreativeId,
- url: currentCreativeId ? `http://localhost:${PREVIEW_PORT}` : null,
- };
- }
|