| import os from 'os' |
| import path from 'path' |
| import execa from 'execa' |
| import fs from 'fs-extra' |
| import { NextInstance } from './base' |
| import { |
| TEST_PROJECT_NAME, |
| TEST_TEAM_NAME, |
| TEST_TOKEN, |
| } from '../../../scripts/reset-project.mjs' |
| import fetch from 'node-fetch' |
| import { Span } from 'next/dist/trace' |
|
|
| export class NextDeployInstance extends NextInstance { |
| private _cliOutput: string |
| private _buildId: string |
| private _writtenHostsLine: string | null = null |
|
|
| protected throwIfUnavailable(): void | never { |
| if (this.isStopping !== null) { |
| throw new Error('Next.js is no longer available.', { |
| cause: this.isStopping, |
| }) |
| } |
| if (this.isDestroyed !== null) { |
| throw new Error('Next.js is no longer available.', { |
| cause: this.isDestroyed, |
| }) |
| } |
| if (this.childProcess === undefined) { |
| |
| } |
| } |
|
|
| public get buildId() { |
| |
| |
| return this._buildId |
| } |
|
|
| public async setup(parentSpan: Span) { |
| super.setup(parentSpan) |
| await super.createTestDir({ parentSpan, skipInstall: true }) |
|
|
| |
| try { |
| const res = await execa('vercel', ['--version']) |
| require('console').log(`Using Vercel CLI version:`, res.stdout) |
| } catch (_) { |
| require('console').log(`Installing Vercel CLI`) |
| await execa('npm', ['i', '-g', 'vercel@latest'], { |
| stdio: 'inherit', |
| }) |
| } |
|
|
| const vercelFlags: string[] = [] |
|
|
| |
| if (TEST_TEAM_NAME) { |
| vercelFlags.push('--scope', TEST_TEAM_NAME) |
| } |
|
|
| const vercelEnv = { ...process.env } |
|
|
| |
| |
| if (TEST_TOKEN) { |
| vercelEnv.TOKEN = TEST_TOKEN |
| } |
|
|
| |
| if (process.env.NEXT_TEST_JOB) { |
| if (!TEST_TOKEN && !TEST_TEAM_NAME) { |
| throw new Error( |
| 'Missing TEST_TOKEN and TEST_TEAM_NAME environment variables for CI' |
| ) |
| } |
|
|
| const vcConfigDir = path.join(os.homedir(), '.vercel') |
| await fs.ensureDir(vcConfigDir) |
| await fs.writeFile( |
| path.join(vcConfigDir, 'auth.json'), |
| JSON.stringify({ token: TEST_TOKEN }) |
| ) |
| vercelFlags.push('--global-config', vcConfigDir) |
| } |
| require('console').log(`Linking project at ${this.testDir}`) |
|
|
| |
| const linkRes = await execa( |
| 'vercel', |
| ['link', '-p', TEST_PROJECT_NAME, '--yes', ...vercelFlags], |
| { |
| cwd: this.testDir, |
| env: vercelEnv, |
| reject: false, |
| } |
| ) |
|
|
| if (linkRes.exitCode !== 0) { |
| throw new Error( |
| `Failed to link project ${linkRes.stdout} ${linkRes.stderr} (${linkRes.exitCode})` |
| ) |
| } |
| require('console').log(`Deploying project at ${this.testDir}`) |
|
|
| const additionalEnv: string[] = [] |
|
|
| for (const key of Object.keys(this.env || {})) { |
| additionalEnv.push(`${key}=${this.env[key]}`) |
| } |
|
|
| additionalEnv.push( |
| `VERCEL_CLI_VERSION=${process.env.VERCEL_CLI_VERSION || 'vercel@latest'}` |
| ) |
|
|
| |
|
|
| if (process.env.__NEXT_CACHE_COMPONENTS) { |
| additionalEnv.push( |
| `NEXT_PRIVATE_EXPERIMENTAL_CACHE_COMPONENTS=${process.env.__NEXT_CACHE_COMPONENTS}` |
| ) |
| } |
|
|
| if (process.env.IS_TURBOPACK_TEST) { |
| additionalEnv.push(`IS_TURBOPACK_TEST=1`) |
| } |
| if (process.env.IS_WEBPACK_TEST) { |
| additionalEnv.push(`IS_WEBPACK_TEST=1`) |
| } |
|
|
| const deployRes = await execa( |
| 'vercel', |
| [ |
| 'deploy', |
| '--build-env', |
| 'NEXT_PRIVATE_TEST_MODE=e2e', |
| '--build-env', |
| 'NEXT_TELEMETRY_DISABLED=1', |
| '--build-env', |
| 'VERCEL_NEXT_BUNDLED_SERVER=1', |
| ...additionalEnv.flatMap((pair) => [ |
| '--env', |
| pair, |
| '--build-env', |
| pair, |
| ]), |
| '--force', |
| ...vercelFlags, |
| ], |
| { |
| cwd: this.testDir, |
| env: vercelEnv, |
| reject: false, |
| |
| |
| |
| stderr: 'inherit', |
| } |
| ) |
|
|
| if (deployRes.exitCode !== 0) { |
| throw new Error( |
| `Failed to deploy project ${deployRes.stdout} ${deployRes.stderr} (${deployRes.exitCode})` |
| ) |
| } |
|
|
| |
| this._url = deployRes.stdout |
| this._parsedUrl = new URL(this._url) |
|
|
| |
| |
| if ( |
| process.env.NEXT_TEST_PROXY_ADDRESS && |
| |
| /^\d+\.\d+\.\d+\.\d+$/.test(process.env.NEXT_TEST_PROXY_ADDRESS) |
| ) { |
| this._writtenHostsLine = `${process.env.NEXT_TEST_PROXY_ADDRESS}\t${this._parsedUrl.hostname}\n` |
|
|
| require('console').log( |
| `Writing proxy address to hosts file: ${this._writtenHostsLine.trim()}` |
| ) |
|
|
| |
| |
| await execa('sudo', ['tee', '-a', '/etc/hosts'], { |
| input: this._writtenHostsLine, |
| stdout: 'inherit', |
| shell: true, |
| }) |
|
|
| |
| const hostsFile = await fs.readFile('/etc/hosts', 'utf8') |
| if (!hostsFile.includes(this._writtenHostsLine)) { |
| throw new Error('Proxy address not found in hosts file after writing') |
| } |
|
|
| require('console').log(`Proxy address written to hosts file`) |
| } |
|
|
| require('console').log(`Deployment URL: ${this._url}`) |
| const buildIdUrl = `${this._url}${ |
| this.basePath || '' |
| }/_next/static/__BUILD_ID` |
|
|
| const buildIdRes = await fetch(buildIdUrl) |
|
|
| if (!buildIdRes.ok) { |
| require('console').error( |
| `Failed to load buildId ${buildIdUrl} (${buildIdRes.status})` |
| ) |
| } |
| this._buildId = (await buildIdRes.text()).trim() |
|
|
| require('console').log(`Got buildId: ${this._buildId}`) |
|
|
| |
| const buildLogs = await execa( |
| 'vercel', |
| ['inspect', '--logs', this._url, ...vercelFlags], |
| { |
| env: vercelEnv, |
| reject: false, |
| } |
| ) |
| if (buildLogs.exitCode !== 0) { |
| throw new Error(`Failed to get build output logs: ${buildLogs.stderr}`) |
| } |
|
|
| |
| |
|
|
| |
| |
| this._cliOutput = buildLogs.stdout + buildLogs.stderr |
| } |
|
|
| public async destroy() { |
| |
| if (this._writtenHostsLine) { |
| const trimmed = this._writtenHostsLine.trim() |
|
|
| require('console').log( |
| `Removing proxy address from hosts file: ${this._writtenHostsLine.trim()}` |
| ) |
|
|
| const hostsFile = await fs.readFile('/etc/hosts', 'utf8') |
|
|
| const cleanedHostsFile = hostsFile |
| .split('\n') |
| .filter((line) => line.trim() !== trimmed) |
| .join('\n') |
|
|
| await execa('sudo', ['tee', '/etc/hosts'], { |
| input: cleanedHostsFile, |
| stdout: 'inherit', |
| shell: true, |
| }) |
|
|
| require('console').log(`Removed proxy address from hosts file`) |
| } |
|
|
| |
| return super.destroy() |
| } |
|
|
| public get cliOutput() { |
| return this._cliOutput || '' |
| } |
|
|
| public async start() { |
| |
| } |
|
|
| public async patchFile( |
| filename: string, |
| content: string |
| ): Promise<{ newFile: boolean }> { |
| throw new Error('patchFile is not available in deploy test mode') |
| } |
| public async readFile(filename: string): Promise<string> { |
| throw new Error('readFile is not available in deploy test mode') |
| } |
| public async deleteFile(filename: string): Promise<void> { |
| throw new Error('deleteFile is not available in deploy test mode') |
| } |
| public async renameFile( |
| filename: string, |
| newFilename: string |
| ): Promise<void> { |
| throw new Error('renameFile is not available in deploy test mode') |
| } |
| } |
|
|