2017-05-10 01:24:34 +02:00
|
|
|
import express from 'express'
|
2021-08-03 17:06:26 +02:00
|
|
|
import {
|
|
|
|
existsSync,
|
|
|
|
readFileSync,
|
|
|
|
unlinkSync,
|
|
|
|
writeFileSync,
|
|
|
|
createReadStream,
|
|
|
|
} from 'fs'
|
2023-06-23 19:42:50 +02:00
|
|
|
import { promisify } from 'util'
|
2020-06-11 20:29:38 +02:00
|
|
|
import http from 'http'
|
|
|
|
import path from 'path'
|
2023-06-23 19:42:50 +02:00
|
|
|
|
|
|
|
import spawn from 'cross-spawn'
|
|
|
|
import { writeFile } from 'fs-extra'
|
|
|
|
import getPort from 'get-port'
|
2023-09-11 23:48:29 +02:00
|
|
|
import { getRandomPort } from 'get-port-please'
|
2023-06-23 19:42:50 +02:00
|
|
|
import fetch from 'node-fetch'
|
2020-06-11 20:29:38 +02:00
|
|
|
import qs from 'querystring'
|
|
|
|
import treeKill from 'tree-kill'
|
2024-01-16 18:25:49 +01:00
|
|
|
import { once } from 'events'
|
2017-01-12 05:14:49 +01:00
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
import server from 'next/dist/server/next'
|
|
|
|
import _pkg from 'next/package.json'
|
|
|
|
|
|
|
|
import type { SpawnOptions, ChildProcess } from 'child_process'
|
|
|
|
import type { RequestInit, Response } from 'node-fetch'
|
|
|
|
import type { NextServer } from 'next/dist/server/next'
|
|
|
|
import type { BrowserInterface } from './browsers/base'
|
|
|
|
|
2023-09-06 19:46:54 +02:00
|
|
|
import { getTurbopackFlag, shouldRunTurboDevTest } from './turbo'
|
2023-09-05 13:40:00 +02:00
|
|
|
import stripAnsi from 'strip-ansi'
|
2023-06-23 19:42:50 +02:00
|
|
|
|
|
|
|
export { shouldRunTurboDevTest }
|
|
|
|
|
2017-01-12 05:14:49 +01:00
|
|
|
export const nextServer = server
|
|
|
|
export const pkg = _pkg
|
|
|
|
|
2019-11-11 04:24:53 +01:00
|
|
|
export function initNextServerScript(
|
2023-06-23 19:42:50 +02:00
|
|
|
scriptPath: string,
|
|
|
|
successRegexp: RegExp,
|
|
|
|
env: NodeJS.ProcessEnv,
|
|
|
|
failRegexp?: RegExp,
|
|
|
|
opts?: {
|
|
|
|
cwd?: string
|
|
|
|
nodeArgs?: string[]
|
|
|
|
onStdout?: (data: any) => void
|
|
|
|
onStderr?: (data: any) => void
|
|
|
|
}
|
|
|
|
): Promise<ChildProcess> {
|
2018-02-02 15:43:36 +01:00
|
|
|
return new Promise((resolve, reject) => {
|
2022-04-05 23:51:47 +02:00
|
|
|
const instance = spawn(
|
|
|
|
'node',
|
2023-10-20 21:38:35 +02:00
|
|
|
[...((opts && opts.nodeArgs) || []), '--no-deprecation', scriptPath],
|
2022-04-05 23:51:47 +02:00
|
|
|
{
|
|
|
|
env,
|
|
|
|
cwd: opts && opts.cwd,
|
|
|
|
}
|
|
|
|
)
|
2018-02-02 15:43:36 +01:00
|
|
|
|
2019-11-11 04:24:53 +01:00
|
|
|
function handleStdout(data) {
|
2018-02-02 15:43:36 +01:00
|
|
|
const message = data.toString()
|
|
|
|
if (successRegexp.test(message)) {
|
|
|
|
resolve(instance)
|
|
|
|
}
|
|
|
|
process.stdout.write(message)
|
2020-04-06 18:54:42 +02:00
|
|
|
|
|
|
|
if (opts && opts.onStdout) {
|
|
|
|
opts.onStdout(message.toString())
|
|
|
|
}
|
2018-02-02 15:43:36 +01:00
|
|
|
}
|
|
|
|
|
2019-11-11 04:24:53 +01:00
|
|
|
function handleStderr(data) {
|
2019-03-23 17:20:54 +01:00
|
|
|
const message = data.toString()
|
|
|
|
if (failRegexp && failRegexp.test(message)) {
|
|
|
|
instance.kill()
|
|
|
|
return reject(new Error('received failRegexp'))
|
|
|
|
}
|
|
|
|
process.stderr.write(message)
|
2020-04-06 18:54:42 +02:00
|
|
|
|
|
|
|
if (opts && opts.onStderr) {
|
|
|
|
opts.onStderr(message.toString())
|
|
|
|
}
|
2018-02-02 15:43:36 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
instance.stdout.on('data', handleStdout)
|
|
|
|
instance.stderr.on('data', handleStderr)
|
|
|
|
|
|
|
|
instance.on('close', () => {
|
|
|
|
instance.stdout.removeListener('data', handleStdout)
|
|
|
|
instance.stderr.removeListener('data', handleStderr)
|
|
|
|
})
|
|
|
|
|
2020-05-18 21:24:37 +02:00
|
|
|
instance.on('error', (err) => {
|
2018-02-02 15:43:36 +01:00
|
|
|
reject(err)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
export function getFullUrl(
|
|
|
|
appPortOrUrl: string | number,
|
|
|
|
url?: string,
|
|
|
|
hostname?: string
|
|
|
|
) {
|
2021-09-13 14:36:25 +02:00
|
|
|
let fullUrl =
|
|
|
|
typeof appPortOrUrl === 'string' && appPortOrUrl.startsWith('http')
|
|
|
|
? appPortOrUrl
|
|
|
|
: `http://${hostname ? hostname : 'localhost'}:${appPortOrUrl}${url}`
|
|
|
|
|
|
|
|
if (typeof appPortOrUrl === 'string' && url) {
|
|
|
|
const parsedUrl = new URL(fullUrl)
|
|
|
|
const parsedPathQuery = new URL(url, fullUrl)
|
|
|
|
|
2022-07-21 21:16:38 +02:00
|
|
|
parsedUrl.hash = parsedPathQuery.hash
|
2021-09-13 14:36:25 +02:00
|
|
|
parsedUrl.search = parsedPathQuery.search
|
|
|
|
parsedUrl.pathname = parsedPathQuery.pathname
|
2021-12-21 21:42:54 +01:00
|
|
|
|
|
|
|
if (hostname && parsedUrl.hostname === 'localhost') {
|
|
|
|
parsedUrl.hostname = hostname
|
|
|
|
}
|
2021-09-13 14:36:25 +02:00
|
|
|
fullUrl = parsedUrl.toString()
|
|
|
|
}
|
|
|
|
return fullUrl
|
|
|
|
}
|
|
|
|
|
2023-01-05 16:31:03 +01:00
|
|
|
/**
|
|
|
|
* Appends the querystring to the url
|
|
|
|
*
|
2023-06-23 19:42:50 +02:00
|
|
|
* @param pathname the pathname
|
|
|
|
* @param query the query object to add to the pathname
|
2023-01-05 16:31:03 +01:00
|
|
|
* @returns the pathname with the query
|
|
|
|
*/
|
2023-06-23 19:42:50 +02:00
|
|
|
export function withQuery(
|
|
|
|
pathname: string,
|
|
|
|
query: Record<string, any> | string
|
|
|
|
) {
|
2023-01-05 16:31:03 +01:00
|
|
|
const querystring = typeof query === 'string' ? query : qs.stringify(query)
|
|
|
|
if (querystring.length === 0) {
|
|
|
|
return pathname
|
|
|
|
}
|
|
|
|
|
|
|
|
// If there's a `?` between the pathname and the querystring already, then
|
|
|
|
// don't add another one.
|
|
|
|
if (querystring.startsWith('?') || pathname.endsWith('?')) {
|
|
|
|
return `${pathname}${querystring}`
|
|
|
|
}
|
|
|
|
|
|
|
|
return `${pathname}?${querystring}`
|
|
|
|
}
|
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
export function fetchViaHTTP(
|
|
|
|
appPort: string | number,
|
|
|
|
pathname: string,
|
|
|
|
query?: Record<string, any> | string | null | undefined,
|
|
|
|
opts?: RequestInit
|
|
|
|
): Promise<Response> {
|
2023-01-05 16:31:03 +01:00
|
|
|
const url = query ? withQuery(pathname, query) : pathname
|
2023-08-14 09:23:24 +02:00
|
|
|
return fetch(getFullUrl(appPort, url), opts)
|
2017-01-12 05:14:49 +01:00
|
|
|
}
|
2017-02-09 14:40:09 +01:00
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
export function renderViaHTTP(
|
|
|
|
appPort: string | number,
|
|
|
|
pathname: string,
|
|
|
|
query?: Record<string, any> | string | undefined,
|
|
|
|
opts?: RequestInit
|
|
|
|
) {
|
|
|
|
return fetchViaHTTP(appPort, pathname, query, opts).then((res) => res.text())
|
|
|
|
}
|
|
|
|
|
2019-11-11 04:24:53 +01:00
|
|
|
export function findPort() {
|
2023-09-11 23:48:29 +02:00
|
|
|
// [NOTE] What are we doing here?
|
|
|
|
// There are some flaky tests failures caused by `No available ports found` from 'get-port'.
|
|
|
|
// This may be related / fixed by upstream https://github.com/sindresorhus/get-port/pull/56,
|
|
|
|
// however it happened after get-port switched to pure esm which is not easy to adapt by bump.
|
|
|
|
// get-port-please seems to offer the feature parity so we'll try to use it, and leave get-port as fallback
|
|
|
|
// for a while until we are certain to switch to get-port-please entirely.
|
|
|
|
try {
|
|
|
|
return getRandomPort()
|
|
|
|
} catch (e) {
|
|
|
|
require('console').warn('get-port-please failed, falling back to get-port')
|
|
|
|
return getPort()
|
|
|
|
}
|
2017-07-20 18:00:45 +02:00
|
|
|
}
|
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
export interface NextOptions {
|
|
|
|
cwd?: string
|
|
|
|
env?: NodeJS.Dict<string>
|
|
|
|
nodeArgs?: string[]
|
|
|
|
|
|
|
|
spawnOptions?: SpawnOptions
|
|
|
|
instance?: (instance: ChildProcess) => void
|
|
|
|
stderr?: true | 'log'
|
|
|
|
stdout?: true | 'log'
|
|
|
|
ignoreFail?: boolean
|
|
|
|
|
|
|
|
onStdout?: (data: any) => void
|
|
|
|
onStderr?: (data: any) => void
|
|
|
|
}
|
|
|
|
|
|
|
|
export function runNextCommand(
|
|
|
|
argv: string[],
|
|
|
|
options: NextOptions = {}
|
|
|
|
): Promise<{
|
|
|
|
code: number
|
|
|
|
signal: NodeJS.Signals
|
|
|
|
stdout: string
|
|
|
|
stderr: string
|
|
|
|
}> {
|
2019-03-28 20:13:27 +01:00
|
|
|
const nextDir = path.dirname(require.resolve('next/package'))
|
|
|
|
const nextBin = path.join(nextDir, 'dist/bin/next')
|
|
|
|
const cwd = options.cwd || nextDir
|
2019-03-28 23:05:18 +01:00
|
|
|
// Let Next.js decide the environment
|
2020-01-21 21:33:58 +01:00
|
|
|
const env = {
|
|
|
|
...process.env,
|
2023-06-23 19:42:50 +02:00
|
|
|
NODE_ENV: undefined,
|
2020-01-21 21:33:58 +01:00
|
|
|
__NEXT_TEST_MODE: 'true',
|
2021-11-12 20:59:21 +01:00
|
|
|
...options.env,
|
2020-01-21 21:33:58 +01:00
|
|
|
}
|
2019-03-28 23:05:18 +01:00
|
|
|
|
2018-12-15 22:55:59 +01:00
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
console.log(`Running command "next ${argv.join(' ')}"`)
|
2021-07-29 22:06:13 +02:00
|
|
|
const instance = spawn(
|
|
|
|
'node',
|
2023-10-20 21:38:35 +02:00
|
|
|
[...(options.nodeArgs || []), '--no-deprecation', nextBin, ...argv],
|
2021-07-29 22:06:13 +02:00
|
|
|
{
|
|
|
|
...options.spawnOptions,
|
|
|
|
cwd,
|
|
|
|
env,
|
|
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
|
|
}
|
|
|
|
)
|
2019-02-15 17:49:40 +01:00
|
|
|
|
2019-03-03 19:36:32 +01:00
|
|
|
if (typeof options.instance === 'function') {
|
|
|
|
options.instance(instance)
|
|
|
|
}
|
|
|
|
|
2021-10-27 09:33:43 +02:00
|
|
|
let mergedStdio = ''
|
|
|
|
|
2019-02-15 17:49:40 +01:00
|
|
|
let stderrOutput = ''
|
2023-03-23 15:40:18 +01:00
|
|
|
if (options.stderr || options.onStderr) {
|
2020-05-18 21:24:37 +02:00
|
|
|
instance.stderr.on('data', function (chunk) {
|
2021-10-27 09:33:43 +02:00
|
|
|
mergedStdio += chunk
|
2019-02-15 17:49:40 +01:00
|
|
|
stderrOutput += chunk
|
2021-04-06 19:12:23 +02:00
|
|
|
|
|
|
|
if (options.stderr === 'log') {
|
|
|
|
console.log(chunk.toString())
|
|
|
|
}
|
2023-03-23 15:40:18 +01:00
|
|
|
if (typeof options.onStderr === 'function') {
|
|
|
|
options.onStderr(chunk.toString())
|
|
|
|
}
|
2019-02-15 17:49:40 +01:00
|
|
|
})
|
2021-10-27 09:33:43 +02:00
|
|
|
} else {
|
|
|
|
instance.stderr.on('data', function (chunk) {
|
|
|
|
mergedStdio += chunk
|
|
|
|
})
|
2019-02-15 17:49:40 +01:00
|
|
|
}
|
2018-12-15 22:55:59 +01:00
|
|
|
|
|
|
|
let stdoutOutput = ''
|
2023-03-23 15:40:18 +01:00
|
|
|
if (options.stdout || options.onStdout) {
|
2020-05-18 21:24:37 +02:00
|
|
|
instance.stdout.on('data', function (chunk) {
|
2021-10-27 09:33:43 +02:00
|
|
|
mergedStdio += chunk
|
2018-12-15 22:55:59 +01:00
|
|
|
stdoutOutput += chunk
|
2021-04-06 19:12:23 +02:00
|
|
|
|
|
|
|
if (options.stdout === 'log') {
|
|
|
|
console.log(chunk.toString())
|
|
|
|
}
|
2023-03-23 15:40:18 +01:00
|
|
|
if (typeof options.onStdout === 'function') {
|
|
|
|
options.onStdout(chunk.toString())
|
|
|
|
}
|
2018-12-15 22:55:59 +01:00
|
|
|
})
|
2021-10-27 09:33:43 +02:00
|
|
|
} else {
|
|
|
|
instance.stdout.on('data', function (chunk) {
|
|
|
|
mergedStdio += chunk
|
|
|
|
})
|
2018-12-15 22:55:59 +01:00
|
|
|
}
|
|
|
|
|
2020-11-25 14:30:06 +01:00
|
|
|
instance.on('close', (code, signal) => {
|
2020-06-09 20:28:15 +02:00
|
|
|
if (
|
|
|
|
!options.stderr &&
|
|
|
|
!options.stdout &&
|
|
|
|
!options.ignoreFail &&
|
2022-12-07 17:43:19 +01:00
|
|
|
(code !== 0 || signal)
|
2020-06-09 20:28:15 +02:00
|
|
|
) {
|
2021-10-27 09:33:43 +02:00
|
|
|
return reject(
|
2022-12-07 17:43:19 +01:00
|
|
|
new Error(
|
|
|
|
`command failed with code ${code} signal ${signal}\n${mergedStdio}`
|
|
|
|
)
|
2021-10-27 09:33:43 +02:00
|
|
|
)
|
2020-06-09 20:28:15 +02:00
|
|
|
}
|
|
|
|
|
2022-12-07 17:43:19 +01:00
|
|
|
if (code || signal) {
|
|
|
|
console.error(`process exited with code ${code} and signal ${signal}`)
|
|
|
|
}
|
2018-12-15 22:55:59 +01:00
|
|
|
resolve({
|
2020-01-17 03:39:00 +01:00
|
|
|
code,
|
2020-11-25 14:30:06 +01:00
|
|
|
signal,
|
2019-02-15 17:49:40 +01:00
|
|
|
stdout: stdoutOutput,
|
2019-11-11 04:24:53 +01:00
|
|
|
stderr: stderrOutput,
|
2018-12-15 22:55:59 +01:00
|
|
|
})
|
|
|
|
})
|
|
|
|
|
2020-05-18 21:24:37 +02:00
|
|
|
instance.on('error', (err) => {
|
2023-06-23 19:42:50 +02:00
|
|
|
err['stdout'] = stdoutOutput
|
|
|
|
err['stderr'] = stderrOutput
|
2018-12-15 22:55:59 +01:00
|
|
|
reject(err)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
export interface NextDevOptions {
|
|
|
|
cwd?: string
|
|
|
|
env?: NodeJS.Dict<string>
|
|
|
|
nodeArgs?: string[]
|
|
|
|
nextBin?: string
|
|
|
|
|
|
|
|
bootupMarker?: RegExp
|
|
|
|
nextStart?: boolean
|
|
|
|
turbo?: boolean
|
|
|
|
|
|
|
|
stderr?: false
|
|
|
|
stdout?: false
|
|
|
|
|
|
|
|
onStdout?: (data: any) => void
|
|
|
|
onStderr?: (data: any) => void
|
|
|
|
}
|
|
|
|
|
|
|
|
export function runNextCommandDev(
|
|
|
|
argv: string[],
|
|
|
|
stdOut?: boolean,
|
|
|
|
opts: NextDevOptions = {}
|
|
|
|
): Promise<(typeof stdOut extends true ? string : ChildProcess) | undefined> {
|
2021-05-14 12:50:29 +02:00
|
|
|
const nextDir = path.dirname(require.resolve('next/package'))
|
2023-01-24 20:01:03 +01:00
|
|
|
const nextBin = opts.nextBin || path.join(nextDir, 'dist/bin/next')
|
2021-05-14 12:50:29 +02:00
|
|
|
const cwd = opts.cwd || nextDir
|
2019-08-22 22:34:24 +02:00
|
|
|
const env = {
|
|
|
|
...process.env,
|
|
|
|
NODE_ENV: undefined,
|
|
|
|
__NEXT_TEST_MODE: 'true',
|
2019-11-11 04:24:53 +01:00
|
|
|
...opts.env,
|
2019-08-22 22:34:24 +02:00
|
|
|
}
|
2019-03-30 01:50:24 +01:00
|
|
|
|
2021-07-29 22:06:13 +02:00
|
|
|
const nodeArgs = opts.nodeArgs || []
|
2017-07-20 18:00:45 +02:00
|
|
|
return new Promise((resolve, reject) => {
|
2021-07-29 22:06:13 +02:00
|
|
|
const instance = spawn(
|
|
|
|
'node',
|
2023-10-20 21:38:35 +02:00
|
|
|
[...nodeArgs, '--no-deprecation', nextBin, ...argv],
|
2021-07-29 22:06:13 +02:00
|
|
|
{
|
|
|
|
cwd,
|
|
|
|
env,
|
|
|
|
}
|
|
|
|
)
|
2019-12-10 15:54:56 +01:00
|
|
|
let didResolve = false
|
2017-07-20 18:00:45 +02:00
|
|
|
|
2023-09-05 13:40:00 +02:00
|
|
|
const bootType =
|
2023-09-06 19:46:54 +02:00
|
|
|
opts.nextStart || stdOut ? 'start' : opts?.turbo ? 'turbo' : 'dev'
|
2023-09-05 13:40:00 +02:00
|
|
|
|
2019-11-11 04:24:53 +01:00
|
|
|
function handleStdout(data) {
|
2017-07-20 18:00:45 +02:00
|
|
|
const message = data.toString()
|
2020-04-29 10:49:28 +02:00
|
|
|
const bootupMarkers = {
|
2023-09-05 13:40:00 +02:00
|
|
|
dev: /✓ ready/i,
|
2023-09-06 19:46:54 +02:00
|
|
|
turbo: /✓ ready/i,
|
2023-09-13 15:27:02 +02:00
|
|
|
start: /✓ ready/i,
|
2020-04-29 10:49:28 +02:00
|
|
|
}
|
2023-09-05 13:40:00 +02:00
|
|
|
|
|
|
|
const strippedMessage = stripAnsi(message) as any
|
2023-09-13 15:27:02 +02:00
|
|
|
|
2020-04-29 10:49:28 +02:00
|
|
|
if (
|
2023-09-05 13:40:00 +02:00
|
|
|
(opts.bootupMarker && opts.bootupMarker.test(strippedMessage)) ||
|
|
|
|
bootupMarkers[bootType].test(strippedMessage)
|
2020-04-29 10:49:28 +02:00
|
|
|
) {
|
2019-12-10 15:54:56 +01:00
|
|
|
if (!didResolve) {
|
|
|
|
didResolve = true
|
2023-09-05 13:40:00 +02:00
|
|
|
// Pass down the original message
|
2019-12-10 15:54:56 +01:00
|
|
|
resolve(stdOut ? message : instance)
|
|
|
|
}
|
2017-07-20 18:00:45 +02:00
|
|
|
}
|
2020-04-29 10:49:28 +02:00
|
|
|
|
2019-07-27 10:34:29 +02:00
|
|
|
if (typeof opts.onStdout === 'function') {
|
|
|
|
opts.onStdout(message)
|
|
|
|
}
|
2020-05-04 18:58:19 +02:00
|
|
|
|
|
|
|
if (opts.stdout !== false) {
|
|
|
|
process.stdout.write(message)
|
|
|
|
}
|
2017-07-20 18:00:45 +02:00
|
|
|
}
|
|
|
|
|
2019-11-11 04:24:53 +01:00
|
|
|
function handleStderr(data) {
|
2023-09-20 15:22:16 +02:00
|
|
|
const message = data.toString()
|
|
|
|
|
2019-07-27 10:34:29 +02:00
|
|
|
if (typeof opts.onStderr === 'function') {
|
|
|
|
opts.onStderr(message)
|
|
|
|
}
|
2020-05-04 18:58:19 +02:00
|
|
|
|
|
|
|
if (opts.stderr !== false) {
|
|
|
|
process.stderr.write(message)
|
|
|
|
}
|
2017-07-20 18:00:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
instance.stderr.on('data', handleStderr)
|
2023-09-05 13:40:00 +02:00
|
|
|
instance.stdout.on('data', handleStdout)
|
2017-07-20 18:00:45 +02:00
|
|
|
|
|
|
|
instance.on('close', () => {
|
|
|
|
instance.stderr.removeListener('data', handleStderr)
|
2023-09-05 13:40:00 +02:00
|
|
|
instance.stdout.removeListener('data', handleStdout)
|
2019-12-10 15:54:56 +01:00
|
|
|
if (!didResolve) {
|
|
|
|
didResolve = true
|
2023-06-23 19:42:50 +02:00
|
|
|
resolve(undefined)
|
2019-12-10 15:54:56 +01:00
|
|
|
}
|
2017-07-20 18:00:45 +02:00
|
|
|
})
|
|
|
|
|
2020-05-18 21:24:37 +02:00
|
|
|
instance.on('error', (err) => {
|
2017-07-20 18:00:45 +02:00
|
|
|
reject(err)
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2018-12-15 22:55:59 +01:00
|
|
|
// Launch the app in dev mode.
|
2023-06-23 19:42:50 +02:00
|
|
|
export function launchApp(
|
|
|
|
dir: string,
|
|
|
|
port: string | number,
|
|
|
|
opts?: NextDevOptions
|
|
|
|
) {
|
2022-11-24 00:00:49 +01:00
|
|
|
const options = opts ?? {}
|
2023-06-21 15:52:14 +02:00
|
|
|
const useTurbo = shouldRunTurboDevTest()
|
2022-10-27 07:56:18 +02:00
|
|
|
|
|
|
|
return runNextCommandDev(
|
2023-08-09 07:48:54 +02:00
|
|
|
[
|
2023-09-06 19:46:54 +02:00
|
|
|
useTurbo ? getTurbopackFlag() : undefined,
|
2023-08-09 07:48:54 +02:00
|
|
|
dir,
|
|
|
|
'-p',
|
|
|
|
port as string,
|
|
|
|
].filter(Boolean),
|
2022-10-27 07:56:18 +02:00
|
|
|
undefined,
|
|
|
|
{
|
|
|
|
...options,
|
|
|
|
turbo: useTurbo,
|
|
|
|
}
|
|
|
|
)
|
2018-12-15 22:55:59 +01:00
|
|
|
}
|
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
export function nextBuild(
|
|
|
|
dir: string,
|
|
|
|
args: string[] = [],
|
|
|
|
opts: NextOptions = {}
|
|
|
|
) {
|
2019-06-06 15:57:42 +02:00
|
|
|
return runNextCommand(['build', dir, ...args], opts)
|
2018-12-15 22:55:59 +01:00
|
|
|
}
|
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
export function nextLint(
|
|
|
|
dir: string,
|
|
|
|
args: string[] = [],
|
|
|
|
opts: NextOptions = {}
|
|
|
|
) {
|
2021-06-03 14:01:24 +02:00
|
|
|
return runNextCommand(['lint', dir, ...args], opts)
|
|
|
|
}
|
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
export function nextStart(
|
|
|
|
dir: string,
|
|
|
|
port: string | number,
|
|
|
|
opts: NextDevOptions = {}
|
|
|
|
) {
|
|
|
|
return runNextCommandDev(['start', '-p', port as string, dir], undefined, {
|
2020-04-29 10:49:28 +02:00
|
|
|
...opts,
|
|
|
|
nextStart: true,
|
|
|
|
})
|
2019-05-22 18:36:53 +02:00
|
|
|
}
|
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
export function buildTS(
|
|
|
|
args: string[] = [],
|
|
|
|
cwd?: string,
|
|
|
|
env?: any
|
|
|
|
): Promise<void> {
|
2019-07-04 04:38:58 +02:00
|
|
|
cwd = cwd || path.dirname(require.resolve('next/package'))
|
|
|
|
env = { ...process.env, NODE_ENV: undefined, ...env }
|
|
|
|
|
|
|
|
return new Promise((resolve, reject) => {
|
|
|
|
const instance = spawn(
|
|
|
|
'node',
|
2020-07-20 17:14:02 +02:00
|
|
|
['--no-deprecation', require.resolve('typescript/lib/tsc'), ...args],
|
2019-07-04 04:38:58 +02:00
|
|
|
{ cwd, env }
|
|
|
|
)
|
|
|
|
let output = ''
|
|
|
|
|
2020-05-18 21:24:37 +02:00
|
|
|
const handleData = (chunk) => {
|
2019-07-04 04:38:58 +02:00
|
|
|
output += chunk.toString()
|
|
|
|
}
|
|
|
|
|
|
|
|
instance.stdout.on('data', handleData)
|
|
|
|
instance.stderr.on('data', handleData)
|
|
|
|
|
2020-05-18 21:24:37 +02:00
|
|
|
instance.on('exit', (code) => {
|
2019-07-10 23:13:44 +02:00
|
|
|
if (code) {
|
|
|
|
return reject(new Error('exited with code: ' + code + '\n' + output))
|
|
|
|
}
|
2019-07-04 04:38:58 +02:00
|
|
|
resolve()
|
|
|
|
})
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-09-11 22:17:52 +02:00
|
|
|
export async function killProcess(
|
|
|
|
pid: number,
|
2024-01-16 18:25:49 +01:00
|
|
|
signal: NodeJS.Signals | number = 'SIGTERM'
|
2023-09-11 22:17:52 +02:00
|
|
|
): Promise<void> {
|
2023-06-23 19:42:50 +02:00
|
|
|
return await new Promise((resolve, reject) => {
|
2023-09-11 22:17:52 +02:00
|
|
|
treeKill(pid, signal, (err) => {
|
2019-08-06 18:51:05 +02:00
|
|
|
if (err) {
|
|
|
|
if (
|
|
|
|
process.platform === 'win32' &&
|
|
|
|
typeof err.message === 'string' &&
|
2019-08-23 04:01:12 +02:00
|
|
|
(err.message.includes(`no running instance of the task`) ||
|
|
|
|
err.message.includes(`not found`))
|
2019-08-06 18:51:05 +02:00
|
|
|
) {
|
|
|
|
// Windows throws an error if the process is already dead
|
|
|
|
//
|
|
|
|
// Command failed: taskkill /pid 6924 /T /F
|
|
|
|
// ERROR: The process with PID 6924 (child process of PID 6736) could not be terminated.
|
|
|
|
// Reason: There is no running instance of the task.
|
|
|
|
return resolve()
|
|
|
|
}
|
|
|
|
return reject(err)
|
|
|
|
}
|
|
|
|
|
2019-05-09 04:51:23 +02:00
|
|
|
resolve()
|
|
|
|
})
|
|
|
|
})
|
2017-07-20 18:00:45 +02:00
|
|
|
}
|
|
|
|
|
2021-11-09 14:42:23 +01:00
|
|
|
// Kill a launched app
|
2024-01-16 18:25:49 +01:00
|
|
|
export async function killApp(
|
|
|
|
instance?: ChildProcess,
|
|
|
|
signal: NodeJS.Signals | number = 'SIGKILL'
|
|
|
|
) {
|
|
|
|
if (
|
|
|
|
instance?.pid &&
|
|
|
|
instance.exitCode === null &&
|
|
|
|
instance.signalCode === null
|
|
|
|
) {
|
|
|
|
const exitPromise = once(instance, 'exit')
|
|
|
|
await killProcess(instance.pid, signal)
|
|
|
|
await exitPromise
|
2023-09-11 17:29:49 +02:00
|
|
|
}
|
2021-11-09 14:42:23 +01:00
|
|
|
}
|
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
export async function startApp(app: NextServer) {
|
2021-12-21 19:52:07 +01:00
|
|
|
// force require usage instead of dynamic import in jest
|
|
|
|
// x-ref: https://github.com/nodejs/node/issues/35889
|
|
|
|
process.env.__NEXT_TEST_MODE = 'jest'
|
|
|
|
|
|
|
|
// TODO: tests that use this should be migrated to use
|
|
|
|
// the nextStart test function instead as it tests outside
|
|
|
|
// of jest's context
|
2017-02-09 14:40:09 +01:00
|
|
|
await app.prepare()
|
|
|
|
const handler = app.getRequestHandler()
|
|
|
|
const server = http.createServer(handler)
|
2023-06-23 19:42:50 +02:00
|
|
|
server['__app'] = app
|
|
|
|
|
|
|
|
await promisify(server.listen).apply(server)
|
2017-02-09 14:40:09 +01:00
|
|
|
|
|
|
|
return server
|
|
|
|
}
|
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
export async function stopApp(server: http.Server) {
|
|
|
|
if (server['__app']) {
|
|
|
|
await server['__app'].close()
|
2017-05-10 01:24:34 +02:00
|
|
|
}
|
2023-06-23 19:42:50 +02:00
|
|
|
await promisify(server.close).apply(server)
|
2017-02-09 14:40:09 +01:00
|
|
|
}
|
2017-02-26 20:45:16 +01:00
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
export function waitFor(millis: number) {
|
2020-05-18 21:24:37 +02:00
|
|
|
return new Promise((resolve) => setTimeout(resolve, millis))
|
2017-02-26 20:45:16 +01:00
|
|
|
}
|
2017-05-10 01:24:34 +02:00
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
export async function startStaticServer(
|
|
|
|
dir: string,
|
|
|
|
notFoundFile?: string,
|
|
|
|
fixedPort?: number
|
|
|
|
) {
|
2017-05-10 01:24:34 +02:00
|
|
|
const app = express()
|
|
|
|
const server = http.createServer(app)
|
|
|
|
app.use(express.static(dir))
|
|
|
|
|
2021-08-03 17:06:26 +02:00
|
|
|
if (notFoundFile) {
|
|
|
|
app.use((req, res) => {
|
|
|
|
createReadStream(notFoundFile).pipe(res)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
await promisify(server.listen).call(server, fixedPort)
|
2017-05-10 01:24:34 +02:00
|
|
|
return server
|
|
|
|
}
|
2018-02-26 17:18:46 +01:00
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
export async function startCleanStaticServer(dir: string) {
|
2019-11-13 03:34:12 +01:00
|
|
|
const app = express()
|
|
|
|
const server = http.createServer(app)
|
|
|
|
app.use(express.static(dir, { extensions: ['html'] }))
|
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
await promisify(server.listen).apply(server)
|
2019-11-13 03:34:12 +01:00
|
|
|
return server
|
|
|
|
}
|
|
|
|
|
2023-06-22 17:13:13 +02:00
|
|
|
/**
|
|
|
|
* Check for content in 1 second intervals timing out after 30 seconds.
|
|
|
|
*
|
|
|
|
* @param {() => Promise<unknown> | unknown} contentFn
|
|
|
|
* @param {RegExp | string | number} regex
|
|
|
|
* @param {boolean} hardError
|
|
|
|
* @param {number} maxRetries
|
|
|
|
* @returns {Promise<boolean>}
|
|
|
|
*/
|
2023-03-14 13:17:10 +01:00
|
|
|
export async function check(
|
2023-06-23 19:42:50 +02:00
|
|
|
contentFn: () => any | Promise<any>,
|
|
|
|
regex: any,
|
2023-03-14 13:17:10 +01:00
|
|
|
hardError = true,
|
|
|
|
maxRetries = 30
|
|
|
|
) {
|
2020-05-05 22:19:07 +02:00
|
|
|
let content
|
2020-05-10 11:36:07 +02:00
|
|
|
let lastErr
|
2020-05-05 22:19:07 +02:00
|
|
|
|
2023-03-14 13:17:10 +01:00
|
|
|
for (let tries = 0; tries < maxRetries; tries++) {
|
2018-09-02 17:22:29 +02:00
|
|
|
try {
|
|
|
|
content = await contentFn()
|
2023-04-05 20:17:54 +02:00
|
|
|
if (typeof regex !== typeof /regex/) {
|
2020-06-11 20:29:38 +02:00
|
|
|
if (regex === content) {
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
} else if (regex.test(content)) {
|
2020-05-05 22:19:07 +02:00
|
|
|
// found the content
|
|
|
|
return true
|
2018-07-24 11:24:40 +02:00
|
|
|
}
|
2018-02-26 17:18:46 +01:00
|
|
|
await waitFor(1000)
|
2020-05-05 22:19:07 +02:00
|
|
|
} catch (err) {
|
2020-05-10 11:36:07 +02:00
|
|
|
await waitFor(1000)
|
|
|
|
lastErr = err
|
2020-05-05 22:19:07 +02:00
|
|
|
}
|
|
|
|
}
|
2020-05-10 11:36:07 +02:00
|
|
|
console.error('TIMED OUT CHECK: ', { regex, content, lastErr })
|
2020-05-05 22:19:07 +02:00
|
|
|
|
|
|
|
if (hardError) {
|
2023-03-17 18:38:19 +01:00
|
|
|
throw new Error('TIMED OUT: ' + regex + '\n\n' + content + '\n\n' + lastErr)
|
2018-02-26 17:18:46 +01:00
|
|
|
}
|
2020-05-05 22:19:07 +02:00
|
|
|
return false
|
2018-02-26 17:18:46 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
export class File {
|
2023-06-23 19:42:50 +02:00
|
|
|
path: string
|
|
|
|
originalContent: string
|
|
|
|
|
|
|
|
constructor(path: string) {
|
2018-02-26 17:18:46 +01:00
|
|
|
this.path = path
|
2019-05-30 03:19:32 +02:00
|
|
|
this.originalContent = existsSync(this.path)
|
|
|
|
? readFileSync(this.path, 'utf8')
|
|
|
|
: null
|
2018-02-26 17:18:46 +01:00
|
|
|
}
|
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
write(content: string) {
|
2018-02-26 17:18:46 +01:00
|
|
|
if (!this.originalContent) {
|
|
|
|
this.originalContent = content
|
|
|
|
}
|
|
|
|
writeFileSync(this.path, content, 'utf8')
|
|
|
|
}
|
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
replace(pattern: RegExp | string, newValue: string) {
|
2020-07-16 01:53:31 +02:00
|
|
|
const currentContent = readFileSync(this.path, 'utf8')
|
2020-06-11 20:29:38 +02:00
|
|
|
if (pattern instanceof RegExp) {
|
2020-07-16 01:53:31 +02:00
|
|
|
if (!pattern.test(currentContent)) {
|
2020-06-11 20:29:38 +02:00
|
|
|
throw new Error(
|
2020-07-16 01:53:31 +02:00
|
|
|
`Failed to replace content.\n\nPattern: ${pattern.toString()}\n\nContent: ${currentContent}`
|
2020-06-11 20:29:38 +02:00
|
|
|
)
|
|
|
|
}
|
|
|
|
} else if (typeof pattern === 'string') {
|
2020-07-16 01:53:31 +02:00
|
|
|
if (!currentContent.includes(pattern)) {
|
2020-06-11 20:29:38 +02:00
|
|
|
throw new Error(
|
2020-07-16 01:53:31 +02:00
|
|
|
`Failed to replace content.\n\nPattern: ${pattern}\n\nContent: ${currentContent}`
|
2020-06-11 20:29:38 +02:00
|
|
|
)
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
throw new Error(`Unknown replacement attempt type: ${pattern}`)
|
|
|
|
}
|
|
|
|
|
2020-07-16 01:53:31 +02:00
|
|
|
const newContent = currentContent.replace(pattern, newValue)
|
2018-02-26 17:18:46 +01:00
|
|
|
this.write(newContent)
|
|
|
|
}
|
|
|
|
|
2023-08-21 16:45:00 +02:00
|
|
|
prepend(str: string) {
|
|
|
|
const content = readFileSync(this.path, 'utf8')
|
|
|
|
this.write(str + content)
|
|
|
|
}
|
|
|
|
|
2019-11-11 04:24:53 +01:00
|
|
|
delete() {
|
2018-02-26 17:18:46 +01:00
|
|
|
unlinkSync(this.path)
|
|
|
|
}
|
|
|
|
|
2019-11-11 04:24:53 +01:00
|
|
|
restore() {
|
2018-02-26 17:18:46 +01:00
|
|
|
this.write(this.originalContent)
|
|
|
|
}
|
|
|
|
}
|
2018-07-24 11:24:40 +02:00
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
export async function evaluate(
|
|
|
|
browser: BrowserInterface,
|
|
|
|
input: string | Function
|
|
|
|
) {
|
2020-05-11 01:25:57 +02:00
|
|
|
if (typeof input === 'function') {
|
2021-09-13 14:36:25 +02:00
|
|
|
const result = await browser.eval(input)
|
2020-05-18 21:24:37 +02:00
|
|
|
await new Promise((resolve) => setTimeout(resolve, 30))
|
2020-05-11 01:25:57 +02:00
|
|
|
return result
|
|
|
|
} else {
|
|
|
|
throw new Error(`You must pass a function to be evaluated in the browser.`)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
export async function retry<T>(
|
|
|
|
fn: () => T | Promise<T>,
|
|
|
|
duration: number = 3000,
|
|
|
|
interval: number = 500,
|
|
|
|
description?: string
|
|
|
|
): Promise<T> {
|
2021-01-07 19:06:40 +01:00
|
|
|
if (duration % interval !== 0) {
|
|
|
|
throw new Error(
|
|
|
|
`invalid duration ${duration} and interval ${interval} mix, duration must be evenly divisible by interval`
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let i = duration; i >= 0; i -= interval) {
|
|
|
|
try {
|
|
|
|
return await fn()
|
|
|
|
} catch (err) {
|
|
|
|
if (i === 0) {
|
|
|
|
console.error(
|
|
|
|
`Failed to retry${
|
|
|
|
description ? ` ${description}` : ''
|
|
|
|
} within ${duration}ms`
|
|
|
|
)
|
|
|
|
throw err
|
|
|
|
}
|
|
|
|
console.warn(
|
|
|
|
`Retrying${description ? ` ${description}` : ''} in ${interval}ms`
|
|
|
|
)
|
|
|
|
await waitFor(interval)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-01-15 09:36:44 +01:00
|
|
|
export async function hasRedbox(browser: BrowserInterface): Promise<boolean> {
|
|
|
|
await waitFor(5000)
|
|
|
|
const result = await evaluate(browser, () => {
|
|
|
|
return Boolean(
|
|
|
|
[].slice
|
|
|
|
.call(document.querySelectorAll('nextjs-portal'))
|
|
|
|
.find((p) =>
|
|
|
|
p.shadowRoot.querySelector(
|
|
|
|
'#nextjs__container_errors_label, #nextjs__container_build_error_label, #nextjs__container_root_layout_error_label'
|
2020-05-11 01:25:57 +02:00
|
|
|
)
|
2024-01-15 09:36:44 +01:00
|
|
|
)
|
|
|
|
)
|
|
|
|
})
|
|
|
|
return result
|
2020-05-11 01:25:57 +02:00
|
|
|
}
|
|
|
|
|
2024-02-26 18:02:07 +01:00
|
|
|
export async function hasErrorToast(
|
|
|
|
browser: BrowserInterface
|
|
|
|
): Promise<boolean> {
|
|
|
|
return browser.eval(() => {
|
|
|
|
return Boolean(
|
|
|
|
Array.from(document.querySelectorAll('nextjs-portal')).find((p) =>
|
|
|
|
p.shadowRoot.querySelector('[data-nextjs-toast]')
|
|
|
|
)
|
|
|
|
)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function waitForAndOpenRuntimeError(browser: BrowserInterface) {
|
|
|
|
return browser.waitForElementByCss('[data-nextjs-toast]').click()
|
|
|
|
}
|
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
export async function getRedboxHeader(browser: BrowserInterface) {
|
2021-01-07 19:06:40 +01:00
|
|
|
return retry(
|
2023-03-01 10:23:39 +01:00
|
|
|
() => {
|
2023-09-18 19:30:27 +02:00
|
|
|
return evaluate(browser, () => {
|
|
|
|
const portal = [].slice
|
|
|
|
.call(document.querySelectorAll('nextjs-portal'))
|
|
|
|
.find((p) =>
|
|
|
|
p.shadowRoot.querySelector('[data-nextjs-dialog-header]')
|
|
|
|
)
|
|
|
|
const root = portal?.shadowRoot
|
|
|
|
return root?.querySelector('[data-nextjs-dialog-header]')?.innerText
|
|
|
|
})
|
2023-03-01 10:23:39 +01:00
|
|
|
},
|
2022-11-25 13:06:42 +01:00
|
|
|
10000,
|
2021-01-07 19:06:40 +01:00
|
|
|
500,
|
|
|
|
'getRedboxHeader'
|
|
|
|
)
|
2020-05-11 01:25:57 +02:00
|
|
|
}
|
2018-07-24 11:24:40 +02:00
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
export async function getRedboxSource(browser: BrowserInterface) {
|
2021-01-07 19:06:40 +01:00
|
|
|
return retry(
|
|
|
|
() =>
|
|
|
|
evaluate(browser, () => {
|
|
|
|
const portal = [].slice
|
|
|
|
.call(document.querySelectorAll('nextjs-portal'))
|
|
|
|
.find((p) =>
|
|
|
|
p.shadowRoot.querySelector(
|
2022-10-14 22:55:09 +02:00
|
|
|
'#nextjs__container_errors_label, #nextjs__container_build_error_label, #nextjs__container_root_layout_error_label'
|
2021-01-07 19:06:40 +01:00
|
|
|
)
|
|
|
|
)
|
|
|
|
const root = portal.shadowRoot
|
2022-11-25 13:06:42 +01:00
|
|
|
return root.querySelector(
|
|
|
|
'[data-nextjs-codeframe], [data-nextjs-terminal]'
|
|
|
|
).innerText
|
2021-01-07 19:06:40 +01:00
|
|
|
}),
|
2022-11-25 13:06:42 +01:00
|
|
|
10000,
|
2021-01-07 19:06:40 +01:00
|
|
|
500,
|
|
|
|
'getRedboxSource'
|
|
|
|
)
|
2018-07-24 11:24:40 +02:00
|
|
|
}
|
2018-09-26 01:04:15 +02:00
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
export async function getRedboxDescription(browser: BrowserInterface) {
|
2021-09-13 14:36:25 +02:00
|
|
|
return retry(
|
|
|
|
() =>
|
|
|
|
evaluate(browser, () => {
|
|
|
|
const portal = [].slice
|
|
|
|
.call(document.querySelectorAll('nextjs-portal'))
|
|
|
|
.find((p) =>
|
|
|
|
p.shadowRoot.querySelector('[data-nextjs-dialog-header]')
|
|
|
|
)
|
|
|
|
const root = portal.shadowRoot
|
2023-07-07 11:33:51 +02:00
|
|
|
const text = root.querySelector(
|
|
|
|
'#nextjs__container_errors_desc'
|
|
|
|
).innerText
|
|
|
|
if (text === null) throw new Error('No redbox description found')
|
|
|
|
return text
|
2021-09-13 14:36:25 +02:00
|
|
|
}),
|
|
|
|
3000,
|
|
|
|
500,
|
|
|
|
'getRedboxDescription'
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2024-02-28 16:14:48 +01:00
|
|
|
export async function getRedboxDescriptionWarning(browser: BrowserInterface) {
|
|
|
|
return retry(
|
|
|
|
() =>
|
|
|
|
evaluate(browser, () => {
|
|
|
|
const portal = [].slice
|
|
|
|
.call(document.querySelectorAll('nextjs-portal'))
|
|
|
|
.find((p) =>
|
|
|
|
p.shadowRoot.querySelector('[data-nextjs-dialog-header]')
|
|
|
|
)
|
|
|
|
const root = portal.shadowRoot
|
|
|
|
const text = root.querySelector(
|
|
|
|
'#nextjs__container_errors__extra'
|
|
|
|
)?.innerText
|
|
|
|
return text
|
|
|
|
}),
|
|
|
|
3000,
|
|
|
|
500,
|
|
|
|
'getRedboxDescriptionWarning'
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
export function getBrowserBodyText(browser: BrowserInterface) {
|
2018-09-26 01:04:15 +02:00
|
|
|
return browser.eval('document.getElementsByTagName("body")[0].innerText')
|
|
|
|
}
|
2019-12-14 18:25:55 +01:00
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
export function normalizeRegEx(src: string) {
|
2024-02-29 17:34:11 +01:00
|
|
|
return new RegExp(src).source.replace(/\^\//g, '^\\/')
|
2019-12-14 18:25:55 +01:00
|
|
|
}
|
2020-06-11 10:57:24 +02:00
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
function readJson(path: string) {
|
|
|
|
return JSON.parse(readFileSync(path, 'utf-8'))
|
2020-06-22 23:12:36 +02:00
|
|
|
}
|
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
export function getBuildManifest(dir: string) {
|
2020-06-22 23:12:36 +02:00
|
|
|
return readJson(path.join(dir, '.next/build-manifest.json'))
|
2020-06-11 10:57:24 +02:00
|
|
|
}
|
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
export function getPageFileFromBuildManifest(dir: string, page: string) {
|
2020-06-11 10:57:24 +02:00
|
|
|
const buildManifest = getBuildManifest(dir)
|
|
|
|
const pageFiles = buildManifest.pages[page]
|
|
|
|
if (!pageFiles) {
|
|
|
|
throw new Error(`No files for page ${page}`)
|
|
|
|
}
|
|
|
|
|
2024-02-21 21:49:49 +01:00
|
|
|
const pageFile = pageFiles[pageFiles.length - 1]
|
|
|
|
expect(pageFile).toEndWith('.js')
|
|
|
|
if (!process.env.TURBOPACK) {
|
|
|
|
expect(pageFile).toInclude(`pages${page === '' ? '/index' : page}`)
|
|
|
|
}
|
2020-06-11 10:57:24 +02:00
|
|
|
if (!pageFile) {
|
|
|
|
throw new Error(`No page file for page ${page}`)
|
|
|
|
}
|
|
|
|
|
|
|
|
return pageFile
|
|
|
|
}
|
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
export function readNextBuildClientPageFile(appDir: string, page: string) {
|
2020-06-11 10:57:24 +02:00
|
|
|
const pageFile = getPageFileFromBuildManifest(appDir, page)
|
|
|
|
return readFileSync(path.join(appDir, '.next', pageFile), 'utf8')
|
|
|
|
}
|
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
export function getPagesManifest(dir: string) {
|
2020-06-22 23:12:36 +02:00
|
|
|
const serverFile = path.join(dir, '.next/server/pages-manifest.json')
|
|
|
|
|
2023-02-01 11:00:52 +01:00
|
|
|
return readJson(serverFile)
|
2020-06-11 10:57:24 +02:00
|
|
|
}
|
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
export function updatePagesManifest(dir: string, content: any) {
|
2021-02-23 23:15:46 +01:00
|
|
|
const serverFile = path.join(dir, '.next/server/pages-manifest.json')
|
|
|
|
|
2023-02-01 11:00:52 +01:00
|
|
|
return writeFile(serverFile, content)
|
2021-02-23 23:15:46 +01:00
|
|
|
}
|
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
export function getPageFileFromPagesManifest(dir: string, page: string) {
|
2020-06-11 10:57:24 +02:00
|
|
|
const pagesManifest = getPagesManifest(dir)
|
|
|
|
const pageFile = pagesManifest[page]
|
|
|
|
if (!pageFile) {
|
|
|
|
throw new Error(`No file for page ${page}`)
|
|
|
|
}
|
|
|
|
|
|
|
|
return pageFile
|
|
|
|
}
|
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
export function readNextBuildServerPageFile(appDir: string, page: string) {
|
2020-06-11 10:57:24 +02:00
|
|
|
const pageFile = getPageFileFromPagesManifest(appDir, page)
|
|
|
|
return readFileSync(path.join(appDir, '.next', 'server', pageFile), 'utf8')
|
|
|
|
}
|
test: organize react 18 tests (#36003)
* Organize react 18 test cases, group invalid cases to speed up the regular test cases
* Add `runDevSuite` and `runProdSuite` for group next dev/prod test cases
```js
runDevSuite(name, appDir, {
runTests: (context, env) => { ... },
beforeAll,
afterAll,
})
runProdvSuite(name, appDir, {
runTests: (context, env) => { ... },
beforeAll,
afterAll,
})
```
2022-04-08 17:29:35 +02:00
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
function runSuite(
|
|
|
|
suiteName: string,
|
|
|
|
context: { env: 'prod' | 'dev'; appDir: string } & Partial<{
|
|
|
|
stderr: string
|
|
|
|
stdout: string
|
|
|
|
appPort: number
|
|
|
|
code: number
|
|
|
|
server: ChildProcess
|
|
|
|
}>,
|
|
|
|
options: {
|
|
|
|
beforeAll?: Function
|
|
|
|
afterAll?: Function
|
|
|
|
runTests: Function
|
|
|
|
} & NextDevOptions
|
|
|
|
) {
|
test: organize react 18 tests (#36003)
* Organize react 18 test cases, group invalid cases to speed up the regular test cases
* Add `runDevSuite` and `runProdSuite` for group next dev/prod test cases
```js
runDevSuite(name, appDir, {
runTests: (context, env) => { ... },
beforeAll,
afterAll,
})
runProdvSuite(name, appDir, {
runTests: (context, env) => { ... },
beforeAll,
afterAll,
})
```
2022-04-08 17:29:35 +02:00
|
|
|
const { appDir, env } = context
|
|
|
|
describe(`${suiteName} ${env}`, () => {
|
|
|
|
beforeAll(async () => {
|
|
|
|
options.beforeAll?.(env)
|
|
|
|
context.stderr = ''
|
|
|
|
const onStderr = (msg) => {
|
|
|
|
context.stderr += msg
|
|
|
|
}
|
2022-04-21 18:01:47 +02:00
|
|
|
context.stdout = ''
|
|
|
|
const onStdout = (msg) => {
|
|
|
|
context.stdout += msg
|
|
|
|
}
|
test: organize react 18 tests (#36003)
* Organize react 18 test cases, group invalid cases to speed up the regular test cases
* Add `runDevSuite` and `runProdSuite` for group next dev/prod test cases
```js
runDevSuite(name, appDir, {
runTests: (context, env) => { ... },
beforeAll,
afterAll,
})
runProdvSuite(name, appDir, {
runTests: (context, env) => { ... },
beforeAll,
afterAll,
})
```
2022-04-08 17:29:35 +02:00
|
|
|
if (env === 'prod') {
|
|
|
|
context.appPort = await findPort()
|
|
|
|
const { stdout, stderr, code } = await nextBuild(appDir, [], {
|
|
|
|
stderr: true,
|
|
|
|
stdout: true,
|
2022-07-02 01:57:45 +02:00
|
|
|
env: options.env || {},
|
2022-07-06 19:35:20 +02:00
|
|
|
nodeArgs: options.nodeArgs,
|
test: organize react 18 tests (#36003)
* Organize react 18 test cases, group invalid cases to speed up the regular test cases
* Add `runDevSuite` and `runProdSuite` for group next dev/prod test cases
```js
runDevSuite(name, appDir, {
runTests: (context, env) => { ... },
beforeAll,
afterAll,
})
runProdvSuite(name, appDir, {
runTests: (context, env) => { ... },
beforeAll,
afterAll,
})
```
2022-04-08 17:29:35 +02:00
|
|
|
})
|
|
|
|
context.stdout = stdout
|
|
|
|
context.stderr = stderr
|
|
|
|
context.code = code
|
|
|
|
context.server = await nextStart(context.appDir, context.appPort, {
|
|
|
|
onStderr,
|
2022-04-21 18:01:47 +02:00
|
|
|
onStdout,
|
2022-07-02 01:57:45 +02:00
|
|
|
env: options.env || {},
|
2022-07-06 19:35:20 +02:00
|
|
|
nodeArgs: options.nodeArgs,
|
test: organize react 18 tests (#36003)
* Organize react 18 test cases, group invalid cases to speed up the regular test cases
* Add `runDevSuite` and `runProdSuite` for group next dev/prod test cases
```js
runDevSuite(name, appDir, {
runTests: (context, env) => { ... },
beforeAll,
afterAll,
})
runProdvSuite(name, appDir, {
runTests: (context, env) => { ... },
beforeAll,
afterAll,
})
```
2022-04-08 17:29:35 +02:00
|
|
|
})
|
|
|
|
} else if (env === 'dev') {
|
|
|
|
context.appPort = await findPort()
|
|
|
|
context.server = await launchApp(context.appDir, context.appPort, {
|
|
|
|
onStderr,
|
2022-04-21 18:01:47 +02:00
|
|
|
onStdout,
|
2022-07-02 01:57:45 +02:00
|
|
|
env: options.env || {},
|
2022-07-06 19:35:20 +02:00
|
|
|
nodeArgs: options.nodeArgs,
|
test: organize react 18 tests (#36003)
* Organize react 18 test cases, group invalid cases to speed up the regular test cases
* Add `runDevSuite` and `runProdSuite` for group next dev/prod test cases
```js
runDevSuite(name, appDir, {
runTests: (context, env) => { ... },
beforeAll,
afterAll,
})
runProdvSuite(name, appDir, {
runTests: (context, env) => { ... },
beforeAll,
afterAll,
})
```
2022-04-08 17:29:35 +02:00
|
|
|
})
|
|
|
|
}
|
|
|
|
})
|
|
|
|
afterAll(async () => {
|
|
|
|
options.afterAll?.(env)
|
|
|
|
if (context.server) {
|
|
|
|
await killApp(context.server)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
options.runTests(context, env)
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
export function runDevSuite(
|
|
|
|
suiteName: string,
|
|
|
|
appDir: string,
|
|
|
|
options: {
|
|
|
|
beforeAll?: Function
|
|
|
|
afterAll?: Function
|
|
|
|
runTests: Function
|
|
|
|
env?: NodeJS.ProcessEnv
|
|
|
|
}
|
|
|
|
) {
|
test: organize react 18 tests (#36003)
* Organize react 18 test cases, group invalid cases to speed up the regular test cases
* Add `runDevSuite` and `runProdSuite` for group next dev/prod test cases
```js
runDevSuite(name, appDir, {
runTests: (context, env) => { ... },
beforeAll,
afterAll,
})
runProdvSuite(name, appDir, {
runTests: (context, env) => { ... },
beforeAll,
afterAll,
})
```
2022-04-08 17:29:35 +02:00
|
|
|
return runSuite(suiteName, { appDir, env: 'dev' }, options)
|
|
|
|
}
|
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
export function runProdSuite(
|
|
|
|
suiteName: string,
|
|
|
|
appDir: string,
|
|
|
|
options: {
|
|
|
|
beforeAll?: Function
|
|
|
|
afterAll?: Function
|
|
|
|
runTests: Function
|
|
|
|
env?: NodeJS.ProcessEnv
|
|
|
|
}
|
|
|
|
) {
|
2023-10-02 09:42:32 +02:00
|
|
|
;(process.env.TURBOPACK ? describe.skip : describe)('production mode', () => {
|
|
|
|
runSuite(suiteName, { appDir, env: 'prod' }, options)
|
|
|
|
})
|
test: organize react 18 tests (#36003)
* Organize react 18 test cases, group invalid cases to speed up the regular test cases
* Add `runDevSuite` and `runProdSuite` for group next dev/prod test cases
```js
runDevSuite(name, appDir, {
runTests: (context, env) => { ... },
beforeAll,
afterAll,
})
runProdvSuite(name, appDir, {
runTests: (context, env) => { ... },
beforeAll,
afterAll,
})
```
2022-04-08 17:29:35 +02:00
|
|
|
}
|
2022-11-08 18:02:26 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Parse the output and return all entries that match the provided `eventName`
|
|
|
|
* @param {string} output output of the console
|
|
|
|
* @param {string} eventName
|
|
|
|
* @returns {Array<{}>}
|
|
|
|
*/
|
2023-06-23 19:42:50 +02:00
|
|
|
export function findAllTelemetryEvents(output: string, eventName: string) {
|
2022-11-08 18:02:26 +01:00
|
|
|
const regex = /\[telemetry\] ({.+?^})/gms
|
|
|
|
// Pop the last element of each entry to retrieve contents of the capturing group
|
|
|
|
const events = [...output.matchAll(regex)].map((entry) =>
|
|
|
|
JSON.parse(entry.pop())
|
|
|
|
)
|
|
|
|
return events.filter((e) => e.eventName === eventName).map((e) => e.payload)
|
|
|
|
}
|
2022-11-24 00:00:49 +01:00
|
|
|
|
2023-09-06 19:46:54 +02:00
|
|
|
type TestVariants = 'default' | 'turbo'
|
2023-01-13 21:02:44 +01:00
|
|
|
|
|
|
|
// WEB-168: There are some differences / incompletes in turbopack implementation enforces jest requires to update
|
|
|
|
// test snapshot when run against turbo. This fn returns describe, or describe.skip dependes on the running context
|
|
|
|
// to avoid force-snapshot update per each runs until turbopack update includes all the changes.
|
2023-06-23 19:42:50 +02:00
|
|
|
export function getSnapshotTestDescribe(variant: TestVariants) {
|
2023-01-13 21:02:44 +01:00
|
|
|
const runningEnv = variant ?? 'default'
|
2023-09-06 19:46:54 +02:00
|
|
|
if (runningEnv !== 'default' && runningEnv !== 'turbo') {
|
2023-06-21 21:47:21 +02:00
|
|
|
throw new Error(
|
2023-09-06 19:46:54 +02:00
|
|
|
`An invalid test env was passed: ${variant} (only "default" and "turbo" are valid options)`
|
2023-06-21 21:47:21 +02:00
|
|
|
)
|
2023-01-13 21:02:44 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
const shouldRunTurboDev = shouldRunTurboDevTest()
|
|
|
|
const shouldSkip =
|
|
|
|
(runningEnv === 'turbo' && !shouldRunTurboDev) ||
|
2023-09-06 19:46:54 +02:00
|
|
|
(runningEnv === 'default' && shouldRunTurboDev)
|
2023-01-13 21:02:44 +01:00
|
|
|
|
|
|
|
return shouldSkip ? describe.skip : describe
|
|
|
|
}
|
2023-06-21 21:47:21 +02:00
|
|
|
|
2023-10-05 01:33:21 +02:00
|
|
|
export async function getRedboxComponentStack(
|
|
|
|
browser: BrowserInterface
|
|
|
|
): Promise<string> {
|
|
|
|
await browser.waitForElementByCss(
|
2024-02-28 16:14:48 +01:00
|
|
|
'[data-nextjs-container-errors-pseudo-html]',
|
2023-10-05 01:33:21 +02:00
|
|
|
30000
|
|
|
|
)
|
|
|
|
// TODO: the type for elementsByCss is incorrect
|
|
|
|
const componentStackFrameElements: any = await browser.elementsByCss(
|
2024-02-28 16:14:48 +01:00
|
|
|
'[data-nextjs-container-errors-pseudo-html]'
|
2023-10-05 01:33:21 +02:00
|
|
|
)
|
|
|
|
const componentStackFrameTexts = await Promise.all(
|
|
|
|
componentStackFrameElements.map((f) => f.innerText())
|
|
|
|
)
|
|
|
|
|
2024-01-18 13:44:03 +01:00
|
|
|
return componentStackFrameTexts.join('\n').trim()
|
2023-10-05 01:33:21 +02:00
|
|
|
}
|
|
|
|
|
2024-02-28 16:14:48 +01:00
|
|
|
export async function toggleComponentStack(
|
|
|
|
browser: BrowserInterface
|
|
|
|
): Promise<void> {
|
|
|
|
await browser
|
|
|
|
.elementByCss('[data-nextjs-container-errors-pseudo-html-collapse]')
|
|
|
|
.click()
|
|
|
|
}
|
|
|
|
|
2024-02-08 16:53:43 +01:00
|
|
|
export async function expandCallStack(
|
|
|
|
browser: BrowserInterface
|
|
|
|
): Promise<void> {
|
|
|
|
// Open full Call Stack
|
|
|
|
await browser
|
|
|
|
.elementByCss('[data-nextjs-data-runtime-error-collapsed-action]')
|
|
|
|
.click()
|
|
|
|
}
|
|
|
|
|
|
|
|
export async function getRedboxCallStack(
|
|
|
|
browser: BrowserInterface
|
|
|
|
): Promise<string> {
|
|
|
|
await browser.waitForElementByCss('[data-nextjs-call-stack-frame]', 30000)
|
|
|
|
|
|
|
|
const callStackFrameElements: any = await browser.elementsByCss(
|
|
|
|
'[data-nextjs-call-stack-frame]'
|
|
|
|
)
|
|
|
|
const callStackFrameTexts = await Promise.all(
|
|
|
|
callStackFrameElements.map((f) => f.innerText())
|
|
|
|
)
|
|
|
|
|
|
|
|
return callStackFrameTexts.join('\n').trim()
|
|
|
|
}
|
|
|
|
|
2024-01-24 06:37:18 +01:00
|
|
|
export async function getVersionCheckerText(
|
|
|
|
browser: BrowserInterface
|
|
|
|
): Promise<string> {
|
|
|
|
await browser.waitForElementByCss('[data-nextjs-version-checker]', 30000)
|
|
|
|
const versionCheckerElement = await browser.elementByCss(
|
|
|
|
'[data-nextjs-version-checker]'
|
|
|
|
)
|
|
|
|
const versionCheckerText = await versionCheckerElement.innerText()
|
|
|
|
return versionCheckerText.trim()
|
|
|
|
}
|
|
|
|
|
2023-06-21 21:47:21 +02:00
|
|
|
/**
|
|
|
|
* For better editor support, pass in the variants this should run on (`default` and/or `turbo`) as cases.
|
|
|
|
*
|
|
|
|
* This is necessary if separate snapshots are needed for next.js with webpack vs turbopack.
|
|
|
|
*/
|
|
|
|
export const describeVariants = {
|
2023-06-23 19:42:50 +02:00
|
|
|
each(variants: TestVariants[]) {
|
2023-10-19 19:38:24 +02:00
|
|
|
return (name: string, fn: (variants: TestVariants) => any) => {
|
2023-06-23 19:42:50 +02:00
|
|
|
if (
|
|
|
|
!Array.isArray(variants) ||
|
|
|
|
!variants.every((val) => typeof val === 'string')
|
|
|
|
) {
|
|
|
|
throw new Error('variants need to be an array of strings')
|
|
|
|
}
|
2023-06-21 21:47:21 +02:00
|
|
|
|
2023-06-23 19:42:50 +02:00
|
|
|
for (const variant of variants) {
|
|
|
|
getSnapshotTestDescribe(variant).each([variant])(name, fn)
|
|
|
|
}
|
2023-06-21 21:47:21 +02:00
|
|
|
}
|
|
|
|
},
|
|
|
|
}
|