Add tracing for testing tools (#44046)
This commit is contained in:
parent
daa14ab3e2
commit
ab328c6c39
15 changed files with 509 additions and 353 deletions
|
@ -135,10 +135,10 @@ if (!allowedActions.has(actionInfo.actionName) && !actionInfo.isRelease) {
|
|||
|
||||
logger(`Linking packages in ${dir}`)
|
||||
const isMainRepo = dir === mainRepoDir
|
||||
const pkgPaths = await linkPackages(
|
||||
dir,
|
||||
isMainRepo ? mainNextSwcVersion : undefined
|
||||
)
|
||||
const pkgPaths = await linkPackages({
|
||||
repoDir: dir,
|
||||
nextSwcPkg: isMainRepo ? mainNextSwcVersion : undefined,
|
||||
})
|
||||
|
||||
if (isMainRepo) mainRepoPkgPaths = pkgPaths
|
||||
else diffRepoPkgPaths = pkgPaths
|
||||
|
|
|
@ -5,6 +5,11 @@ const { remove } = require('fs-extra')
|
|||
const logger = require('../util/logger')
|
||||
const semver = require('semver')
|
||||
|
||||
const mockTrace = () => ({
|
||||
traceAsyncFn: (fn) => fn(mockTrace()),
|
||||
traceChild: () => mockTrace(),
|
||||
})
|
||||
|
||||
module.exports = (actionInfo) => {
|
||||
return {
|
||||
async cloneRepo(repoPath = '', dest = '') {
|
||||
|
@ -53,93 +58,115 @@ module.exports = (actionInfo) => {
|
|||
}
|
||||
}
|
||||
},
|
||||
async linkPackages(repoDir = '', nextSwcPkg) {
|
||||
const pkgPaths = new Map()
|
||||
const pkgDatas = new Map()
|
||||
let pkgs
|
||||
async linkPackages({ repoDir = '', nextSwcPkg, parentSpan }) {
|
||||
const rootSpan = parentSpan
|
||||
? parentSpan.traceChild('linkPackages')
|
||||
: mockTrace()
|
||||
|
||||
try {
|
||||
pkgs = await fs.readdir(path.join(repoDir, 'packages'))
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
console.log('no packages to link')
|
||||
return pkgPaths
|
||||
}
|
||||
throw err
|
||||
}
|
||||
return await rootSpan.traceAsyncFn(async () => {
|
||||
const pkgPaths = new Map()
|
||||
const pkgDatas = new Map()
|
||||
let pkgs
|
||||
|
||||
for (const pkg of pkgs) {
|
||||
const pkgPath = path.join(repoDir, 'packages', pkg)
|
||||
const packedPkgPath = path.join(pkgPath, `${pkg}-packed.tgz`)
|
||||
|
||||
const pkgDataPath = path.join(pkgPath, 'package.json')
|
||||
if (!fs.existsSync(pkgDataPath)) {
|
||||
console.log(`Skipping ${pkgDataPath}`)
|
||||
continue
|
||||
}
|
||||
const pkgData = require(pkgDataPath)
|
||||
const { name } = pkgData
|
||||
pkgDatas.set(name, {
|
||||
pkgDataPath,
|
||||
pkg,
|
||||
pkgPath,
|
||||
pkgData,
|
||||
packedPkgPath,
|
||||
})
|
||||
pkgPaths.set(name, packedPkgPath)
|
||||
}
|
||||
|
||||
for (const pkg of pkgDatas.keys()) {
|
||||
const { pkgDataPath, pkgData } = pkgDatas.get(pkg)
|
||||
|
||||
for (const pkg of pkgDatas.keys()) {
|
||||
const { packedPkgPath } = pkgDatas.get(pkg)
|
||||
if (!pkgData.dependencies || !pkgData.dependencies[pkg]) continue
|
||||
pkgData.dependencies[pkg] = packedPkgPath
|
||||
}
|
||||
// make sure native binaries are included in local linking
|
||||
if (pkg === '@next/swc') {
|
||||
if (!pkgData.files) {
|
||||
pkgData.files = []
|
||||
try {
|
||||
pkgs = await fs.readdir(path.join(repoDir, 'packages'))
|
||||
} catch (err) {
|
||||
if (err.code === 'ENOENT') {
|
||||
console.log('no packages to link')
|
||||
return pkgPaths
|
||||
}
|
||||
pkgData.files.push('native')
|
||||
console.log(
|
||||
'using swc binaries: ',
|
||||
await exec(`ls ${path.join(path.dirname(pkgDataPath), 'native')}`)
|
||||
)
|
||||
throw err
|
||||
}
|
||||
if (pkg === 'next') {
|
||||
if (nextSwcPkg) {
|
||||
Object.assign(pkgData.dependencies, nextSwcPkg)
|
||||
} else {
|
||||
if (pkgDatas.get('@next/swc')) {
|
||||
pkgData.dependencies['@next/swc'] =
|
||||
pkgDatas.get('@next/swc').packedPkgPath
|
||||
} else {
|
||||
pkgData.files.push('native')
|
||||
|
||||
await rootSpan
|
||||
.traceChild('prepare packages for packing')
|
||||
.traceAsyncFn(async () => {
|
||||
for (const pkg of pkgs) {
|
||||
const pkgPath = path.join(repoDir, 'packages', pkg)
|
||||
const packedPkgPath = path.join(pkgPath, `${pkg}-packed.tgz`)
|
||||
|
||||
const pkgDataPath = path.join(pkgPath, 'package.json')
|
||||
if (!fs.existsSync(pkgDataPath)) {
|
||||
console.log(`Skipping ${pkgDataPath}`)
|
||||
continue
|
||||
}
|
||||
const pkgData = require(pkgDataPath)
|
||||
const { name } = pkgData
|
||||
pkgDatas.set(name, {
|
||||
pkgDataPath,
|
||||
pkg,
|
||||
pkgPath,
|
||||
pkgData,
|
||||
packedPkgPath,
|
||||
})
|
||||
pkgPaths.set(name, packedPkgPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
await fs.writeFile(
|
||||
pkgDataPath,
|
||||
JSON.stringify(pkgData, null, 2),
|
||||
'utf8'
|
||||
)
|
||||
}
|
||||
|
||||
// wait to pack packages until after dependency paths have been updated
|
||||
// to the correct versions
|
||||
for (const pkgName of pkgDatas.keys()) {
|
||||
const { pkg, pkgPath } = pkgDatas.get(pkgName)
|
||||
await exec(`cd ${pkgPath} && yarn pack -f ${pkg}-packed.tgz`, true, {
|
||||
env: {
|
||||
// Yarn installed through corepack will not run in pnpm project without this env var set
|
||||
// This var works for corepack >=0.15.0
|
||||
COREPACK_ENABLE_STRICT: '0',
|
||||
},
|
||||
})
|
||||
}
|
||||
return pkgPaths
|
||||
for (const pkg of pkgDatas.keys()) {
|
||||
const { pkgDataPath, pkgData } = pkgDatas.get(pkg)
|
||||
|
||||
for (const pkg of pkgDatas.keys()) {
|
||||
const { packedPkgPath } = pkgDatas.get(pkg)
|
||||
if (!pkgData.dependencies || !pkgData.dependencies[pkg])
|
||||
continue
|
||||
pkgData.dependencies[pkg] = packedPkgPath
|
||||
}
|
||||
|
||||
// make sure native binaries are included in local linking
|
||||
if (pkg === '@next/swc') {
|
||||
if (!pkgData.files) {
|
||||
pkgData.files = []
|
||||
}
|
||||
pkgData.files.push('native')
|
||||
console.log(
|
||||
'using swc binaries: ',
|
||||
await exec(
|
||||
`ls ${path.join(path.dirname(pkgDataPath), 'native')}`
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
if (pkg === 'next') {
|
||||
if (nextSwcPkg) {
|
||||
Object.assign(pkgData.dependencies, nextSwcPkg)
|
||||
} else {
|
||||
if (pkgDatas.get('@next/swc')) {
|
||||
pkgData.dependencies['@next/swc'] =
|
||||
pkgDatas.get('@next/swc').packedPkgPath
|
||||
} else {
|
||||
pkgData.files.push('native')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await fs.writeFile(
|
||||
pkgDataPath,
|
||||
JSON.stringify(pkgData, null, 2),
|
||||
'utf8'
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
// wait to pack packages until after dependency paths have been updated
|
||||
// to the correct versions
|
||||
await rootSpan
|
||||
.traceChild('packing packages')
|
||||
.traceAsyncFn(async (packingSpan) => {
|
||||
for (const pkgName of pkgDatas.keys()) {
|
||||
await packingSpan
|
||||
.traceChild(`pack ${pkgName}`)
|
||||
.traceAsyncFn(async () => {
|
||||
const { pkg, pkgPath } = pkgDatas.get(pkgName)
|
||||
await exec(
|
||||
`cd ${pkgPath} && yarn pack -f '${pkg}-packed.tgz'`,
|
||||
true
|
||||
)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return pkgPaths
|
||||
})
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -28,6 +28,7 @@ test/**/tsconfig.json
|
|||
.DS_Store
|
||||
/e2e-tests
|
||||
test/tmp/**
|
||||
test/.trace
|
||||
|
||||
# Editors
|
||||
**/.idea
|
||||
|
|
|
@ -76,3 +76,7 @@ Some test-specific environment variables can be used to help debug isolated test
|
|||
### Debugging
|
||||
|
||||
When tests are run in CI and a test failure occurs we attempt to capture traces of the playwright run to make debugging the failure easier. A test-trace artifact should be uploaded after the workflow completes which can be downloaded, unzipped, and then inspected with `pnpm playwright show-trace ./path/to/trace`
|
||||
|
||||
### Profiling tests
|
||||
|
||||
Add `NEXT_TEST_TRACE=1` to enable test profiling. It's useful for improving our testing infrastructure.
|
||||
|
|
|
@ -93,17 +93,17 @@ export class Span {
|
|||
this.attrs[key] = String(value)
|
||||
}
|
||||
|
||||
traceFn<T>(fn: () => T): T {
|
||||
traceFn<T>(fn: (span: Span) => T): T {
|
||||
try {
|
||||
return fn()
|
||||
return fn(this)
|
||||
} finally {
|
||||
this.stop()
|
||||
}
|
||||
}
|
||||
|
||||
async traceAsyncFn<T>(fn: () => T | Promise<T>): Promise<T> {
|
||||
async traceAsyncFn<T>(fn: (span: Span) => T | Promise<T>): Promise<T> {
|
||||
try {
|
||||
return await fn()
|
||||
return await fn(this)
|
||||
} finally {
|
||||
this.stop()
|
||||
}
|
||||
|
|
12
run-tests.js
12
run-tests.js
|
@ -29,6 +29,11 @@ const testFilters = {
|
|||
development: 'development/',
|
||||
}
|
||||
|
||||
const mockTrace = () => ({
|
||||
traceAsyncFn: (fn) => fn(mockTrace()),
|
||||
traceChild: () => mockTrace(),
|
||||
})
|
||||
|
||||
// which types we have configured to run separate
|
||||
const configuredTestTypes = Object.values(testFilters)
|
||||
|
||||
|
@ -223,8 +228,11 @@ async function main() {
|
|||
console.log('Creating Next.js install for isolated tests')
|
||||
const reactVersion = process.env.NEXT_TEST_REACT_VERSION || 'latest'
|
||||
const testStarter = await createNextInstall({
|
||||
react: reactVersion,
|
||||
'react-dom': reactVersion,
|
||||
parentSpan: mockTrace(),
|
||||
dependencies: {
|
||||
react: reactVersion,
|
||||
'react-dom': reactVersion,
|
||||
},
|
||||
})
|
||||
process.env.NEXT_TEST_STARTER = testStarter
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ async function main() {
|
|||
console.log('using repodir', repoDir)
|
||||
await fs.ensureDir(workDir)
|
||||
|
||||
const pkgPaths = await linkPackages(repoDir)
|
||||
const pkgPaths = await linkPackages({ repoDir })
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(workDir, 'package.json'),
|
||||
|
|
24
test/e2e/test-utils-tests/basic/basic.test.ts
Normal file
24
test/e2e/test-utils-tests/basic/basic.test.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
import { createNext, FileRef } from 'e2e-utils'
|
||||
import { NextInstance } from 'test/lib/next-modes/base'
|
||||
import { fetchViaHTTP } from 'next-test-utils'
|
||||
|
||||
describe('createNext', () => {
|
||||
let next: NextInstance
|
||||
|
||||
beforeAll(async () => {
|
||||
next = await createNext({
|
||||
files: new FileRef(__dirname),
|
||||
dependencies: {
|
||||
typescript: 'latest',
|
||||
'@types/react': 'latest',
|
||||
'@types/node': 'latest',
|
||||
},
|
||||
})
|
||||
})
|
||||
afterAll(() => next.destroy())
|
||||
|
||||
it('should work', async () => {
|
||||
const res = await fetchViaHTTP(next.url, '/')
|
||||
expect(await res.text()).toContain('Hello World')
|
||||
})
|
||||
})
|
3
test/e2e/test-utils-tests/basic/pages/index.tsx
Normal file
3
test/e2e/test-utils-tests/basic/pages/index.tsx
Normal file
|
@ -0,0 +1,3 @@
|
|||
export default function Page() {
|
||||
return <h1>Hello World</h1>
|
||||
}
|
|
@ -7,115 +7,151 @@ const { randomBytes } = require('crypto')
|
|||
const { linkPackages } =
|
||||
require('../../.github/actions/next-stats-action/src/prepare/repo-setup')()
|
||||
|
||||
async function createNextInstall(
|
||||
async function createNextInstall({
|
||||
parentSpan,
|
||||
dependencies,
|
||||
installCommand,
|
||||
packageJson = {},
|
||||
packageLockPath = '',
|
||||
dirSuffix = ''
|
||||
) {
|
||||
const tmpDir = await fs.realpath(process.env.NEXT_TEST_DIR || os.tmpdir())
|
||||
const origRepoDir = path.join(__dirname, '../../')
|
||||
const installDir = path.join(
|
||||
tmpDir,
|
||||
`next-install-${randomBytes(32).toString('hex')}${dirSuffix}`
|
||||
)
|
||||
const tmpRepoDir = path.join(
|
||||
tmpDir,
|
||||
`next-repo-${randomBytes(32).toString('hex')}${dirSuffix}`
|
||||
)
|
||||
dirSuffix = '',
|
||||
}) {
|
||||
return await parentSpan
|
||||
.traceChild('createNextInstall')
|
||||
.traceAsyncFn(async (rootSpan) => {
|
||||
const tmpDir = await fs.realpath(process.env.NEXT_TEST_DIR || os.tmpdir())
|
||||
const origRepoDir = path.join(__dirname, '../../')
|
||||
const installDir = path.join(
|
||||
tmpDir,
|
||||
`next-install-${randomBytes(32).toString('hex')}${dirSuffix}`
|
||||
)
|
||||
const tmpRepoDir = path.join(
|
||||
tmpDir,
|
||||
`next-repo-${randomBytes(32).toString('hex')}${dirSuffix}`
|
||||
)
|
||||
|
||||
// ensure swc binary is present in the native folder if
|
||||
// not already built
|
||||
for (const folder of await fs.readdir(
|
||||
path.join(origRepoDir, 'node_modules/@next')
|
||||
)) {
|
||||
if (folder.startsWith('swc-')) {
|
||||
const swcPkgPath = path.join(origRepoDir, 'node_modules/@next', folder)
|
||||
const outputPath = path.join(origRepoDir, 'packages/next-swc/native')
|
||||
await fs.copy(swcPkgPath, outputPath, {
|
||||
filter: (item) => {
|
||||
return (
|
||||
item === swcPkgPath ||
|
||||
(item.endsWith('.node') &&
|
||||
!fs.pathExistsSync(path.join(outputPath, path.basename(item))))
|
||||
)
|
||||
},
|
||||
await rootSpan.traceChild(' enruse swc binary').traceAsyncFn(async () => {
|
||||
// ensure swc binary is present in the native folder if
|
||||
// not already built
|
||||
for (const folder of await fs.readdir(
|
||||
path.join(origRepoDir, 'node_modules/@next')
|
||||
)) {
|
||||
if (folder.startsWith('swc-')) {
|
||||
const swcPkgPath = path.join(
|
||||
origRepoDir,
|
||||
'node_modules/@next',
|
||||
folder
|
||||
)
|
||||
const outputPath = path.join(
|
||||
origRepoDir,
|
||||
'packages/next-swc/native'
|
||||
)
|
||||
await fs.copy(swcPkgPath, outputPath, {
|
||||
filter: (item) => {
|
||||
return (
|
||||
item === swcPkgPath ||
|
||||
(item.endsWith('.node') &&
|
||||
!fs.pathExistsSync(
|
||||
path.join(outputPath, path.basename(item))
|
||||
))
|
||||
)
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
for (const item of ['package.json', 'packages']) {
|
||||
await fs.copy(path.join(origRepoDir, item), path.join(tmpRepoDir, item), {
|
||||
filter: (item) => {
|
||||
return (
|
||||
!item.includes('node_modules') &&
|
||||
!item.includes('.DS_Store') &&
|
||||
// Exclude Rust compilation files
|
||||
!/next[\\/]build[\\/]swc[\\/]target/.test(item) &&
|
||||
!/next-swc[\\/]target/.test(item)
|
||||
for (const item of ['package.json', 'packages']) {
|
||||
await rootSpan
|
||||
.traceChild(`copy ${item} to temp dir`)
|
||||
.traceAsyncFn(async () => {
|
||||
await fs.copy(
|
||||
path.join(origRepoDir, item),
|
||||
path.join(tmpRepoDir, item),
|
||||
{
|
||||
filter: (item) => {
|
||||
return (
|
||||
!item.includes('node_modules') &&
|
||||
!item.includes('.DS_Store') &&
|
||||
// Exclude Rust compilation files
|
||||
!/next[\\/]build[\\/]swc[\\/]target/.test(item) &&
|
||||
!/next-swc[\\/]target/.test(item)
|
||||
)
|
||||
},
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
let combinedDependencies = dependencies
|
||||
|
||||
if (!(packageJson && packageJson.nextPrivateSkipLocalDeps)) {
|
||||
const pkgPaths = await linkPackages({
|
||||
repoDir: tmpRepoDir,
|
||||
parentSpan: rootSpan,
|
||||
})
|
||||
combinedDependencies = {
|
||||
next: pkgPaths.get('next'),
|
||||
...Object.keys(dependencies).reduce((prev, pkg) => {
|
||||
const pkgPath = pkgPaths.get(pkg)
|
||||
prev[pkg] = pkgPath || dependencies[pkg]
|
||||
return prev
|
||||
}, {}),
|
||||
}
|
||||
}
|
||||
|
||||
await fs.ensureDir(installDir)
|
||||
await fs.writeFile(
|
||||
path.join(installDir, 'package.json'),
|
||||
JSON.stringify(
|
||||
{
|
||||
...packageJson,
|
||||
dependencies: combinedDependencies,
|
||||
private: true,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
},
|
||||
)
|
||||
|
||||
if (packageLockPath) {
|
||||
await fs.copy(
|
||||
packageLockPath,
|
||||
path.join(installDir, path.basename(packageLockPath))
|
||||
)
|
||||
}
|
||||
|
||||
if (installCommand) {
|
||||
const installString =
|
||||
typeof installCommand === 'function'
|
||||
? installCommand({ dependencies: combinedDependencies })
|
||||
: installCommand
|
||||
|
||||
console.log('running install command', installString)
|
||||
rootSpan.traceChild('run custom install').traceFn(() => {
|
||||
childProcess.execSync(installString, {
|
||||
cwd: installDir,
|
||||
stdio: ['ignore', 'inherit', 'inherit'],
|
||||
})
|
||||
})
|
||||
} else {
|
||||
await rootSpan
|
||||
.traceChild('run generic install command')
|
||||
.traceAsyncFn(async () => {
|
||||
await execa(
|
||||
'pnpm',
|
||||
['install', '--strict-peer-dependencies=false'],
|
||||
{
|
||||
cwd: installDir,
|
||||
stdio: ['ignore', 'inherit', 'inherit'],
|
||||
env: process.env,
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
await fs.remove(tmpRepoDir)
|
||||
return installDir
|
||||
})
|
||||
}
|
||||
|
||||
let combinedDependencies = dependencies
|
||||
|
||||
if (!(packageJson && packageJson.nextPrivateSkipLocalDeps)) {
|
||||
const pkgPaths = await linkPackages(tmpRepoDir)
|
||||
combinedDependencies = {
|
||||
next: pkgPaths.get('next'),
|
||||
...Object.keys(dependencies).reduce((prev, pkg) => {
|
||||
const pkgPath = pkgPaths.get(pkg)
|
||||
prev[pkg] = pkgPath || dependencies[pkg]
|
||||
return prev
|
||||
}, {}),
|
||||
}
|
||||
}
|
||||
|
||||
await fs.ensureDir(installDir)
|
||||
await fs.writeFile(
|
||||
path.join(installDir, 'package.json'),
|
||||
JSON.stringify(
|
||||
{
|
||||
...packageJson,
|
||||
dependencies: combinedDependencies,
|
||||
private: true,
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
|
||||
if (packageLockPath) {
|
||||
await fs.copy(
|
||||
packageLockPath,
|
||||
path.join(installDir, path.basename(packageLockPath))
|
||||
)
|
||||
}
|
||||
|
||||
if (installCommand) {
|
||||
const installString =
|
||||
typeof installCommand === 'function'
|
||||
? installCommand({ dependencies: combinedDependencies })
|
||||
: installCommand
|
||||
|
||||
console.log('running install command', installString)
|
||||
|
||||
childProcess.execSync(installString, {
|
||||
cwd: installDir,
|
||||
stdio: ['ignore', 'inherit', 'inherit'],
|
||||
})
|
||||
} else {
|
||||
await execa('pnpm', ['install', '--strict-peer-dependencies=false'], {
|
||||
cwd: installDir,
|
||||
stdio: ['ignore', 'inherit', 'inherit'],
|
||||
env: process.env,
|
||||
})
|
||||
}
|
||||
|
||||
await fs.remove(tmpRepoDir)
|
||||
return installDir
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import path from 'path'
|
||||
import assert from 'assert'
|
||||
import { flushAllTraces, setGlobal, trace } from 'next/trace'
|
||||
import { PHASE_DEVELOPMENT_SERVER } from 'next/constants'
|
||||
import { NextInstance, NextInstanceOpts } from './next-modes/base'
|
||||
import { NextDevInstance } from './next-modes/next-dev'
|
||||
import { NextStartInstance } from './next-modes/next-start'
|
||||
|
@ -105,6 +107,15 @@ if (typeof afterAll === 'function') {
|
|||
})
|
||||
}
|
||||
|
||||
const setupTracing = () => {
|
||||
if (!process.env.NEXT_TEST_TRACE) return
|
||||
|
||||
setGlobal('distDir', './test/.trace')
|
||||
// This is a hacky way to use tracing utils even for tracing test utils.
|
||||
// We want the same treatment as DEVELOPMENT_SERVER - adds a reasonable treshold for logs size.
|
||||
setGlobal('phase', PHASE_DEVELOPMENT_SERVER)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up and manages a Next.js instance in the configured
|
||||
* test mode. The next instance will be isolated from the monorepo
|
||||
|
@ -118,45 +129,61 @@ export async function createNext(
|
|||
throw new Error(`createNext called without destroying previous instance`)
|
||||
}
|
||||
|
||||
const useTurbo = !!process.env.TEST_WASM
|
||||
? false
|
||||
: opts?.turbo ?? shouldRunTurboDevTest()
|
||||
setupTracing()
|
||||
return await trace('createNext').traceAsyncFn(async (rootSpan) => {
|
||||
const useTurbo = !!process.env.TEST_WASM
|
||||
? false
|
||||
: opts?.turbo ?? shouldRunTurboDevTest()
|
||||
|
||||
if (testMode === 'dev') {
|
||||
// next dev
|
||||
nextInstance = new NextDevInstance({
|
||||
...opts,
|
||||
turbo: useTurbo,
|
||||
})
|
||||
} else if (testMode === 'deploy') {
|
||||
// Vercel
|
||||
nextInstance = new NextDeployInstance({
|
||||
...opts,
|
||||
turbo: false,
|
||||
})
|
||||
} else {
|
||||
// next build + next start
|
||||
nextInstance = new NextStartInstance({
|
||||
...opts,
|
||||
turbo: false,
|
||||
})
|
||||
}
|
||||
if (testMode === 'dev') {
|
||||
// next dev
|
||||
rootSpan.traceChild('init next dev instance').traceFn(() => {
|
||||
nextInstance = new NextDevInstance({
|
||||
...opts,
|
||||
turbo: useTurbo,
|
||||
})
|
||||
})
|
||||
} else if (testMode === 'deploy') {
|
||||
// Vercel
|
||||
rootSpan.traceChild('init next deploy instance').traceFn(() => {
|
||||
nextInstance = new NextDeployInstance({
|
||||
...opts,
|
||||
turbo: false,
|
||||
})
|
||||
})
|
||||
} else {
|
||||
// next build + next start
|
||||
rootSpan.traceChild('init next start instance').traceFn(() => {
|
||||
nextInstance = new NextStartInstance({
|
||||
...opts,
|
||||
turbo: false,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
nextInstance.on('destroy', () => {
|
||||
nextInstance = undefined
|
||||
nextInstance.on('destroy', () => {
|
||||
nextInstance = undefined
|
||||
})
|
||||
|
||||
await nextInstance.setup(rootSpan)
|
||||
|
||||
if (!opts.skipStart) {
|
||||
await rootSpan
|
||||
.traceChild('start next instance')
|
||||
.traceAsyncFn(async () => {
|
||||
await nextInstance.start()
|
||||
})
|
||||
}
|
||||
|
||||
return nextInstance!
|
||||
})
|
||||
|
||||
await nextInstance.setup()
|
||||
|
||||
if (!opts.skipStart) {
|
||||
await nextInstance.start()
|
||||
}
|
||||
return nextInstance!
|
||||
} catch (err) {
|
||||
require('console').error('Failed to create next instance', err)
|
||||
try {
|
||||
nextInstance.destroy()
|
||||
} catch (_) {}
|
||||
process.exit(1)
|
||||
} finally {
|
||||
flushAllTraces()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import { NextConfig } from 'next'
|
|||
import { FileRef } from '../e2e-utils'
|
||||
import { ChildProcess } from 'child_process'
|
||||
import { createNextInstall } from '../create-next-install'
|
||||
import { Span } from 'next/trace'
|
||||
|
||||
type Event = 'stdout' | 'stderr' | 'error' | 'destroy'
|
||||
export type InstallCommand =
|
||||
|
@ -84,148 +85,170 @@ export class NextInstance {
|
|||
|
||||
protected async createTestDir({
|
||||
skipInstall = false,
|
||||
}: { skipInstall?: boolean } = {}) {
|
||||
parentSpan,
|
||||
}: {
|
||||
skipInstall?: boolean
|
||||
parentSpan: Span
|
||||
}) {
|
||||
if (this.isDestroyed) {
|
||||
throw new Error('next instance already destroyed')
|
||||
}
|
||||
require('console').log(`Creating test directory with isolated next...`)
|
||||
|
||||
const skipIsolatedNext = !!process.env.NEXT_SKIP_ISOLATE
|
||||
const tmpDir = skipIsolatedNext
|
||||
? path.join(__dirname, '../../tmp')
|
||||
: process.env.NEXT_TEST_DIR || (await fs.realpath(os.tmpdir()))
|
||||
this.testDir = path.join(
|
||||
tmpDir,
|
||||
`next-test-${Date.now()}-${(Math.random() * 1000) | 0}${this.dirSuffix}`
|
||||
)
|
||||
|
||||
const reactVersion = process.env.NEXT_TEST_REACT_VERSION || 'latest'
|
||||
const finalDependencies = {
|
||||
react: reactVersion,
|
||||
'react-dom': reactVersion,
|
||||
...this.dependencies,
|
||||
...this.packageJson?.dependencies,
|
||||
}
|
||||
|
||||
if (skipInstall) {
|
||||
const pkgScripts = (this.packageJson['scripts'] as {}) || {}
|
||||
await fs.ensureDir(this.testDir)
|
||||
await fs.writeFile(
|
||||
path.join(this.testDir, 'package.json'),
|
||||
JSON.stringify(
|
||||
{
|
||||
...this.packageJson,
|
||||
dependencies: {
|
||||
...finalDependencies,
|
||||
next:
|
||||
process.env.NEXT_TEST_VERSION ||
|
||||
require('next/package.json').version,
|
||||
},
|
||||
scripts: {
|
||||
...pkgScripts,
|
||||
build:
|
||||
(pkgScripts['build'] || this.buildCommand || 'next build') +
|
||||
' && yarn post-build',
|
||||
// since we can't get the build id as a build artifact, make it
|
||||
// available under the static files
|
||||
'post-build': 'cp .next/BUILD_ID .next/static/__BUILD_ID',
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
await parentSpan
|
||||
.traceChild('createTestDir')
|
||||
.traceAsyncFn(async (rootSpan) => {
|
||||
const skipIsolatedNext = !!process.env.NEXT_SKIP_ISOLATE
|
||||
const tmpDir = skipIsolatedNext
|
||||
? path.join(__dirname, '../../tmp')
|
||||
: process.env.NEXT_TEST_DIR || (await fs.realpath(os.tmpdir()))
|
||||
this.testDir = path.join(
|
||||
tmpDir,
|
||||
`next-test-${Date.now()}-${(Math.random() * 1000) | 0}${
|
||||
this.dirSuffix
|
||||
}`
|
||||
)
|
||||
)
|
||||
} else {
|
||||
if (
|
||||
process.env.NEXT_TEST_STARTER &&
|
||||
!this.dependencies &&
|
||||
!this.installCommand &&
|
||||
!this.packageJson &&
|
||||
!(global as any).isNextDeploy
|
||||
) {
|
||||
await fs.copy(process.env.NEXT_TEST_STARTER, this.testDir)
|
||||
} else if (!skipIsolatedNext) {
|
||||
this.testDir = await createNextInstall(
|
||||
finalDependencies,
|
||||
this.installCommand,
|
||||
this.packageJson,
|
||||
this.packageLockPath,
|
||||
this.dirSuffix
|
||||
)
|
||||
}
|
||||
require('console').log('created next.js install, writing test files')
|
||||
}
|
||||
|
||||
await this.writeInitialFiles()
|
||||
const reactVersion = process.env.NEXT_TEST_REACT_VERSION || 'latest'
|
||||
const finalDependencies = {
|
||||
react: reactVersion,
|
||||
'react-dom': reactVersion,
|
||||
...this.dependencies,
|
||||
...this.packageJson?.dependencies,
|
||||
}
|
||||
|
||||
let nextConfigFile = Object.keys(this.files).find((file) =>
|
||||
file.startsWith('next.config.')
|
||||
)
|
||||
if (skipInstall) {
|
||||
const pkgScripts = (this.packageJson['scripts'] as {}) || {}
|
||||
await fs.ensureDir(this.testDir)
|
||||
await fs.writeFile(
|
||||
path.join(this.testDir, 'package.json'),
|
||||
JSON.stringify(
|
||||
{
|
||||
...this.packageJson,
|
||||
dependencies: {
|
||||
...finalDependencies,
|
||||
next:
|
||||
process.env.NEXT_TEST_VERSION ||
|
||||
require('next/package.json').version,
|
||||
},
|
||||
scripts: {
|
||||
...pkgScripts,
|
||||
build:
|
||||
(pkgScripts['build'] || this.buildCommand || 'next build') +
|
||||
' && yarn post-build',
|
||||
// since we can't get the build id as a build artifact, make it
|
||||
// available under the static files
|
||||
'post-build': 'cp .next/BUILD_ID .next/static/__BUILD_ID',
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
)
|
||||
)
|
||||
} else {
|
||||
if (
|
||||
process.env.NEXT_TEST_STARTER &&
|
||||
!this.dependencies &&
|
||||
!this.installCommand &&
|
||||
!this.packageJson &&
|
||||
!(global as any).isNextDeploy
|
||||
) {
|
||||
await fs.copy(process.env.NEXT_TEST_STARTER, this.testDir)
|
||||
} else if (!skipIsolatedNext) {
|
||||
this.testDir = await createNextInstall({
|
||||
parentSpan: rootSpan,
|
||||
dependencies: finalDependencies,
|
||||
installCommand: this.installCommand,
|
||||
packageJson: this.packageJson,
|
||||
packageLockPath: this.packageLockPath,
|
||||
dirSuffix: this.dirSuffix,
|
||||
})
|
||||
}
|
||||
require('console').log('created next.js install, writing test files')
|
||||
}
|
||||
|
||||
if (await fs.pathExists(path.join(this.testDir, 'next.config.js'))) {
|
||||
nextConfigFile = 'next.config.js'
|
||||
}
|
||||
|
||||
if (nextConfigFile && this.nextConfig) {
|
||||
throw new Error(
|
||||
`nextConfig provided on "createNext()" and as a file "${nextConfigFile}", use one or the other to continue`
|
||||
)
|
||||
}
|
||||
|
||||
if (this.nextConfig || ((global as any).isNextDeploy && !nextConfigFile)) {
|
||||
const functions = []
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(this.testDir, 'next.config.js'),
|
||||
`
|
||||
module.exports = ` +
|
||||
JSON.stringify(
|
||||
{
|
||||
...this.nextConfig,
|
||||
} as NextConfig,
|
||||
(key, val) => {
|
||||
if (typeof val === 'function') {
|
||||
functions.push(
|
||||
val
|
||||
.toString()
|
||||
.replace(new RegExp(`${val.name}[\\s]{0,}\\(`), 'function(')
|
||||
)
|
||||
return `__func_${functions.length - 1}`
|
||||
}
|
||||
return val
|
||||
},
|
||||
2
|
||||
).replace(/"__func_[\d]{1,}"/g, function (str) {
|
||||
return functions.shift()
|
||||
await rootSpan
|
||||
.traceChild('writeInitialFiles')
|
||||
.traceAsyncFn(async () => {
|
||||
await this.writeInitialFiles()
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if ((global as any).isNextDeploy) {
|
||||
const fileName = path.join(
|
||||
this.testDir,
|
||||
nextConfigFile || 'next.config.js'
|
||||
)
|
||||
const content = await fs.readFile(fileName, 'utf8')
|
||||
let nextConfigFile = Object.keys(this.files).find((file) =>
|
||||
file.startsWith('next.config.')
|
||||
)
|
||||
|
||||
if (content.includes('basePath')) {
|
||||
this.basePath =
|
||||
content.match(/['"`]?basePath['"`]?:.*?['"`](.*?)['"`]/)?.[1] || ''
|
||||
}
|
||||
if (await fs.pathExists(path.join(this.testDir, 'next.config.js'))) {
|
||||
nextConfigFile = 'next.config.js'
|
||||
}
|
||||
|
||||
await fs.writeFile(
|
||||
fileName,
|
||||
`${content}\n` +
|
||||
`
|
||||
if (nextConfigFile && this.nextConfig) {
|
||||
throw new Error(
|
||||
`nextConfig provided on "createNext()" and as a file "${nextConfigFile}", use one or the other to continue`
|
||||
)
|
||||
}
|
||||
|
||||
if (
|
||||
this.nextConfig ||
|
||||
((global as any).isNextDeploy && !nextConfigFile)
|
||||
) {
|
||||
const functions = []
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(this.testDir, 'next.config.js'),
|
||||
`
|
||||
module.exports = ` +
|
||||
JSON.stringify(
|
||||
{
|
||||
...this.nextConfig,
|
||||
} as NextConfig,
|
||||
(key, val) => {
|
||||
if (typeof val === 'function') {
|
||||
functions.push(
|
||||
val
|
||||
.toString()
|
||||
.replace(
|
||||
new RegExp(`${val.name}[\\s]{0,}\\(`),
|
||||
'function('
|
||||
)
|
||||
)
|
||||
return `__func_${functions.length - 1}`
|
||||
}
|
||||
return val
|
||||
},
|
||||
2
|
||||
).replace(/"__func_[\d]{1,}"/g, function (str) {
|
||||
return functions.shift()
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
if ((global as any).isNextDeploy) {
|
||||
const fileName = path.join(
|
||||
this.testDir,
|
||||
nextConfigFile || 'next.config.js'
|
||||
)
|
||||
const content = await fs.readFile(fileName, 'utf8')
|
||||
|
||||
if (content.includes('basePath')) {
|
||||
this.basePath =
|
||||
content.match(/['"`]?basePath['"`]?:.*?['"`](.*?)['"`]/)?.[1] ||
|
||||
''
|
||||
}
|
||||
|
||||
await fs.writeFile(
|
||||
fileName,
|
||||
`${content}\n` +
|
||||
`
|
||||
// alias __NEXT_TEST_MODE for next-deploy as "_" is not a valid
|
||||
// env variable during deploy
|
||||
if (process.env.NEXT_PRIVATE_TEST_MODE) {
|
||||
process.env.__NEXT_TEST_MODE = process.env.NEXT_PRIVATE_TEST_MODE
|
||||
}
|
||||
`
|
||||
)
|
||||
}
|
||||
require('console').log(`Test directory created at ${this.testDir}`)
|
||||
)
|
||||
}
|
||||
require('console').log(`Test directory created at ${this.testDir}`)
|
||||
})
|
||||
}
|
||||
|
||||
public async clean() {
|
||||
|
@ -245,7 +268,7 @@ export class NextInstance {
|
|||
public async export(): Promise<{ exitCode?: number; cliOutput?: string }> {
|
||||
return {}
|
||||
}
|
||||
public async setup(): Promise<void> {}
|
||||
public async setup(parentSpan: Span): Promise<void> {}
|
||||
public async start(useDirArg: boolean = false): Promise<void> {}
|
||||
public async stop(): Promise<void> {
|
||||
this.isStopping = true
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
TEST_TOKEN,
|
||||
} from '../../../scripts/reset-vercel-project.mjs'
|
||||
import fetch from 'node-fetch'
|
||||
import { Span } from 'next/trace'
|
||||
|
||||
export class NextDeployInstance extends NextInstance {
|
||||
private _cliOutput: string
|
||||
|
@ -20,8 +21,8 @@ export class NextDeployInstance extends NextInstance {
|
|||
return this._buildId
|
||||
}
|
||||
|
||||
public async setup() {
|
||||
await super.createTestDir({ skipInstall: true })
|
||||
public async setup(parentSpan: Span) {
|
||||
await super.createTestDir({ parentSpan, skipInstall: true })
|
||||
|
||||
// ensure Vercel CLI is installed
|
||||
try {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { spawn } from 'child_process'
|
||||
import { Span } from 'next/trace'
|
||||
import { NextInstance } from './base'
|
||||
|
||||
export class NextDevInstance extends NextInstance {
|
||||
|
@ -8,8 +9,8 @@ export class NextDevInstance extends NextInstance {
|
|||
return 'development'
|
||||
}
|
||||
|
||||
public async setup() {
|
||||
await super.createTestDir()
|
||||
public async setup(parentSpan: Span) {
|
||||
await super.createTestDir({ parentSpan })
|
||||
}
|
||||
|
||||
public get cliOutput() {
|
||||
|
|
|
@ -2,6 +2,7 @@ import path from 'path'
|
|||
import fs from 'fs-extra'
|
||||
import { NextInstance } from './base'
|
||||
import { spawn, SpawnOptions } from 'child_process'
|
||||
import { Span } from 'next/trace'
|
||||
|
||||
export class NextStartInstance extends NextInstance {
|
||||
private _buildId: string
|
||||
|
@ -16,8 +17,8 @@ export class NextStartInstance extends NextInstance {
|
|||
return this._cliOutput
|
||||
}
|
||||
|
||||
public async setup() {
|
||||
await super.createTestDir()
|
||||
public async setup(parentSpan: Span) {
|
||||
await super.createTestDir({ parentSpan })
|
||||
}
|
||||
|
||||
private handleStdio = (childProcess) => {
|
||||
|
|
Loading…
Reference in a new issue