Ctrl K
ComunidadGuías…

scripts/serve-backend-playwright.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { spawn, spawnSync } from 'node:child_process'
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import process, { env } from 'node:process'
import { getPlaywrightStripeApiBaseUrl } from './playwright-stripe'
import { getSupabaseWorktreeConfig } from './supabase-worktree-config'
import { getSupabaseStatus, type SupabaseStatus } from './supabase-worktree-status'

const repoRoot = process.cwd()
const sourceEnvPath = resolve(repoRoot, 'supabase/functions/.env')
const generatedEnvPath = resolve(repoRoot, '.context/playwright/supabase-functions.playwright.env')
const supabaseConfig = getSupabaseWorktreeConfig(repoRoot)

const stripeApiBaseUrl = getPlaywrightStripeApiBaseUrl(env)
const webAppUrl = env.WEBAPP_URL || 'http://localhost:5173'
const functionsReadyTimeoutMs = Number(env.PLAYWRIGHT_BACKEND_TIMEOUT_MS || '360000')
// Comma-separated Supabase services to skip (`supabase start -x`). CI skips the ones E2E
// never touches so a cold runner pulls fewer images before the stack is healthy.
const supabaseStartExclude = env.PLAYWRIGHT_SUPABASE_EXCLUDE?.trim()

function upsertEnvValue(content: string, key: string, value: string): string {
  const escapedKey = key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
  const matcher = new RegExp(`^${escapedKey}=.*$`, 'm')
  const line = `${key}=${value}`

  if (matcher.test(content))
    return content.replace(matcher, line)

  return content.endsWith('\n') || content.length === 0
    ? `${content}${line}\n`
    : `${content}\n${line}\n`
}

const baseEnv = existsSync(sourceEnvPath) ? readFileSync(sourceEnvPath, 'utf8') : ''
const overriddenEnv = [
  ['ENV', env.ENV || 'local'],
  ['S3_ENDPOINT', `127.0.0.1:${supabaseConfig.ports.api}/storage/v1/s3`],
  ['STRIPE_SECRET_KEY', env.STRIPE_SECRET_KEY || 'sk_test_emulator'],
  ['STRIPE_SECRET_KEY_US', env.STRIPE_SECRET_KEY_US || env.STRIPE_SECRET_KEY || 'sk_test_emulator'],
  ['STRIPE_API_BASE_URL', stripeApiBaseUrl],
  ['STRIPE_WEBHOOK_SECRET', env.STRIPE_WEBHOOK_SECRET || 'testsecret'],
  ['STRIPE_WEBHOOK_SECRET_US', env.STRIPE_WEBHOOK_SECRET_US || env.STRIPE_WEBHOOK_SECRET || 'testsecret'],
  ['WEBAPP_URL', webAppUrl],
] as const

function sleep(ms: number): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, ms))
}

function hasHealthySupabaseApi(status: SupabaseStatus | null) {
  return Boolean(
    status?.API_URL
    && (status?.ANON_KEY || status?.PUBLISHABLE_KEY)
    && (status?.SERVICE_ROLE_KEY || status?.SECRET_KEY),
  )
}

function stopSupabase() {
  spawnSync('bun', ['run', 'supabase:stop'], {
    cwd: repoRoot,
    stdio: 'inherit',
    env: process.env,
  })
}

function stopExistingPlaywrightBackend() {
  spawnSync('pkill', ['-f', 'supabase-functions.playwright.env'], {
    cwd: repoRoot,
    stdio: 'ignore',
    env: process.env,
  })
}

async function resetSupabaseDb() {
  const maxAttempts = 3

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    const resetResult = spawnSync('bun', ['run', 'supabase:db:reset'], {
      cwd: repoRoot,
      stdio: 'inherit',
      env: process.env,
    })

    if ((resetResult.status ?? 1) === 0)
      return

    stopSupabase()

    if (attempt === maxAttempts)
      process.exit(resetResult.status ?? 1)

    await sleep(attempt * 2000)
    await ensureSupabaseStarted()
  }
}

async function ensureSupabaseStarted() {
  const maxAttempts = 4

  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
    if (hasHealthySupabaseApi(getSupabaseStatus()))
      return

    const startResult = spawnSync('bun', ['run', 'supabase:start', ...(supabaseStartExclude ? ['-x', supabaseStartExclude] : [])], {
      cwd: repoRoot,
      stdio: 'inherit',
      env: process.env,
    })

    if ((startResult.status ?? 1) === 0 && hasHealthySupabaseApi(getSupabaseStatus()))
      return

    stopSupabase()

    if (attempt === maxAttempts)
      process.exit(startResult.status ?? 1)

    await sleep(attempt * 2000)
  }
}

async function waitForFunctionsReady(timeoutMs: number) {
  const apiUrl = getSupabaseStatus()?.API_URL || `http://127.0.0.1:${supabaseConfig.ports.api}`
  // The console's first paint now goes through the `private` function
  // (/private/console/query), so warm it too instead of only the `ok` probe.
  const targetUrls = [`${apiUrl}/functions/v1/ok`, `${apiUrl}/functions/v1/private/config`]
  const startedAt = Date.now()

  for (const targetUrl of targetUrls) {
    let ready = false
    while (!ready && Date.now() - startedAt < timeoutMs) {
      try {
        const response = await fetch(targetUrl)
        ready = response.ok
      }
      catch {
        // Keep polling until the edge runtime serves requests again.
      }
      if (!ready)
        await sleep(1000)
    }
    if (!ready)
      throw new Error(`Timed out waiting for Supabase functions at ${targetUrl}`)
  }
}

async function stopChildProcess(child: ReturnType<typeof spawn>, signal: NodeJS.Signals = 'SIGTERM') {
  if (child.exitCode !== null)
    return

  const exitPromise = new Promise<void>((resolve) => {
    child.once('exit', () => resolve())
  })

  child.kill(signal)
  await Promise.race([
    exitPromise,
    sleep(5000).then(() => {
      if (child.exitCode === null)
        child.kill('SIGKILL')
      return exitPromise
    }),
  ])
}

let envFileContent = baseEnv
for (const [key, value] of overriddenEnv)
  envFileContent = upsertEnvValue(envFileContent, key, value)

mkdirSync(dirname(generatedEnvPath), { recursive: true })
writeFileSync(generatedEnvPath, envFileContent)

stopExistingPlaywrightBackend()
await ensureSupabaseStarted()

// Playwright E2E expects the seeded schema helpers and deterministic fixture data.
if (!env.SKIP_SUPABASE_DB_RESET) {
  await resetSupabaseDb()
  await ensureSupabaseStarted()
}

const child = spawn('bun', ['scripts/supabase-worktree.ts', 'functions', 'serve', '--env-file', generatedEnvPath], {
  cwd: repoRoot,
  stdio: 'inherit',
  env: process.env,
})

let childStarted = false

try {
  await waitForFunctionsReady(functionsReadyTimeoutMs)
  childStarted = true
}
finally {
  if (!childStarted)
    await stopChildProcess(child)
}

if (env.PLAYWRIGHT_READY_FILE) {
  mkdirSync(dirname(env.PLAYWRIGHT_READY_FILE), { recursive: true })
  writeFileSync(env.PLAYWRIGHT_READY_FILE, 'ready\n')
}

const signalHandlers = new Map<NodeJS.Signals, () => void>()

function forwardSignal(signal: NodeJS.Signals) {
  const handler = () => {
    child.kill(signal)
  }
  signalHandlers.set(signal, handler)
  process.on(signal, handler)
}

forwardSignal('SIGINT')
forwardSignal('SIGTERM')

child.on('exit', (code, signal) => {
  if (signal) {
    for (const [registeredSignal, handler] of signalHandlers)
      process.off(registeredSignal, handler)
    process.kill(process.pid, signal)
    return
  }
  process.exit(code ?? 0)
})