import { execSync, spawn } from 'child_process' import { join } from 'path' import { fileURLToPath } from 'url' import fetch from 'node-fetch' import { existsSync, readFileSync, writeFileSync, unlinkSync, promises as fs, } from 'fs' import prettyMs from 'pretty-ms' import treeKill from 'tree-kill' const ROOT_DIR = join(fileURLToPath(import.meta.url), '..', '..', '..') const CWD = join(ROOT_DIR, 'bench', 'nested-deps') const NEXT_BIN = join(ROOT_DIR, 'packages', 'next', 'dist', 'bin', 'next') const [, , command = 'all'] = process.argv async function killApp(instance) { await new Promise((resolve, reject) => { treeKill(instance.pid, (err) => { if (err) { if ( process.platform === 'win32' && typeof err.message === 'string' && (err.message.includes(`no running instance of the task`) || err.message.includes(`not found`)) ) { // Windows throws an error if the process is already stopped // // 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) } resolve() }) }) } class File { constructor(path) { this.path = path this.originalContent = existsSync(this.path) ? readFileSync(this.path, 'utf8') : null } write(content) { if (!this.originalContent) { this.originalContent = content } writeFileSync(this.path, content, 'utf8') } replace(pattern, newValue) { const currentContent = readFileSync(this.path, 'utf8') if (pattern instanceof RegExp) { if (!pattern.test(currentContent)) { throw new Error( `Failed to replace content.\n\nPattern: ${pattern.toString()}\n\nContent: ${currentContent}` ) } } else if (typeof pattern === 'string') { if (!currentContent.includes(pattern)) { throw new Error( `Failed to replace content.\n\nPattern: ${pattern}\n\nContent: ${currentContent}` ) } } else { throw new Error(`Unknown replacement attempt type: ${pattern}`) } const newContent = currentContent.replace(pattern, newValue) this.write(newContent) } prepend(line) { const currentContent = readFileSync(this.path, 'utf8') this.write(line + '\n' + currentContent) } delete() { unlinkSync(this.path) } restore() { this.write(this.originalContent) } } function runNextCommandDev(argv, opts = {}) { const env = { ...process.env, NODE_ENV: undefined, __NEXT_TEST_MODE: 'true', FORCE_COLOR: 3, ...opts.env, } const nodeArgs = opts.nodeArgs || [] return new Promise((resolve, reject) => { const instance = spawn(NEXT_BIN, [...nodeArgs, ...argv], { cwd: CWD, env, }) let didResolve = false function handleStdout(data) { const message = data.toString() const bootupMarkers = { dev: /compiled .*successfully/i, start: /started server/i, } if ( (opts.bootupMarker && opts.bootupMarker.test(message)) || bootupMarkers[opts.nextStart ? 'start' : 'dev'].test(message) ) { if (!didResolve) { didResolve = true resolve(instance) instance.removeListener('data', handleStdout) } } if (typeof opts.onStdout === 'function') { opts.onStdout(message) } if (opts.stdout !== false) { process.stdout.write(message) } } function handleStderr(data) { const message = data.toString() if (typeof opts.onStderr === 'function') { opts.onStderr(message) } if (opts.stderr !== false) { process.stderr.write(message) } } instance.stdout.on('data', handleStdout) instance.stderr.on('data', handleStderr) instance.on('close', () => { instance.stdout.removeListener('data', handleStdout) instance.stderr.removeListener('data', handleStderr) if (!didResolve) { didResolve = true resolve() } }) instance.on('error', (err) => { reject(err) }) }) } function waitFor(millis) { return new Promise((resolve) => setTimeout(resolve, millis)) } await fs.rm('.next', { recursive: true }).catch(() => {}) const file = new File(join(CWD, 'pages/index.jsx')) const results = [] try { if (command === 'dev' || command === 'all') { const instance = await runNextCommandDev(['dev', '--port', '3000']) function waitForCompiled() { return new Promise((resolve) => { function waitForOnData(data) { const message = data.toString() const compiledRegex = /compiled client and server successfully in (\d*[.]?\d+)\s*(m?s) \((\d+) modules\)/gm const matched = compiledRegex.exec(message) if (matched) { resolve({ 'time (ms)': (matched[2] === 's' ? 1000 : 1) * Number(matched[1]), modules: Number(matched[3]), }) instance.stdout.removeListener('data', waitForOnData) } } instance.stdout.on('data', waitForOnData) }) } const [res, initial] = await Promise.all([ fetch('http://localhost:3000/'), waitForCompiled(), ]) if (res.status !== 200) { throw new Error('Fetching / failed') } results.push(initial) file.prepend('// First edit') results.push(await waitForCompiled()) await waitFor(1000) file.prepend('// Second edit') results.push(await waitForCompiled()) await waitFor(1000) file.prepend('// Third edit') results.push(await waitForCompiled()) console.table(results) await killApp(instance) } if (command === 'build' || command === 'all') { // ignore error await fs.rm('.next', { recursive: true, force: true }).catch(() => {}) execSync(`node ${NEXT_BIN} build ./bench/nested-deps`, { cwd: ROOT_DIR, stdio: 'inherit', env: { ...process.env, TRACE_TARGET: 'jaeger', }, }) const traceString = await fs.readFile(join(CWD, '.next', 'trace'), 'utf8') const traces = traceString .split('\n') .filter((line) => line) .map((line) => JSON.parse(line)) const { duration } = traces.pop().find(({ name }) => name === 'next-build') console.info('next build duration: ', prettyMs(duration / 1000)) } } finally { file.restore() }