first commit
Some checks failed
Test examples / Test Examples (20) (push) Has been cancelled
Test examples / Test Examples (22) (push) Has been cancelled
Lock Threads / action (push) Has been cancelled
Trigger Release / start (push) Has been cancelled
Stale issue handler / stale (push) Has been cancelled
Update Font Data / create-pull-request (push) Has been cancelled
build-and-deploy / deploy-target (push) Has been cancelled
build-and-deploy / build (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-musl - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-unknown-linux-gnu - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-pc-windows-msvc - node@16 (push) Has been cancelled
build-and-deploy / stable - aarch64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / stable - x86_64-apple-darwin - node@16 (push) Has been cancelled
build-and-deploy / build-wasm (nodejs) (push) Has been cancelled
build-and-deploy / build-wasm (web) (push) Has been cancelled
build-and-deploy / Deploy preview tarball (push) Has been cancelled
build-and-deploy / Potentially publish release (push) Has been cancelled
build-and-deploy / publish-turbopack-npm-packages (push) Has been cancelled
build-and-deploy / Deploy examples (push) Has been cancelled
build-and-deploy / thank you, build (push) Has been cancelled
build-and-deploy / Upload Turbopack Bytesize metrics to Datadog (push) Has been cancelled
Rspack Next.js development integration tests / Rspack integration tests (push) Has been cancelled
Rspack Next.js production integration tests / Rspack integration tests (push) Has been cancelled
Turbopack Next.js development integration tests / Next.js integration tests (push) Has been cancelled
Turbopack Next.js production integration tests / Next.js integration tests (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack development test manifest (push) Has been cancelled
Update Rspack test manifest / Update and upload Rspack production test manifest (push) Has been cancelled
Upload bundler test manifests to areweturboyet.com / Upload test results (push) Has been cancelled
Update React / create-pull-request (push) Has been cancelled
test-e2e-project-reset-cron / reset-test-project (push) Has been cancelled
Notify about the top 15 issues/PRs/feature requests (most reacted) in the last 90 days / run (push) Has been cancelled

This commit is contained in:
Arian Tron
2026-03-10 19:37:31 +03:30
commit 61f56f997c
27684 changed files with 2784175 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`create-next-app Biome configuration should match biome.json snapshot 1`] = `
"{
"$schema": "https://biomejs.dev/schemas/2.2.0/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"ignoreUnknown": true,
"includes": ["**", "!node_modules", "!.next", "!dist", "!build"]
},
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
},
"domains": {
"next": "recommended",
"react": "recommended"
}
},
"assist": {
"actions": {
"source": {
"organizeImports": "on"
}
}
}
}
"
`;

View File

@@ -0,0 +1,222 @@
import execa from 'execa'
import { readFile, writeFile } from 'fs/promises'
import { join } from 'path'
import { run, useTempDir } from './utils'
describe('create-next-app Biome configuration', () => {
let nextTgzFilename: string
beforeAll(() => {
if (!process.env.NEXT_TEST_PKG_PATHS) {
throw new Error('This test needs to be run with `node run-tests.js`.')
}
const pkgPaths = new Map<string, string>(
JSON.parse(process.env.NEXT_TEST_PKG_PATHS)
)
nextTgzFilename = pkgPaths.get('next')
})
it('should match biome.json snapshot', async () => {
await useTempDir(async (cwd) => {
const projectName = 'test-biome-snapshot'
const { exitCode } = await run(
[
projectName,
'--ts',
'--biome',
'--no-tailwind',
'--no-src-dir',
'--app',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
'--skip-install',
],
nextTgzFilename,
{ cwd }
)
expect(exitCode).toBe(0)
const projectDir = join(cwd, projectName)
const biomeConfig = await readFile(join(projectDir, 'biome.json'), 'utf8')
expect(biomeConfig).toMatchSnapshot()
})
})
it('should run biome check successfully on generated TypeScript project', async () => {
await useTempDir(async (cwd) => {
const projectName = 'test-biome-ts-check'
const { exitCode } = await run(
[
projectName,
'--ts',
'--biome',
'--no-tailwind',
'--no-src-dir',
'--app',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
],
nextTgzFilename,
{ cwd }
)
expect(exitCode).toBe(0)
const projectDir = join(cwd, projectName)
// Run biome check on the generated project
const { exitCode: biomeExitCode, stdout } = await execa(
'npm',
['run', 'lint'],
{
cwd: projectDir,
}
)
expect(biomeExitCode).toBe(0)
expect(stdout).toContain('Checked')
})
})
it('should run biome check successfully on generated JavaScript project', async () => {
await useTempDir(async (cwd) => {
const projectName = 'test-biome-js-check'
const { exitCode } = await run(
[
projectName,
'--js',
'--biome',
'--no-tailwind',
'--no-src-dir',
'--app',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
],
nextTgzFilename,
{ cwd }
)
expect(exitCode).toBe(0)
const projectDir = join(cwd, projectName)
// Run biome check on the generated project
const { exitCode: biomeExitCode, stdout } = await execa(
'npm',
['run', 'lint'],
{
cwd: projectDir,
}
)
expect(biomeExitCode).toBe(0)
expect(stdout).toContain('Checked')
})
})
it('should format code with biome successfully', async () => {
await useTempDir(async (cwd) => {
const projectName = 'test-biome-format'
const { exitCode } = await run(
[
projectName,
'--ts',
'--biome',
'--no-tailwind',
'--no-src-dir',
'--app',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
],
nextTgzFilename,
{ cwd }
)
expect(exitCode).toBe(0)
const projectDir = join(cwd, projectName)
// Run biome format on the generated project
const { exitCode: biomeFormatCode, stdout } = await execa(
'npm',
['run', 'format'],
{
cwd: projectDir,
}
)
expect(biomeFormatCode).toBe(0)
expect(stdout).toContain('Formatted')
})
})
it('should show errors when biome detects issues', async () => {
await useTempDir(async (cwd) => {
const projectName = 'test-biome-errors'
const { exitCode } = await run(
[
projectName,
'--ts',
'--biome',
'--no-tailwind',
'--no-src-dir',
'--app',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
],
nextTgzFilename,
{ cwd }
)
expect(exitCode).toBe(0)
const projectDir = join(cwd, projectName)
// Add a file with linting issues
const problematicFile = join(projectDir, 'app', 'problematic.tsx')
await writeFile(
problematicFile,
`export default function Component() {
var unusedVar = 5;
const a = 1
const b = 2
// Double equals instead of triple
if (a == b) {
console.log("test")
}
// Debugger statement
debugger;
return <div>Test</div>
}`
)
// Run biome check on the project with the problematic file
try {
await execa('npm', ['run', 'lint'], {
cwd: projectDir,
})
// If we get here, the command succeeded when it shouldn't have
expect(true).toBe(false) // Force test to fail
} catch (error) {
// The command should fail with exit code 1
expect(error.exitCode).toBe(1)
expect(error.stdout + error.stderr).toContain('problematic.tsx')
// Check for specific error messages
const output = error.stdout + error.stderr
expect(output).toMatch(/debugger|no-debugger/)
}
})
})
})

View File

@@ -0,0 +1,113 @@
import { existsSync } from 'fs'
import { readFile } from 'fs/promises'
import { join } from 'path'
import { run, useTempDir } from './utils'
describe('create-next-app ESLint configuration', () => {
let nextTgzFilename: string
beforeAll(() => {
if (!process.env.NEXT_TEST_PKG_PATHS) {
throw new Error('This test needs to be run with `node run-tests.js`.')
}
const pkgPaths = new Map<string, string>(
JSON.parse(process.env.NEXT_TEST_PKG_PATHS)
)
nextTgzFilename = pkgPaths.get('next')
})
it('should generate eslint.config.mjs for TypeScript project with ESLint', async () => {
await useTempDir(async (cwd) => {
const projectName = 'test-eslint-ts'
const { exitCode } = await run(
[
projectName,
'--ts',
'--eslint',
'--no-tailwind',
'--no-src-dir',
'--no-react-compiler',
'--no-agents-md',
'--app',
'--no-import-alias',
'--skip-install',
],
nextTgzFilename,
{ cwd }
)
expect(exitCode).toBe(0)
const projectDir = join(cwd, projectName)
// Should have eslint.config.mjs
expect(existsSync(join(projectDir, 'eslint.config.mjs'))).toBe(true)
// Should NOT have biome.json
expect(existsSync(join(projectDir, 'biome.json'))).toBe(false)
// Check eslint.config.mjs content
const eslintConfig = await readFile(
join(projectDir, 'eslint.config.mjs'),
'utf8'
)
expect(eslintConfig).toContain('next/core-web-vitals')
expect(eslintConfig).toContain('next/typescript')
// Check package.json scripts
const packageJson = JSON.parse(
await readFile(join(projectDir, 'package.json'), 'utf8')
)
expect(packageJson.scripts.lint).toBe('eslint')
expect(packageJson.devDependencies.eslint).toBeTruthy()
expect(packageJson.devDependencies['eslint-config-next']).toBeTruthy()
})
})
it('should generate eslint.config.mjs for JavaScript project with ESLint', async () => {
await useTempDir(async (cwd) => {
const projectName = 'test-eslint-js'
const { exitCode } = await run(
[
projectName,
'--js',
'--eslint',
'--no-tailwind',
'--no-src-dir',
'--no-react-compiler',
'--no-agents-md',
'--app',
'--no-import-alias',
'--skip-install',
],
nextTgzFilename,
{ cwd }
)
expect(exitCode).toBe(0)
const projectDir = join(cwd, projectName)
// Should have eslint.config.mjs
expect(existsSync(join(projectDir, 'eslint.config.mjs'))).toBe(true)
// Check eslint.config.mjs content for JS project
const eslintConfig = await readFile(
join(projectDir, 'eslint.config.mjs'),
'utf8'
)
expect(eslintConfig).toContain('next/core-web-vitals')
expect(eslintConfig).not.toContain('next/typescript')
// Check package.json scripts
const packageJson = JSON.parse(
await readFile(join(projectDir, 'package.json'), 'utf8')
)
expect(packageJson.scripts.lint).toBe('eslint')
expect(packageJson.devDependencies.eslint).toBeTruthy()
expect(packageJson.devDependencies['eslint-config-next']).toBeTruthy()
})
})
})

View File

@@ -0,0 +1,309 @@
import {
EXAMPLE_PATH,
EXAMPLE_REPO,
FULL_EXAMPLE_PATH,
projectFilesShouldExist,
projectFilesShouldNotExist,
shouldBeTemplateProject,
run,
useTempDir,
} from './utils'
describe('create-next-app --example', () => {
let nextTgzFilename: string
beforeAll(() => {
if (!process.env.NEXT_TEST_PKG_PATHS) {
throw new Error('This test needs to be run with `node run-tests.js`.')
}
const pkgPaths = new Map<string, string>(
JSON.parse(process.env.NEXT_TEST_PKG_PATHS)
)
nextTgzFilename = pkgPaths.get('next')
})
it('should create on valid Next.js example name', async () => {
await useTempDir(async (cwd) => {
const projectName = 'valid-example'
const res = await run(
[
projectName,
'--example',
'basic-css',
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
],
nextTgzFilename,
{
cwd,
}
)
expect(res.exitCode).toBe(0)
projectFilesShouldExist({
cwd,
projectName,
files: [
'.gitignore',
'package.json',
'app/page.tsx',
'app/layout.tsx',
'node_modules/next',
],
})
})
})
it('should create with GitHub URL', async () => {
await useTempDir(async (cwd) => {
const projectName = 'github-url'
const res = await run(
[
projectName,
'--example',
FULL_EXAMPLE_PATH,
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
],
nextTgzFilename,
{
cwd,
}
)
expect(res.exitCode).toBe(0)
projectFilesShouldExist({
cwd,
projectName,
files: [
'.gitignore',
'package.json',
'app/page.tsx',
'app/layout.tsx',
'node_modules/next',
],
})
})
})
it('should create with GitHub URL trailing slash', async () => {
await useTempDir(async (cwd) => {
const projectName = 'github-url-trailing-slash'
const res = await run(
[
projectName,
'--example',
// since vercel/examples is not a template repo, we use the following
// GH#39665
'https://github.com/vercel/nextjs-portfolio-starter/',
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
],
nextTgzFilename,
{
cwd,
}
)
expect(res.exitCode).toBe(0)
projectFilesShouldExist({
cwd,
projectName,
files: [
'.gitignore',
'package.json',
'pages/index.mdx',
'node_modules/next',
],
})
})
})
it('should create with GitHub URL and --example-path', async () => {
await useTempDir(async (cwd) => {
const projectName = 'github-url-and-example-path'
const res = await run(
[
projectName,
'--js',
'--no-tailwind',
'--eslint',
'--example',
EXAMPLE_REPO,
'--example-path',
EXAMPLE_PATH,
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
],
nextTgzFilename,
{
cwd,
}
)
expect(res.exitCode).toBe(0)
projectFilesShouldExist({
cwd,
projectName,
files: [
'.gitignore',
'package.json',
'app/page.tsx',
'app/layout.tsx',
'node_modules/next',
],
})
})
})
it('should use --example-path over the GitHub URL', async () => {
await useTempDir(async (cwd) => {
const projectName = 'example-path-over-github-url'
const res = await run(
[
projectName,
'--js',
'--no-tailwind',
'--eslint',
'--example',
FULL_EXAMPLE_PATH,
'--example-path',
EXAMPLE_PATH,
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
],
nextTgzFilename,
{
cwd,
}
)
expect(res.exitCode).toBe(0)
projectFilesShouldExist({
cwd,
projectName,
files: [
'.gitignore',
'package.json',
'app/page.tsx',
'app/layout.tsx',
'node_modules/next',
],
})
})
})
// TODO: investigate why this test stalls on yarn install when
// stdin is piped instead of inherited on windows
if (process.platform !== 'win32') {
it('should fall back to default template if failed to download', async () => {
await useTempDir(async (cwd) => {
const projectName = 'fallback-to-default'
const res = await run(
[
projectName,
'--js',
'--no-tailwind',
'--eslint',
'--app',
'--example',
'__internal-testing-retry',
'--import-alias=@/*',
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
],
nextTgzFilename,
{
cwd,
input: '\n', // 'Yes' to retry
stdio: 'pipe',
}
)
expect(res.exitCode).toBe(0)
shouldBeTemplateProject({
cwd,
projectName,
template: 'app',
mode: 'js',
})
})
})
}
it('should create if --example value is default', async () => {
await useTempDir(async (cwd) => {
const projectName = 'example-default'
const res = await run(
[
projectName,
'--js',
'--no-tailwind',
'--eslint',
'--example',
'default',
'--import-alias=@/*',
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
],
nextTgzFilename,
{
cwd,
}
)
expect(res.exitCode).toBe(0)
shouldBeTemplateProject({
cwd,
projectName,
template: 'default',
mode: 'js',
})
})
})
it('should not create if --example flag value is invalid', async () => {
await useTempDir(async (cwd) => {
const projectName = 'invalid-example'
const res = await run(
[
projectName,
'--example',
'not a real example',
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
],
nextTgzFilename,
{
cwd,
reject: false,
}
)
expect(res.exitCode).toBe(1)
projectFilesShouldNotExist({
cwd,
projectName,
files: ['package.json'],
})
})
})
it('should not create if --example flag value is absent', async () => {
await useTempDir(async (cwd) => {
const projectName = 'no-example'
const res = await run(
[
projectName,
'--ts',
'--app',
'--eslint',
'--no-src-dir',
'--no-tailwind',
'--example',
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
],
nextTgzFilename,
{
cwd,
reject: false,
}
)
expect(res.exitCode).toBe(1)
})
})
})

View File

@@ -0,0 +1,199 @@
import { mkdir, writeFile } from 'fs/promises'
import { join } from 'path'
import {
run,
useTempDir,
projectFilesShouldExist,
projectFilesShouldNotExist,
} from './utils'
describe('create-next-app', () => {
let nextTgzFilename: string
beforeAll(() => {
if (!process.env.NEXT_TEST_PKG_PATHS) {
throw new Error('This test needs to be run with `node run-tests.js`.')
}
const pkgPaths = new Map<string, string>(
JSON.parse(process.env.NEXT_TEST_PKG_PATHS)
)
nextTgzFilename = pkgPaths.get('next')
})
it('should not create if the target directory is not empty', async () => {
await useTempDir(async (cwd) => {
const projectName = 'non-empty-dir'
await mkdir(join(cwd, projectName))
const pkg = join(cwd, projectName, 'package.json')
await writeFile(pkg, `{ "name": "${projectName}" }`)
const res = await run(
[
projectName,
'--ts',
'--app',
'--no-linter',
'--no-tailwind',
'--no-src-dir',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
],
nextTgzFilename,
{
cwd,
reject: false,
}
)
expect(res.exitCode).toBe(1)
expect(res.stdout).toMatch(/contains files that could conflict/)
})
})
it('should not create if the target directory is not writable', async () => {
const expectedErrorMessage =
/you do not have write permissions for this folder|EPERM: operation not permitted/
await useTempDir(async (cwd) => {
const projectName = 'dir-not-writable'
// if the folder isn't able to be write restricted we can't test so skip
if (
await writeFile(join(cwd, 'test'), 'hello')
.then(() => true)
.catch(() => false)
) {
console.warn(
`Test folder is not write restricted skipping write permission test`
)
return
}
const res = await run(
[
projectName,
'--ts',
'--app',
'--eslint',
'--no-tailwind',
'--no-src-dir',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
],
nextTgzFilename,
{
cwd,
reject: false,
}
)
expect(res.stderr).toMatch(expectedErrorMessage)
expect(res.exitCode).toBe(1)
}, 0o500).catch((err) => {
if (!expectedErrorMessage.test(err.message)) {
throw err
}
})
})
it('should create AGENTS.md and CLAUDE.md with --agents-md flag', async () => {
await useTempDir(async (cwd) => {
const projectName = 'with-agents-md'
const res = await run(
[
projectName,
'--ts',
'--app',
'--no-linter',
'--no-tailwind',
'--no-src-dir',
'--no-import-alias',
'--no-react-compiler',
'--agents-md',
'--skip-install',
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
],
nextTgzFilename,
{
cwd,
}
)
expect(res.exitCode).toBe(0)
projectFilesShouldExist({
cwd,
projectName,
files: ['AGENTS.md', 'CLAUDE.md'],
})
})
})
it('should not create AGENTS.md and CLAUDE.md with --no-agents-md flag', async () => {
await useTempDir(async (cwd) => {
const projectName = 'without-agents-md'
const res = await run(
[
projectName,
'--ts',
'--app',
'--no-linter',
'--no-tailwind',
'--no-src-dir',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
'--skip-install',
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
],
nextTgzFilename,
{
cwd,
}
)
expect(res.exitCode).toBe(0)
projectFilesShouldNotExist({
cwd,
projectName,
files: ['AGENTS.md', 'CLAUDE.md'],
})
})
})
it('should not install dependencies if --skip-install', async () => {
await useTempDir(async (cwd) => {
const projectName = 'empty-dir'
const res = await run(
[
projectName,
'--ts',
'--app',
'--no-linter',
'--no-tailwind',
'--no-src-dir',
'--no-import-alias',
'--skip-install',
'--no-react-compiler',
'--no-agents-md',
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
],
nextTgzFilename,
{
cwd,
}
)
expect(res.exitCode).toBe(0)
projectFilesShouldExist({
cwd,
projectName,
files: ['.gitignore', 'package.json'],
})
projectFilesShouldNotExist({ cwd, projectName, files: ['node_modules'] })
})
})
})

View File

@@ -0,0 +1,300 @@
import path from 'path'
import {
SRC_DIR_NAMES,
TemplateMode,
TemplateType,
} from '../../../../packages/create-next-app/templates'
export type ProjectSettings = {
files: string[]
deps: string[]
devDeps: string[]
}
export type ProjectSpecification = {
global: ProjectSettings
} & {
[key in TemplateType]: {
[key in TemplateMode]: ProjectSettings
}
}
/**
* Required files for a given project template and mode.
*/
export const projectSpecification: ProjectSpecification = {
global: {
files: [
'package.json',
'eslint.config.mjs',
'node_modules/next',
'.gitignore',
],
deps: [
'next',
'react',
'react-dom',
...(process.env.NEXT_RSPACK ? ['next-rspack'] : []),
],
devDeps: ['eslint', 'eslint-config-next'],
},
default: {
js: {
files: [
'pages/index.js',
'pages/_app.js',
'pages/api/hello.js',
'jsconfig.json',
],
deps: [],
devDeps: [],
},
ts: {
files: [
'pages/index.tsx',
'pages/_app.tsx',
'pages/api/hello.ts',
'tsconfig.json',
'next-env.d.ts',
],
deps: [],
devDeps: [
'@types/node',
'@types/react',
'@types/react-dom',
'typescript',
],
},
},
'default-empty': {
js: {
files: ['pages/index.js', 'pages/_app.js', 'jsconfig.json'],
deps: [],
devDeps: [],
},
ts: {
files: [
'pages/index.tsx',
'pages/_app.tsx',
'tsconfig.json',
'next-env.d.ts',
],
deps: [],
devDeps: [
'@types/node',
'@types/react',
'@types/react-dom',
'typescript',
],
},
},
'default-tw': {
js: {
files: [
'jsconfig.json',
'pages/_app.js',
'pages/api/hello.js',
'pages/index.js',
'postcss.config.mjs',
],
deps: [],
devDeps: ['@tailwindcss/postcss', 'tailwindcss'],
},
ts: {
files: [
'next-env.d.ts',
'pages/_app.tsx',
'pages/api/hello.ts',
'pages/index.tsx',
'postcss.config.mjs',
'tsconfig.json',
],
deps: [],
devDeps: [
'@types/node',
'@types/react-dom',
'@types/react',
'@tailwindcss/postcss',
'tailwindcss',
'typescript',
],
},
},
'default-tw-empty': {
js: {
files: [
'jsconfig.json',
'pages/_app.js',
'pages/index.js',
'postcss.config.mjs',
],
deps: [],
devDeps: ['@tailwindcss/postcss', 'tailwindcss'],
},
ts: {
files: [
'next-env.d.ts',
'pages/_app.tsx',
'pages/index.tsx',
'postcss.config.mjs',
'tsconfig.json',
],
deps: [],
devDeps: [
'@types/node',
'@types/react-dom',
'@types/react',
'@tailwindcss/postcss',
'tailwindcss',
'typescript',
],
},
},
app: {
js: {
deps: [],
devDeps: [],
files: ['app/page.js', 'app/layout.js', 'jsconfig.json'],
},
ts: {
deps: [],
devDeps: [
'@types/node',
'@types/react',
'@types/react-dom',
'typescript',
],
files: [
'app/page.tsx',
'app/layout.tsx',
'tsconfig.json',
'next-env.d.ts',
],
},
},
'app-api': {
js: {
deps: ['next', ...(process.env.NEXT_RSPACK ? ['next-rspack'] : [])],
devDeps: [],
files: ['app/route.js', 'app/[slug]/route.js', 'jsconfig.json'],
},
ts: {
deps: ['next', ...(process.env.NEXT_RSPACK ? ['next-rspack'] : [])],
devDeps: ['@types/node', '@types/react', 'typescript'],
files: [
'app/route.ts',
'app/[slug]/route.ts',
'tsconfig.json',
'next-env.d.ts',
],
},
},
'app-empty': {
js: {
deps: [],
devDeps: [],
files: ['app/page.js', 'app/layout.js', 'jsconfig.json'],
},
ts: {
deps: [],
devDeps: [
'@types/node',
'@types/react',
'@types/react-dom',
'typescript',
],
files: [
'app/page.tsx',
'app/layout.tsx',
'tsconfig.json',
'next-env.d.ts',
],
},
},
'app-tw': {
js: {
deps: [],
devDeps: ['@tailwindcss/postcss', 'tailwindcss'],
files: [
'app/layout.js',
'app/page.js',
'jsconfig.json',
'postcss.config.mjs',
],
},
ts: {
deps: [],
devDeps: [
'@types/node',
'@types/react-dom',
'@types/react',
'@tailwindcss/postcss',
'tailwindcss',
'typescript',
],
files: [
'app/layout.tsx',
'app/page.tsx',
'next-env.d.ts',
'postcss.config.mjs',
'tsconfig.json',
],
},
},
'app-tw-empty': {
js: {
deps: [],
devDeps: ['@tailwindcss/postcss', 'tailwindcss'],
files: [
'app/layout.js',
'app/page.js',
'jsconfig.json',
'postcss.config.mjs',
],
},
ts: {
deps: [],
devDeps: [
'@types/node',
'@types/react-dom',
'@types/react',
'@tailwindcss/postcss',
'tailwindcss',
'typescript',
],
files: [
'app/layout.tsx',
'app/page.tsx',
'next-env.d.ts',
'postcss.config.mjs',
'tsconfig.json',
],
},
},
}
export type GetProjectSettingsArgs = {
template: TemplateType
mode: TemplateMode
setting: keyof ProjectSettings
srcDir?: boolean
}
export const mapSrcFiles = (files: string[], srcDir?: boolean) =>
files.map((file) =>
srcDir && SRC_DIR_NAMES.some((name) => file.startsWith(name))
? path.join('src', file)
: file
)
export const getProjectSetting = ({
template,
mode,
setting,
srcDir,
}: GetProjectSettingsArgs) => {
return [
...projectSpecification.global[setting],
...mapSrcFiles(projectSpecification[template][mode][setting], srcDir),
]
}

View File

@@ -0,0 +1,24 @@
import {
TemplateMode,
TemplateType,
} from '../../../../packages/create-next-app/templates'
export interface DefaultTemplateOptions {
cwd: string
projectName: string
}
export interface CustomTemplateOptions extends DefaultTemplateOptions {
mode: TemplateMode
template: TemplateType
srcDir?: boolean
}
export interface ProjectFiles extends DefaultTemplateOptions {
files: string[]
}
export interface ProjectDeps extends DefaultTemplateOptions {
type: 'dependencies' | 'devDependencies'
deps: string[]
}

View File

@@ -0,0 +1,191 @@
/**
* @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<CustomTemplateOptions, 'mode'>) => {
shouldBeTemplateProject({ cwd, projectName, template, mode: 'js', srcDir })
}
export const shouldBeTypescriptProject = ({
cwd,
projectName,
template,
srcDir,
}: Omit<CustomTemplateOptions, 'mode'>) => {
shouldBeTemplateProject({ cwd, projectName, template, mode: 'ts', srcDir })
}

View File

@@ -0,0 +1,138 @@
import execa from 'execa'
import * as semver from 'semver'
import {
command,
DEFAULT_FILES,
FULL_EXAMPLE_PATH,
projectFilesShouldExist,
run,
useTempDir,
} from '../utils'
describe('create-next-app with package manager bun', () => {
let nextTgzFilename: string
let files: string[]
beforeAll(async () => {
if (!process.env.NEXT_TEST_PKG_PATHS) {
throw new Error('This test needs to be run with `node run-tests.js`.')
}
const pkgPaths = new Map<string, string>(
JSON.parse(process.env.NEXT_TEST_PKG_PATHS)
)
nextTgzFilename = pkgPaths.get('next')
await command('bun', ['--version'])
// install bun if not available
.catch(() => command('npm', ['i', '-g', 'bun']))
const bunVersion = (await execa('bun', ['--version'])).stdout.trim()
// Some CI runners pre-install Bun.
// Locally, we don't pin Bun either.
const lockFile = semver.gte(bunVersion, '1.2.0') ? 'bun.lock' : 'bun.lockb'
files = [...DEFAULT_FILES, lockFile]
})
it('should use bun for --use-bun flag', async () => {
await useTempDir(async (cwd) => {
const projectName = 'use-bun'
const res = await run(
[
projectName,
'--ts',
'--app',
'--use-bun',
'--no-linter',
'--no-src-dir',
'--no-tailwind',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
],
nextTgzFilename,
{
cwd,
}
)
expect(res.exitCode).toBe(0)
projectFilesShouldExist({
cwd,
projectName,
files,
})
})
})
it('should use bun when user-agent is bun', async () => {
await useTempDir(async (cwd) => {
const projectName = 'user-agent-bun'
const res = await run(
[
projectName,
'--ts',
'--app',
'--no-linter',
'--no-src-dir',
'--no-tailwind',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
],
nextTgzFilename,
{
cwd,
env: { npm_config_user_agent: 'bun' },
}
)
expect(res.exitCode).toBe(0)
projectFilesShouldExist({
cwd,
projectName,
files,
})
})
})
it('should use bun for --use-bun flag with example', async () => {
await useTempDir(async (cwd) => {
const projectName = 'use-bun-with-example'
const res = await run(
[projectName, '--use-bun', '--example', FULL_EXAMPLE_PATH],
nextTgzFilename,
{ cwd }
)
expect(res.exitCode).toBe(0)
projectFilesShouldExist({
cwd,
projectName,
files,
})
})
})
it('should use bun when user-agent is bun with example', async () => {
await useTempDir(async (cwd) => {
const projectName = 'user-agent-bun-with-example'
const res = await run(
[projectName, '--example', FULL_EXAMPLE_PATH],
nextTgzFilename,
{
cwd,
env: { npm_config_user_agent: 'bun' },
}
)
expect(res.exitCode).toBe(0)
projectFilesShouldExist({
cwd,
projectName,
files,
})
})
})
})

View File

@@ -0,0 +1,127 @@
import {
DEFAULT_FILES,
FULL_EXAMPLE_PATH,
projectFilesShouldExist,
run,
useTempDir,
} from '../utils'
const lockFile = 'package-lock.json'
const files = [...DEFAULT_FILES, lockFile]
describe('create-next-app with package manager npm', () => {
let nextTgzFilename: string
beforeAll(() => {
if (!process.env.NEXT_TEST_PKG_PATHS) {
throw new Error('This test needs to be run with `node run-tests.js`.')
}
const pkgPaths = new Map<string, string>(
JSON.parse(process.env.NEXT_TEST_PKG_PATHS)
)
nextTgzFilename = pkgPaths.get('next')
})
it('should use npm for --use-npm flag', async () => {
await useTempDir(async (cwd) => {
const projectName = 'use-npm'
const res = await run(
[
projectName,
'--ts',
'--app',
'--use-npm',
'--no-linter',
'--no-src-dir',
'--no-tailwind',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
],
nextTgzFilename,
{
cwd,
}
)
expect(res.exitCode).toBe(0)
projectFilesShouldExist({
cwd,
projectName,
files,
})
})
})
it('should use npm when user-agent is npm', async () => {
await useTempDir(async (cwd) => {
const projectName = 'user-agent-npm'
const res = await run(
[
projectName,
'--ts',
'--app',
'--no-linter',
'--no-src-dir',
'--no-tailwind',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
],
nextTgzFilename,
{
cwd,
env: { npm_config_user_agent: 'npm' },
}
)
expect(res.exitCode).toBe(0)
projectFilesShouldExist({
cwd,
projectName,
files,
})
})
})
it('should use npm for --use-npm flag with example', async () => {
await useTempDir(async (cwd) => {
const projectName = 'use-npm-with-example'
const res = await run(
[projectName, '--use-npm', '--example', FULL_EXAMPLE_PATH],
nextTgzFilename,
{ cwd }
)
expect(res.exitCode).toBe(0)
projectFilesShouldExist({
cwd,
projectName,
files,
})
})
})
it('should use npm when user-agent is npm with example', async () => {
await useTempDir(async (cwd) => {
const projectName = 'user-agent-npm-with-example'
const res = await run(
[projectName, '--example', FULL_EXAMPLE_PATH],
nextTgzFilename,
{
cwd,
env: { npm_config_user_agent: 'npm' },
}
)
expect(res.exitCode).toBe(0)
projectFilesShouldExist({
cwd,
projectName,
files,
})
})
})
})

View File

@@ -0,0 +1,196 @@
import {
DEFAULT_FILES,
FULL_EXAMPLE_PATH,
projectFilesShouldExist,
projectFilesShouldNotExist,
run,
useTempDir,
} from '../utils'
const lockFile = 'pnpm-lock.yaml'
const files = [...DEFAULT_FILES, lockFile]
describe('create-next-app with package manager pnpm', () => {
let nextTgzFilename: string
beforeAll(async () => {
if (!process.env.NEXT_TEST_PKG_PATHS) {
throw new Error('This test needs to be run with `node run-tests.js`.')
}
const pkgPaths = new Map<string, string>(
JSON.parse(process.env.NEXT_TEST_PKG_PATHS)
)
nextTgzFilename = pkgPaths.get('next')
})
it('should use pnpm for --use-pnpm flag', async () => {
await useTempDir(async (cwd) => {
const projectName = 'use-pnpm'
const res = await run(
[
projectName,
'--ts',
'--app',
'--use-pnpm',
'--no-linter',
'--no-src-dir',
'--no-tailwind',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
],
nextTgzFilename,
{
cwd,
}
)
expect(res.exitCode).toBe(0)
projectFilesShouldExist({
cwd,
projectName,
files,
})
})
})
it('should use pnpm when user-agent is pnpm', async () => {
await useTempDir(async (cwd) => {
const projectName = 'user-agent-pnpm'
const res = await run(
[
projectName,
'--ts',
'--app',
'--no-linter',
'--no-src-dir',
'--no-tailwind',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
],
nextTgzFilename,
{
cwd,
env: { npm_config_user_agent: 'pnpm' },
}
)
expect(res.exitCode).toBe(0)
projectFilesShouldExist({
cwd,
projectName,
files,
})
})
})
// These tests use --skip-install because:
// 1. We only need to verify the workspace file is created/not created
// 2. The CI runs pnpm v9, but when testing v10 behavior, the workspace file
// created for v10 (without packages field) would fail with pnpm v9
it('should create pnpm-workspace.yaml for pnpm v10+', async () => {
await useTempDir(async (cwd) => {
const projectName = 'pnpm-v10-workspace'
const res = await run(
[
projectName,
'--ts',
'--app',
'--no-linter',
'--no-src-dir',
'--no-tailwind',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
'--skip-install',
],
nextTgzFilename,
{
cwd,
env: { npm_config_user_agent: 'pnpm/10.0.0 npm/? node/v20.0.0' },
}
)
expect(res.exitCode).toBe(0)
projectFilesShouldExist({
cwd,
projectName,
files: ['package.json', 'pnpm-workspace.yaml'],
})
})
})
it('should NOT create pnpm-workspace.yaml for pnpm v9', async () => {
await useTempDir(async (cwd) => {
const projectName = 'pnpm-v9-no-workspace'
const res = await run(
[
projectName,
'--ts',
'--app',
'--no-linter',
'--no-src-dir',
'--no-tailwind',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
'--skip-install',
],
nextTgzFilename,
{
cwd,
env: { npm_config_user_agent: 'pnpm/9.13.2 npm/? node/v20.0.0' },
}
)
expect(res.exitCode).toBe(0)
projectFilesShouldNotExist({
cwd,
projectName,
files: ['pnpm-workspace.yaml'],
})
})
})
it('should use pnpm for --use-pnpm flag with example', async () => {
await useTempDir(async (cwd) => {
const projectName = 'use-pnpm-with-example'
const res = await run(
[projectName, '--use-pnpm', '--example', FULL_EXAMPLE_PATH],
nextTgzFilename,
{ cwd }
)
expect(res.exitCode).toBe(0)
projectFilesShouldExist({
cwd,
projectName,
files,
})
})
})
it('should use pnpm when user-agent is pnpm with example', async () => {
await useTempDir(async (cwd) => {
const projectName = 'user-agent-pnpm-with-example'
const res = await run(
[projectName, '--example', FULL_EXAMPLE_PATH],
nextTgzFilename,
{
cwd,
env: { npm_config_user_agent: 'pnpm' },
}
)
expect(res.exitCode).toBe(0)
projectFilesShouldExist({
cwd,
projectName,
files,
})
})
})
})

View File

@@ -0,0 +1,135 @@
import {
command,
DEFAULT_FILES,
FULL_EXAMPLE_PATH,
projectFilesShouldExist,
run,
useTempDir,
} from '../utils'
const lockFile = 'yarn.lock'
const files = [...DEFAULT_FILES, lockFile]
describe('create-next-app with package manager yarn', () => {
let nextTgzFilename: string
beforeAll(async () => {
if (!process.env.NEXT_TEST_PKG_PATHS) {
throw new Error('This test needs to be run with `node run-tests.js`.')
}
const pkgPaths = new Map<string, string>(
JSON.parse(process.env.NEXT_TEST_PKG_PATHS)
)
nextTgzFilename = pkgPaths.get('next')
await command('yarn', ['--version'])
// install yarn if not available
.catch(() =>
command('corepack', ['prepare', '--activate', 'yarn@1.22.19'])
)
.catch(() => command('npm', ['i', '-g', 'yarn']))
})
it('should use yarn for --use-yarn flag', async () => {
await useTempDir(async (cwd) => {
const projectName = 'use-yarn'
const res = await run(
[
projectName,
'--ts',
'--app',
'--use-yarn',
'--no-linter',
'--no-src-dir',
'--no-tailwind',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
],
nextTgzFilename,
{
cwd,
}
)
expect(res.exitCode).toBe(0)
projectFilesShouldExist({
cwd,
projectName,
files,
})
})
})
it('should use yarn when user-agent is yarn', async () => {
await useTempDir(async (cwd) => {
const projectName = 'user-agent-yarn'
const res = await run(
[
projectName,
'--ts',
'--app',
'--no-linter',
'--no-src-dir',
'--no-tailwind',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
],
nextTgzFilename,
{
cwd,
env: { npm_config_user_agent: 'yarn' },
}
)
expect(res.exitCode).toBe(0)
projectFilesShouldExist({
cwd,
projectName,
files,
})
})
})
it('should use yarn for --use-yarn flag with example', async () => {
await useTempDir(async (cwd) => {
const projectName = 'use-yarn-with-example'
const res = await run(
[projectName, '--use-yarn', '--example', FULL_EXAMPLE_PATH],
nextTgzFilename,
{ cwd }
)
expect(res.exitCode).toBe(0)
projectFilesShouldExist({
cwd,
projectName,
files,
})
})
})
it('should use yarn when user-agent is yarn with example', async () => {
await useTempDir(async (cwd) => {
const projectName = 'user-agent-yarn-with-example'
const res = await run(
[projectName, '--example', FULL_EXAMPLE_PATH],
nextTgzFilename,
{
cwd,
env: { npm_config_user_agent: 'yarn' },
}
)
expect(res.exitCode).toBe(0)
projectFilesShouldExist({
cwd,
projectName,
files,
})
})
})
})

View File

@@ -0,0 +1,357 @@
import { check } from 'next-test-utils'
import { join } from 'path'
import { createNextApp, projectFilesShouldExist, useTempDir } from './utils'
describe('create-next-app prompts', () => {
let nextTgzFilename: string
beforeAll(() => {
if (!process.env.NEXT_TEST_PKG_PATHS) {
throw new Error('This test needs to be run with `node run-tests.js`.')
}
const pkgPaths = new Map<string, string>(
JSON.parse(process.env.NEXT_TEST_PKG_PATHS)
)
nextTgzFilename = pkgPaths.get('next')
})
it('should prompt user for choice if directory name is absent', async () => {
await useTempDir(async (cwd) => {
const projectName = 'no-dir-name'
const childProcess = createNextApp(
[
'--ts',
'--app',
'--eslint',
'--no-src-dir',
'--no-tailwind',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
],
{
cwd,
},
nextTgzFilename
)
await new Promise<void>((resolve) => {
childProcess.on('exit', async (exitCode) => {
expect(exitCode).toBe(0)
projectFilesShouldExist({
cwd,
projectName,
files: ['package.json'],
})
resolve()
})
// enter project name
childProcess.stdin.write(`${projectName}\n`)
})
const pkg = require(join(cwd, projectName, 'package.json'))
expect(pkg.name).toBe(projectName)
})
})
it('should prompt user for choice if --js or --ts flag is absent', async () => {
await useTempDir(async (cwd) => {
const projectName = 'ts-js'
const childProcess = createNextApp(
[
projectName,
'--app',
'--eslint',
'--no-tailwind',
'--no-src-dir',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
],
{
cwd,
},
nextTgzFilename
)
await new Promise<void>((resolve) => {
childProcess.on('exit', async (exitCode) => {
expect(exitCode).toBe(0)
projectFilesShouldExist({
cwd,
projectName,
files: ['tsconfig.json'],
})
resolve()
})
// select default choice: typescript
childProcess.stdin.write('\n')
})
})
})
it('should prompt user for choice if --tailwind is absent', async () => {
await useTempDir(async (cwd) => {
const projectName = 'tw'
const childProcess = createNextApp(
[
projectName,
'--ts',
'--app',
'--eslint',
'--no-src-dir',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
],
{
cwd,
},
nextTgzFilename
)
await new Promise<void>((resolve) => {
childProcess.on('exit', async (exitCode) => {
expect(exitCode).toBe(0)
projectFilesShouldExist({
cwd,
projectName,
files: ['postcss.config.mjs'],
})
resolve()
})
// select default choice: tailwind
childProcess.stdin.write('\n')
})
})
})
it('should prompt user for choice if --import-alias is absent', async () => {
await useTempDir(async (cwd) => {
const projectName = 'import-alias'
const childProcess = createNextApp(
[
projectName,
'--ts',
'--app',
'--eslint',
'--no-tailwind',
'--no-src-dir',
'--no-react-compiler',
'--no-agents-md',
],
{
cwd,
},
nextTgzFilename
)
await new Promise<void>(async (resolve) => {
childProcess.on('exit', async (exitCode) => {
expect(exitCode).toBe(0)
resolve()
})
let output = ''
childProcess.stdout.on('data', (data) => {
output += data
process.stdout.write(data)
})
// cursor forward, choose 'Yes' for custom import alias
childProcess.stdin.write('\u001b[C\n')
// used check here since it needs to wait for the prompt
await check(() => output, /What import alias would you like configured/)
childProcess.stdin.write('@/something/*\n')
})
const tsConfig = require(join(cwd, projectName, 'tsconfig.json'))
expect(tsConfig.compilerOptions.paths).toMatchInlineSnapshot(`
{
"@/something/*": [
"./*",
],
}
`)
})
})
it('should not prompt user for choice and use defaults if --yes is defined', async () => {
await useTempDir(async (cwd) => {
const projectName = 'yes-we-can'
const childProcess = createNextApp(
[projectName, '--yes'],
{
cwd,
},
nextTgzFilename
)
await new Promise<void>((resolve) => {
childProcess.on('exit', async (exitCode) => {
expect(exitCode).toBe(0)
projectFilesShouldExist({
cwd,
projectName,
files: [
'app',
'package.json',
'postcss.config.mjs',
'tsconfig.json',
'AGENTS.md',
'CLAUDE.md',
],
})
resolve()
})
})
const pkg = require(join(cwd, projectName, 'package.json'))
expect(pkg.name).toBe(projectName)
const tsConfig = require(join(cwd, projectName, 'tsconfig.json'))
expect(tsConfig.compilerOptions.paths).toMatchInlineSnapshot(`
{
"@/*": [
"./*",
],
}
`)
})
})
it('should use recommended defaults when user selects that option', async () => {
await useTempDir(async (cwd) => {
const projectName = 'recommended-defaults'
const childProcess = createNextApp(
[projectName],
{
cwd,
},
nextTgzFilename
)
await new Promise<void>((resolve) => {
childProcess.on('exit', async (exitCode) => {
expect(exitCode).toBe(0)
projectFilesShouldExist({
cwd,
projectName,
files: [
'app',
'package.json',
'postcss.config.mjs', // tailwind
'tsconfig.json', // typescript
'AGENTS.md', // agent files
'CLAUDE.md',
],
})
resolve()
})
// Select "Yes, use recommended defaults" (default option, just press enter)
childProcess.stdin.write('\n')
})
const pkg = require(join(cwd, projectName, 'package.json'))
expect(pkg.name).toBe(projectName)
})
})
it('should show reuse previous settings option when preferences exist', async () => {
const Conf = require('next/dist/compiled/conf')
await useTempDir(async (cwd) => {
// Manually set preferences to simulate a previous run
const conf = new Conf({ projectName: 'create-next-app' })
conf.set('preferences', {
typescript: false,
eslint: true,
linter: 'eslint',
tailwind: false,
app: false,
srcDir: false,
importAlias: '@/*',
customizeImportAlias: false,
reactCompiler: false,
})
const projectName = 'reuse-prefs-project'
const childProcess = createNextApp(
[projectName],
{
cwd,
},
nextTgzFilename,
false // Don't clear preferences
)
await new Promise<void>(async (resolve) => {
let output = ''
childProcess.stdout.on('data', (data) => {
output += data
process.stdout.write(data)
})
// Select "reuse previous settings" (cursor down once, then enter)
childProcess.stdin.write('\u001b[B\n')
// Wait for the prompt to appear with "reuse previous settings"
await check(() => output, /No, reuse previous settings/)
childProcess.on('exit', async (exitCode) => {
expect(exitCode).toBe(0)
projectFilesShouldExist({
cwd,
projectName,
files: [
'pages', // pages router (not app)
'package.json',
'jsconfig.json', // javascript
],
})
resolve()
})
})
const pkg = require(join(cwd, projectName, 'package.json'))
expect(pkg.name).toBe(projectName)
})
})
it('should prompt user to confirm reset preferences', async () => {
await useTempDir(async (cwd) => {
const childProcess = createNextApp(
['--reset'],
{
cwd,
},
nextTgzFilename
)
await new Promise<void>(async (resolve) => {
childProcess.on('exit', async (exitCode) => {
expect(exitCode).toBe(0)
resolve()
})
let output = ''
childProcess.stdout.on('data', (data) => {
output += data
process.stdout.write(data)
})
await check(
() => output,
/Would you like to reset the saved preferences/
)
// cursor forward, choose 'Yes' for reset preferences
childProcess.stdin.write('\u001b[C\n')
await check(
() => output,
/The preferences have been reset successfully/
)
})
})
})
})

View File

@@ -0,0 +1,157 @@
import {
projectShouldHaveNoGitChanges,
tryNextDev,
run,
useTempDir,
projectFilesShouldExist,
} from '../utils'
import { mapSrcFiles, projectSpecification } from '../lib/specification'
import { projectDepsShouldBe } from '../lib/utils'
function shouldBeApiTemplateProject({
cwd,
projectName,
mode,
srcDir,
}: {
cwd: string
projectName: string
mode: 'js' | 'ts'
srcDir?: boolean
}) {
const template = 'app-api'
projectFilesShouldExist({
cwd,
projectName,
files: mapSrcFiles(projectSpecification[template][mode].files, srcDir),
})
projectDepsShouldBe({
type: 'dependencies',
cwd,
projectName,
deps: mapSrcFiles(projectSpecification[template][mode].deps, srcDir),
})
projectDepsShouldBe({
type: 'devDependencies',
cwd,
projectName,
deps: mapSrcFiles(projectSpecification[template][mode].devDeps, srcDir),
})
}
describe('create-next-app --api (Headless App)', () => {
let nextTgzFilename: string
beforeAll(() => {
if (!process.env.NEXT_TEST_PKG_PATHS) {
throw new Error('This test needs to be run with `node run-tests.js`.')
}
const pkgPaths = new Map<string, string>(
JSON.parse(process.env.NEXT_TEST_PKG_PATHS)
)
nextTgzFilename = pkgPaths.get('next')!
})
it('should create JavaScript project with --js flag', async () => {
await useTempDir(async (cwd) => {
const projectName = 'app-js'
const { exitCode } = await run(
[
projectName,
'--js',
'--api',
'--no-src-dir',
'--no-import-alias',
'--no-agents-md',
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
],
nextTgzFilename,
{
cwd,
}
)
expect(exitCode).toBe(0)
shouldBeApiTemplateProject({
cwd,
projectName,
mode: 'js',
})
await tryNextDev({
cwd,
isApi: true,
projectName,
})
})
})
it('should create TypeScript project with --ts flag', async () => {
await useTempDir(async (cwd) => {
const projectName = 'app-ts'
const { exitCode } = await run(
[
projectName,
'--ts',
'--api',
'--no-src-dir',
'--no-import-alias',
'--no-agents-md',
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
],
nextTgzFilename,
{
cwd,
}
)
expect(exitCode).toBe(0)
shouldBeApiTemplateProject({
cwd,
projectName,
mode: 'ts',
})
await tryNextDev({ cwd, isApi: true, projectName })
projectShouldHaveNoGitChanges({ cwd, projectName })
})
})
it('should create project inside "src" directory with --src-dir flag', async () => {
await useTempDir(async (cwd) => {
const projectName = 'app-src-dir'
const { exitCode } = await run(
[
projectName,
'--ts',
'--api',
'--src-dir',
'--no-import-alias',
'--no-agents-md',
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
],
nextTgzFilename,
{
cwd,
stdio: 'inherit',
}
)
expect(exitCode).toBe(0)
shouldBeApiTemplateProject({
cwd,
projectName,
mode: 'ts',
srcDir: true,
})
await tryNextDev({
cwd,
isApi: true,
projectName,
})
})
})
})

View File

@@ -0,0 +1,238 @@
import {
projectShouldHaveNoGitChanges,
shouldBeTemplateProject,
tryNextDev,
run,
useTempDir,
} from '../utils'
describe('create-next-app --app (App Router)', () => {
let nextTgzFilename: string
beforeAll(() => {
if (!process.env.NEXT_TEST_PKG_PATHS) {
throw new Error('This test needs to be run with `node run-tests.js`.')
}
const pkgPaths = new Map<string, string>(
JSON.parse(process.env.NEXT_TEST_PKG_PATHS)
)
nextTgzFilename = pkgPaths.get('next')
})
it('should create JavaScript project with --js flag', async () => {
await useTempDir(async (cwd) => {
const projectName = 'app-js'
const { exitCode } = await run(
[
projectName,
'--js',
'--app',
'--eslint',
'--no-src-dir',
'--no-tailwind',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
],
nextTgzFilename,
{
cwd,
}
)
expect(exitCode).toBe(0)
shouldBeTemplateProject({ cwd, projectName, template: 'app', mode: 'js' })
await tryNextDev({
cwd,
projectName,
})
})
})
it('should create TypeScript project with --ts flag', async () => {
await useTempDir(async (cwd) => {
const projectName = 'app-ts'
const { exitCode } = await run(
[
projectName,
'--ts',
'--app',
'--eslint',
'--no-src-dir',
'--no-tailwind',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
],
nextTgzFilename,
{
cwd,
}
)
expect(exitCode).toBe(0)
shouldBeTemplateProject({ cwd, projectName, template: 'app', mode: 'ts' })
await tryNextDev({ cwd, projectName })
projectShouldHaveNoGitChanges({ cwd, projectName })
})
})
it('should create project inside "src" directory with --src-dir flag', async () => {
await useTempDir(async (cwd) => {
const projectName = 'app-src-dir'
const { exitCode } = await run(
[
projectName,
'--ts',
'--app',
'--eslint',
'--src-dir',
'--no-tailwind',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
],
nextTgzFilename,
{
cwd,
stdio: 'inherit',
}
)
expect(exitCode).toBe(0)
shouldBeTemplateProject({
cwd,
projectName,
template: 'app',
mode: 'ts',
srcDir: true,
})
await tryNextDev({
cwd,
projectName,
})
})
})
it('should create TailwindCSS project with --tailwind flag', async () => {
await useTempDir(async (cwd) => {
const projectName = 'app-tw'
const { exitCode } = await run(
[
projectName,
'--ts',
'--app',
'--eslint',
'--src-dir',
'--tailwind',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
],
nextTgzFilename,
{
cwd,
}
)
expect(exitCode).toBe(0)
shouldBeTemplateProject({
cwd,
projectName,
template: 'app-tw',
mode: 'ts',
srcDir: true,
})
await tryNextDev({
cwd,
projectName,
})
})
})
it('should create an empty project with --empty flag', async () => {
await useTempDir(async (cwd) => {
const projectName = 'app-empty'
const { exitCode } = await run(
[
projectName,
'--ts',
'--app',
'--eslint',
'--src-dir',
'--empty',
'--no-tailwind',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
],
nextTgzFilename,
{
cwd,
}
)
const isEmpty = true
expect(exitCode).toBe(0)
shouldBeTemplateProject({
cwd,
projectName,
template: 'app-empty',
mode: 'ts',
srcDir: true,
})
await tryNextDev({
cwd,
projectName,
isEmpty,
})
})
})
it('should create an empty TailwindCSS project with --empty flag', async () => {
await useTempDir(async (cwd) => {
const projectName = 'app-tw-empty'
const { exitCode } = await run(
[
projectName,
'--ts',
'--app',
'--eslint',
'--src-dir',
'--tailwind',
'--empty',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
],
nextTgzFilename,
{
cwd,
}
)
const isEmpty = true
expect(exitCode).toBe(0)
shouldBeTemplateProject({
cwd,
projectName,
template: 'app-tw-empty',
mode: 'ts',
srcDir: true,
})
await tryNextDev({
cwd,
projectName,
isEmpty,
})
})
})
})

View File

@@ -0,0 +1,87 @@
import { run, tryNextDev, useTempDir } from '../utils'
describe.each(['app', 'pages'] as const)(
'CNA options matrix - %s',
(pagesOrApp) => {
let nextTgzFilename: string
beforeAll(() => {
if (!process.env.NEXT_TEST_PKG_PATHS) {
throw new Error('This test needs to be run with `node run-tests.js`.')
}
const pkgPaths = new Map<string, string>(
JSON.parse(process.env.NEXT_TEST_PKG_PATHS)
)
nextTgzFilename = pkgPaths.get('next')!
})
const isApp = pagesOrApp === 'app'
const allFlagValues = {
app: [isApp ? '--app' : '--no-app'],
ts: ['--js', '--ts'],
importAlias: [
'--import-alias=@acme/*',
'--import-alias=@/*',
'--no-import-alias',
],
// doesn't affect if the app builds or not
// eslint: ['--eslint', '--no-linter'],
eslint: ['--eslint'],
// Trading test perf for robustness:
// srcDir and reactCompiler don't interact so we're testing them together
// instead of all permutations.
srcDirAndCompiler: [
'--src-dir --react-compiler --no-agents-md',
'--no-src-dir --no-react-compiler --no-agents-md',
],
tailwind: ['--tailwind', '--no-tailwind'],
// shouldn't affect if the app builds or not
// packageManager: ['--use-npm', '--use-pnpm', '--use-yarn', '--use-bun'],
}
const getCombinations = (items: string[][]): string[][] => {
if (!items.length) return [[]]
const [first, ...rest] = items
const children = getCombinations(rest)
return first.flatMap((value) =>
children.map((child) => [...value.split(' '), ...child])
)
}
const flagCombinations = getCombinations(Object.values(allFlagValues))
const testCases = flagCombinations.map((flags) => ({
name: flags.join(' '),
flags,
}))
let id = 0
it.each(testCases)('$name', async ({ flags }) => {
await useTempDir(async (cwd) => {
const projectName = `cna-matrix-${pagesOrApp}-${id++}`
const { exitCode } = await run(
[
projectName,
...flags,
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
],
nextTgzFilename,
{
cwd,
}
)
expect(exitCode).toBe(0)
await tryNextDev({
cwd,
projectName,
isApp,
})
})
})
}
)

View File

@@ -0,0 +1,249 @@
import {
projectShouldHaveNoGitChanges,
run,
shouldBeTemplateProject,
tryNextDev,
useTempDir,
} from '../utils'
describe('create-next-app --no-app (Pages Router)', () => {
let nextTgzFilename: string
beforeAll(() => {
if (!process.env.NEXT_TEST_PKG_PATHS) {
throw new Error('This test needs to be run with `node run-tests.js`.')
}
const pkgPaths = new Map<string, string>(
JSON.parse(process.env.NEXT_TEST_PKG_PATHS)
)
nextTgzFilename = pkgPaths.get('next')
})
it('should create JavaScript project with --js flag', async () => {
await useTempDir(async (cwd) => {
const projectName = 'pages-js'
const { exitCode } = await run(
[
projectName,
'--js',
'--no-app',
'--eslint',
'--no-src-dir',
'--no-tailwind',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
],
nextTgzFilename,
{
cwd,
}
)
expect(exitCode).toBe(0)
shouldBeTemplateProject({
cwd,
projectName,
template: 'default',
mode: 'js',
})
await tryNextDev({
cwd,
projectName,
isApp: false,
})
})
})
it('should create TypeScript project with --ts flag', async () => {
await useTempDir(async (cwd) => {
const projectName = 'pages-ts'
const { exitCode } = await run(
[
projectName,
'--ts',
'--no-app',
'--eslint',
'--no-src-dir',
'--no-tailwind',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
],
nextTgzFilename,
{
cwd,
}
)
expect(exitCode).toBe(0)
shouldBeTemplateProject({
cwd,
projectName,
template: 'default',
mode: 'ts',
})
await tryNextDev({ cwd, projectName, isApp: false })
await projectShouldHaveNoGitChanges({ cwd, projectName })
})
})
it('should create project inside "src" directory with --src-dir flag', async () => {
await useTempDir(async (cwd) => {
const projectName = 'pages-src-dir'
const { exitCode } = await run(
[
projectName,
'--ts',
'--no-app',
'--eslint',
'--src-dir',
'--no-tailwind',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
],
nextTgzFilename,
{
cwd,
}
)
expect(exitCode).toBe(0)
shouldBeTemplateProject({
cwd,
projectName,
template: 'default',
mode: 'ts',
srcDir: true,
})
await tryNextDev({
cwd,
projectName,
isApp: false,
})
})
})
it('should create TailwindCSS project with --tailwind flag', async () => {
await useTempDir(async (cwd) => {
const projectName = 'pages-tw'
const { exitCode } = await run(
[
projectName,
'--ts',
'--no-app',
'--eslint',
'--src-dir',
'--tailwind',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
],
nextTgzFilename,
{
cwd,
}
)
expect(exitCode).toBe(0)
shouldBeTemplateProject({
cwd,
projectName,
template: 'default-tw',
mode: 'ts',
srcDir: true,
})
await tryNextDev({
cwd,
projectName,
isApp: false,
})
})
})
it('should create an empty project with --empty flag', async () => {
await useTempDir(async (cwd) => {
const projectName = 'pages-empty'
const { exitCode } = await run(
[
projectName,
'--ts',
'--no-app',
'--eslint',
'--src-dir',
'--no-tailwind',
'--empty',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
],
nextTgzFilename,
{
cwd,
}
)
const isEmpty = true
expect(exitCode).toBe(0)
shouldBeTemplateProject({
cwd,
projectName,
template: 'default-empty',
mode: 'ts',
srcDir: true,
})
await tryNextDev({
cwd,
projectName,
isApp: false,
isEmpty,
})
})
})
it('should create an empty TailwindCSS project with --empty flag', async () => {
await useTempDir(async (cwd) => {
const projectName = 'pages-tw-empty'
const { exitCode } = await run(
[
projectName,
'--ts',
'--no-app',
'--eslint',
'--src-dir',
'--tailwind',
'--empty',
'--no-import-alias',
'--no-react-compiler',
'--no-agents-md',
...(process.env.NEXT_RSPACK ? ['--rspack'] : []),
],
nextTgzFilename,
{
cwd,
}
)
const isEmpty = true
expect(exitCode).toBe(0)
shouldBeTemplateProject({
cwd,
projectName,
template: 'default-tw-empty',
mode: 'ts',
srcDir: true,
})
await tryNextDev({
cwd,
projectName,
isApp: false,
isEmpty,
})
})
})
})

View File

@@ -0,0 +1,100 @@
import execa from 'execa'
import { join } from 'path'
import { fetchViaHTTP, findPort, killApp, launchApp } from 'next-test-utils'
export const CNA_PATH = require.resolve('create-next-app/dist/index.js')
export const EXAMPLE_REPO = 'https://github.com/vercel/next.js/tree/canary'
export const EXAMPLE_PATH = 'examples/basic-css'
export const FULL_EXAMPLE_PATH = `${EXAMPLE_REPO}/${EXAMPLE_PATH}`
export const DEFAULT_FILES = [
'.gitignore',
'package.json',
'app/page.tsx',
'app/layout.tsx',
'node_modules/next',
]
export const run = async (
args: string[],
nextJSVersion: string,
options:
| execa.Options
| {
reject?: boolean
env?: Record<string, string>
}
) => {
return execa('node', [CNA_PATH].concat(args), {
// tests with options.reject false are expected to exit(1) so don't inherit
stdio: options.reject === false ? 'pipe' : 'inherit',
...options,
env: {
...process.env,
...options.env,
NEXT_PRIVATE_TEST_VERSION: nextJSVersion,
},
})
}
export const command = (cmd: string, args: string[]) =>
execa(cmd, args, {
stdio: 'inherit',
env: { ...process.env },
})
export async function tryNextDev({
cwd,
projectName,
isApp = true,
isApi = false,
isEmpty = false,
}: {
cwd: string
projectName: string
isApp?: boolean
isApi?: boolean
isEmpty?: boolean
}) {
const dir = join(cwd, projectName)
const port = await findPort()
const app = await launchApp(dir, port, {
nextBin: join(dir, 'node_modules/next/dist/bin/next'),
})
try {
const res = await fetchViaHTTP(port, '/')
if (isEmpty || isApi) {
expect(await res.text()).toContain('Hello world!')
} else {
const responseText = await res.text()
// App Router uses page.tsx/page.js, Pages Router uses index.tsx/index.js
const hasAppRouterText =
responseText.includes('To get started, edit the page.tsx file.') ||
responseText.includes('To get started, edit the page.js file.')
const hasPagesRouterText =
responseText.includes('To get started, edit the index.tsx file.') ||
responseText.includes('To get started, edit the index.js file.')
expect(hasAppRouterText || hasPagesRouterText).toBe(true)
}
expect(res.status).toBe(200)
if (!isApp && !isEmpty) {
const apiRes = await fetchViaHTTP(port, '/api/hello')
expect(await apiRes.json()).toEqual({ name: 'John Doe' })
expect(apiRes.status).toBe(200)
}
} finally {
await killApp(app)
}
}
export {
createNextApp,
projectFilesShouldExist,
projectFilesShouldNotExist,
projectShouldHaveNoGitChanges,
shouldBeTemplateProject,
shouldBeJavascriptProject,
shouldBeTypescriptProject,
} from './lib/utils'
export { useTempDir } from '../../lib/use-temp-dir'