test.ts•3.15 kB
import http from 'node:http'
import { $ } from 'zx'
export const MONOREPO_APP_DIR_SERVER = '.next/standalone/packages/service/server.js'
export const DEFAULT_SERVER = '.next/standalone/server.js'
/**
 * Starts the Next.js server and tests the endpoint. It tests:
 * - No workaround + Server Components: if fails, nice error message
 * - No workaround + non-Server Components: if fails, nice error message
 * - Workaround + Server Components: should succeed
 * - Workaround + non-Server Components: should succeed
 * @param endpoint the endpoint to test
 */
async function test({ endpoint, server = DEFAULT_SERVER }: { endpoint: string; server?: string }) {
  console.log(`Testing ${endpoint} with WORKAROUND=${process.env.WORKAROUND}`)
  // prepare and start the next.js server
  const nextJsBuild = await $`pnpm exec next build`.nothrow()
  await $`rm -fr .next/standalone/node_modules/next`.nothrow()
  const nextJsProcess = $`HOSTNAME=127.0.0.1 node ${server}`.nothrow()
  // wait for the server to be fully ready
  for await (const line of nextJsProcess.stdout) {
    if (line.includes('Ready in')) break
  }
  // attempt to query the endpoint with curl
  const code = await getHttpCode(endpoint)
  // kill and proceed with test assertions
  await nextJsProcess.kill('SIGINT')
  // Path 1: No workaround + a nice error message
  if (process.env.WORKAROUND !== 'true' && (code === 500 || nextJsBuild.exitCode !== 0)) {
    // Dual logic: server components error at build & runtime, non-Server components at runtime
    // this is also why we use `.nothrow()` and only check for exit codes as well as http codes
    const stderr = nextJsBuild.stderr + (await nextJsProcess).stderr // dual logic
    const message = `We detected that you are using Next.js, learn how to fix this: https://pris.ly/d/engine-not-found-nextjs`
    if (stderr.includes(message) === false) {
      throw new Error(`Expected an error message starting with "${message}" but got "${stderr}"`)
    }
    return
  }
  // Path 2: Workaround + no error message
  if (process.env.WORKAROUND === 'true' && code === 200) {
    return
  }
  // Path 3: Otherwise, it should succeed
  if (code !== 200) {
    throw new Error(`Expected 200 but got ${code}`)
  }
}
export async function getHttpCode(endpoint: string): Promise<number | undefined> {
  return new Promise((resolve, reject) => {
    http
      .get(new URL(endpoint, 'http://localhost:3000'), (res) => {
        res.resume() // consume body
        res.on('end', () => resolve(res.statusCode))
      })
      .on('error', (error) => reject(error))
  })
}
type TestOptions = {
  monorepo?: boolean
}
export function testServerComponents(options?: TestOptions) {
  return testEndpoint('test/42', options)
}
export async function testNonServerComponents(options?: TestOptions) {
  return testEndpoint('api/test', options)
}
async function testEndpoint(endpoint, { monorepo = true }: TestOptions = {}) {
  const server = monorepo ? MONOREPO_APP_DIR_SERVER : DEFAULT_SERVER
  process.env.WORKAROUND = 'true'
  await test({ endpoint, server })
  process.env.WORKAROUND = 'false'
  await test({ endpoint, server })
}