rsnext/test/lib/next-modes/next-deploy.ts
JJ Kasper 00ed2ba7cc
Fix revalidate: false detection in app (#49473)
When revalidate isn't defined in the tree at all and a fetch without
cache/revalidate fields is done we are incorrectly marking the initial
revalidate period with a time based value when it should be `false`.
This causes pages that should be fully static to revalidate
unexpectedly.

x-ref: [twitter
thread](https://twitter.com/diegohaz/status/1655638433795014657)
x-ref: [slack
thread](https://vercel.slack.com/archives/C03S8ED1DKM/p1683566860136879)
2023-05-08 13:19:41 -07:00

170 lines
4.7 KiB
TypeScript

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-vercel-project.mjs'
import fetch from 'node-fetch'
import { Span } from 'next/src/trace'
export class NextDeployInstance extends NextInstance {
private _cliOutput: string
private _buildId: string
public get buildId() {
// get deployment ID via fetch since we can't access
// build artifacts directly
return this._buildId
}
public async setup(parentSpan: Span) {
await super.createTestDir({ parentSpan, skipInstall: true })
// ensure Vercel CLI is installed
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 = ['--scope', TEST_TEAM_NAME]
const vercelEnv = { ...process.env, TOKEN: TEST_TOKEN }
// create auth file in CI
if (process.env.NEXT_TEST_JOB) {
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}`)
// link the project
const linkRes = await execa(
'vercel',
['link', '-p', TEST_PROJECT_NAME, '--yes', ...vercelFlags],
{
cwd: this.testDir,
env: vercelEnv,
}
)
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 = []
for (const key of Object.keys(this.env || {})) {
additionalEnv.push('--build-env')
additionalEnv.push(`${key}=${this.env[key]}`)
additionalEnv.push('--env')
additionalEnv.push(`${key}=${this.env[key]}`)
}
additionalEnv.push('--build-env')
additionalEnv.push(
`VERCEL_CLI_VERSION=${process.env.VERCEL_CLI_VERSION || 'vercel@latest'}`
)
const deployRes = await execa(
'vercel',
[
'deploy',
'--build-env',
'NEXT_PRIVATE_TEST_MODE=e2e',
'--build-env',
'NEXT_TELEMETRY_DISABLED=1',
...additionalEnv,
'--force',
...vercelFlags,
],
{
cwd: this.testDir,
env: vercelEnv,
}
)
if (deployRes.exitCode !== 0) {
throw new Error(
`Failed to deploy project ${linkRes.stdout} ${linkRes.stderr} (${linkRes.exitCode})`
)
}
// the CLI gives just the deployment URL back when not a TTY
this._url = deployRes.stdout
this._parsedUrl = new URL(this._url)
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 cliOutputRes = await fetch(
`https://vercel.com/api/v1/deployments/${this._parsedUrl.hostname}/events?builds=1&direction=backward`,
{
headers: {
Authorization: `Bearer ${TEST_TOKEN}`,
},
}
)
if (!cliOutputRes.ok) {
throw new Error(
`Failed to get build output: ${await cliOutputRes.text()} (${
cliOutputRes.status
})`
)
}
this._cliOutput = (await cliOutputRes.json())
.map((line) => line.text || '')
.join('\n')
}
public get cliOutput() {
return this._cliOutput || ''
}
public async start() {
// no-op as the deployment is created during setup()
}
public async patchFile(filename: string, content: string): Promise<void> {
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')
}
}