/** * @fileoverview * * This file contains utilities for `create-next-app` testing. */ import { execSync, spawn, SpawnOptions } from 'child_process' import { existsSync } from 'fs' import { join, resolve } from 'path' import glob from 'glob' import Conf from 'next/dist/compiled/conf' import { getProjectSetting, mapSrcFiles, projectSpecification, } from './specification' import { CustomTemplateOptions, DefaultTemplateOptions, ProjectDeps, ProjectFiles, } from './types' const cli = require.resolve('create-next-app/dist/index.js') /** * Run the built version of `create-next-app` with the given arguments. */ export const createNextApp = ( args: string[], options?: SpawnOptions, testVersion?: string, clearPreferences: boolean = true ) => { const conf = new Conf({ projectName: 'create-next-app' }) if (clearPreferences) { conf.clear() } console.log(`[TEST] $ ${cli} ${args.join(' ')}`, { options }) const cloneEnv = { ...process.env } // unset CI env as this skips the auto-install behavior // being tested delete cloneEnv.CI delete cloneEnv.CIRCLECI delete cloneEnv.GITHUB_ACTIONS delete cloneEnv.CONTINUOUS_INTEGRATION delete cloneEnv.RUN_ID delete cloneEnv.BUILD_NUMBER cloneEnv.NEXT_PRIVATE_TEST_VERSION = testVersion || 'canary' return spawn('node', [cli].concat(args), { ...options, env: { ...cloneEnv, ...options.env, }, }) } export const projectShouldHaveNoGitChanges = ({ cwd, projectName, }: DefaultTemplateOptions) => { const projectDirname = join(cwd, projectName) try { execSync('git diff --quiet', { cwd: projectDirname }) } catch { execSync('git status', { cwd: projectDirname, stdio: 'inherit' }) execSync('git --no-pager diff', { cwd: projectDirname, stdio: 'inherit' }) throw new Error('Found unexpected git changes.') } } export const projectFilesShouldExist = ({ cwd, projectName, files, }: ProjectFiles) => { const projectRoot = resolve(cwd, projectName) for (const file of files) { try { expect(existsSync(resolve(projectRoot, file))).toBe(true) } catch (err) { require('console').error( `missing expected file ${file}`, glob.sync('**/*', { cwd, ignore: '**/node_modules/**' }), files ) throw err } } } export const projectFilesShouldNotExist = ({ cwd, projectName, files, }: ProjectFiles) => { const projectRoot = resolve(cwd, projectName) for (const file of files) { try { expect(existsSync(resolve(projectRoot, file))).toBe(false) } catch (err) { require('console').error( `unexpected file present ${file}`, glob.sync('**/*', { cwd, ignore: '**/node_modules/**' }), files ) throw err } } } export const projectDepsShouldBe = ({ cwd, projectName, type, deps, }: ProjectDeps) => { const projectRoot = resolve(cwd, projectName) const pkgJson = require(resolve(projectRoot, 'package.json')) expect(Object.keys(pkgJson[type] || {}).sort()).toEqual(deps.sort()) } export const shouldBeTemplateProject = ({ cwd, projectName, template, mode, srcDir, }: CustomTemplateOptions) => { projectFilesShouldExist({ cwd, projectName, files: getProjectSetting({ template, mode, setting: 'files', srcDir }), }) // Tailwind templates share the same files (tailwind.config.mjs, postcss.config.mjs) if ( !['app-tw', 'app-tw-empty', 'default-tw', 'default-tw-empty'].includes( template ) ) { projectFilesShouldNotExist({ cwd, projectName, files: mapSrcFiles( projectSpecification[template][mode === 'js' ? 'ts' : 'js'].files, srcDir ), }) } projectDepsShouldBe({ type: 'dependencies', cwd, projectName, deps: getProjectSetting({ template, mode, setting: 'deps' }), }) projectDepsShouldBe({ type: 'devDependencies', cwd, projectName, deps: getProjectSetting({ template, mode, setting: 'devDeps' }), }) } export const shouldBeJavascriptProject = ({ cwd, projectName, template, srcDir, }: Omit) => { shouldBeTemplateProject({ cwd, projectName, template, mode: 'js', srcDir }) } export const shouldBeTypescriptProject = ({ cwd, projectName, template, srcDir, }: Omit) => { shouldBeTemplateProject({ cwd, projectName, template, mode: 'ts', srcDir }) }