feat(cpa): support next.config.ts (#7367)

Support new `next.config.ts` config file.

Had to do some weird gymnastics around `swc` in order to use it within
unit tests. Had to pass through the `parsed.span.end` value of any
previous iteration and account for it.

Looks to be an open issue here:
https://github.com/swc-project/swc/issues/1366

Fixes #7318
This commit is contained in:
Elliot DeNolf
2024-07-26 10:33:46 -04:00
committed by GitHub
parent 55c6ce92b0
commit a64f37e014
7 changed files with 289 additions and 120 deletions

View File

@@ -50,6 +50,7 @@
"dependencies": { "dependencies": {
"@clack/prompts": "^0.7.0", "@clack/prompts": "^0.7.0",
"@sindresorhus/slugify": "^1.1.0", "@sindresorhus/slugify": "^1.1.0",
"@swc/core": "^1.6.13",
"arg": "^5.0.0", "arg": "^5.0.0",
"chalk": "^4.1.0", "chalk": "^4.1.0",
"comment-json": "^4.2.3", "comment-json": "^4.2.3",

View File

@@ -79,7 +79,7 @@ export async function initNext(args: InitNextArgs): Promise<InitNextResult> {
const installSpinner = p.spinner() const installSpinner = p.spinner()
installSpinner.start('Installing Payload and dependencies...') installSpinner.start('Installing Payload and dependencies...')
const configurationResult = installAndConfigurePayload({ const configurationResult = await installAndConfigurePayload({
...args, ...args,
nextAppDetails, nextAppDetails,
nextConfigType, nextConfigType,
@@ -143,15 +143,16 @@ async function addPayloadConfigToTsConfig(projectDir: string, isSrcDir: boolean)
} }
} }
function installAndConfigurePayload( async function installAndConfigurePayload(
args: { args: {
nextAppDetails: NextAppDetails nextAppDetails: NextAppDetails
nextConfigType: NextConfigType nextConfigType: NextConfigType
useDistFiles?: boolean useDistFiles?: boolean
} & InitNextArgs, } & InitNextArgs,
): ): Promise<
| { payloadConfigPath: string; success: true } | { payloadConfigPath: string; success: true }
| { payloadConfigPath?: string; reason: string; success: false } { | { payloadConfigPath?: string; reason: string; success: false }
> {
const { const {
'--debug': debug, '--debug': debug,
nextAppDetails: { isSrcDir, nextAppDir, nextConfigPath } = {}, nextAppDetails: { isSrcDir, nextAppDir, nextConfigPath } = {},
@@ -212,7 +213,7 @@ function installAndConfigurePayload(
copyRecursiveSync(templateSrcDir, path.dirname(nextConfigPath), debug) copyRecursiveSync(templateSrcDir, path.dirname(nextConfigPath), debug)
// Wrap next.config.js with withPayload // Wrap next.config.js with withPayload
wrapNextConfig({ nextConfigPath, nextConfigType }) await wrapNextConfig({ nextConfigPath, nextConfigType })
return { return {
payloadConfigPath: path.resolve(nextAppDir, '../payload.config.ts'), payloadConfigPath: path.resolve(nextAppDir, '../payload.config.ts'),
@@ -240,7 +241,7 @@ export async function getNextAppDetails(projectDir: string): Promise<NextAppDeta
const isSrcDir = fs.existsSync(path.resolve(projectDir, 'src')) const isSrcDir = fs.existsSync(path.resolve(projectDir, 'src'))
const nextConfigPath: string | undefined = ( const nextConfigPath: string | undefined = (
await globby('next.config.*js', { absolute: true, cwd: projectDir }) await globby('next.config.*(t|j)s', { absolute: true, cwd: projectDir })
)?.[0] )?.[0]
if (!nextConfigPath || nextConfigPath.length === 0) { if (!nextConfigPath || nextConfigPath.length === 0) {
@@ -286,8 +287,13 @@ export async function getNextAppDetails(projectDir: string): Promise<NextAppDeta
function getProjectType(args: { function getProjectType(args: {
nextConfigPath: string nextConfigPath: string
packageObj: Record<string, unknown> packageObj: Record<string, unknown>
}): 'cjs' | 'esm' { }): NextConfigType {
const { nextConfigPath, packageObj } = args const { nextConfigPath, packageObj } = args
if (nextConfigPath.endsWith('.ts')) {
return 'ts'
}
if (nextConfigPath.endsWith('.mjs')) { if (nextConfigPath.endsWith('.mjs')) {
return 'esm' return 'esm'
} }

View File

@@ -3,6 +3,35 @@ import { jest } from '@jest/globals'
import { parseAndModifyConfigContent, withPayloadStatement } from './wrap-next-config.js' import { parseAndModifyConfigContent, withPayloadStatement } from './wrap-next-config.js'
const tsConfigs = {
defaultNextConfig: `import type { NextConfig } from "next";
const nextConfig: NextConfig = {};
export default nextConfig;`,
nextConfigExportNamedDefault: `import type { NextConfig } from "next";
const nextConfig: NextConfig = {};
const wrapped = someFunc(asdf);
export { wrapped as default };
`,
nextConfigWithFunc: `import type { NextConfig } from "next";
const nextConfig: NextConfig = {};
export default someFunc(nextConfig);
`,
nextConfigWithFuncMultiline: `import type { NextConfig } from "next";
const nextConfig: NextConfig = {};
export default someFunc(
nextConfig
);
`,
nextConfigWithSpread: `import type { NextConfig } from "next";
const nextConfig: NextConfig = {
...someConfig,
};
export default nextConfig;
`,
}
const esmConfigs = { const esmConfigs = {
defaultNextConfig: `/** @type {import('next').NextConfig} */ defaultNextConfig: `/** @type {import('next').NextConfig} */
const nextConfig = {}; const nextConfig = {};
@@ -52,27 +81,66 @@ module.exports = nextConfig;
} }
describe('parseAndInsertWithPayload', () => { describe('parseAndInsertWithPayload', () => {
describe('ts', () => {
const configType = 'ts'
const importStatement = withPayloadStatement[configType]
it('should parse the default next config', async () => {
const { modifiedConfigContent } = await parseAndModifyConfigContent(
tsConfigs.defaultNextConfig,
configType,
)
expect(modifiedConfigContent).toContain(importStatement)
expect(modifiedConfigContent).toContain('withPayload(nextConfig)')
})
it('should parse the config with a function', async () => {
const { modifiedConfigContent: modifiedConfigContent2 } = await parseAndModifyConfigContent(
tsConfigs.nextConfigWithFunc,
configType,
)
expect(modifiedConfigContent2).toContain('withPayload(someFunc(nextConfig))')
})
it('should parse the config with a multi-lined function', async () => {
const { modifiedConfigContent } = await parseAndModifyConfigContent(
tsConfigs.nextConfigWithFuncMultiline,
configType,
)
expect(modifiedConfigContent).toContain(importStatement)
expect(modifiedConfigContent).toMatch(/withPayload\(someFunc\(\n {2}nextConfig\n\)\)/)
})
it('should parse the config with a spread', async () => {
const { modifiedConfigContent } = await parseAndModifyConfigContent(
tsConfigs.nextConfigWithSpread,
configType,
)
expect(modifiedConfigContent).toContain(importStatement)
expect(modifiedConfigContent).toContain('withPayload(nextConfig)')
})
})
describe('esm', () => { describe('esm', () => {
const configType = 'esm' const configType = 'esm'
const importStatement = withPayloadStatement[configType] const importStatement = withPayloadStatement[configType]
it('should parse the default next config', () => { it('should parse the default next config', async () => {
const { modifiedConfigContent } = parseAndModifyConfigContent( const { modifiedConfigContent } = await parseAndModifyConfigContent(
esmConfigs.defaultNextConfig, esmConfigs.defaultNextConfig,
configType, configType,
) )
expect(modifiedConfigContent).toContain(importStatement) expect(modifiedConfigContent).toContain(importStatement)
expect(modifiedConfigContent).toContain('withPayload(nextConfig)') expect(modifiedConfigContent).toContain('withPayload(nextConfig)')
}) })
it('should parse the config with a function', () => { it('should parse the config with a function', async () => {
const { modifiedConfigContent } = parseAndModifyConfigContent( const { modifiedConfigContent } = await parseAndModifyConfigContent(
esmConfigs.nextConfigWithFunc, esmConfigs.nextConfigWithFunc,
configType, configType,
) )
expect(modifiedConfigContent).toContain('withPayload(someFunc(nextConfig))') expect(modifiedConfigContent).toContain('withPayload(someFunc(nextConfig))')
}) })
it('should parse the config with a function on a new line', () => { it('should parse the config with a multi-lined function', async () => {
const { modifiedConfigContent } = parseAndModifyConfigContent( const { modifiedConfigContent } = await parseAndModifyConfigContent(
esmConfigs.nextConfigWithFuncMultiline, esmConfigs.nextConfigWithFuncMultiline,
configType, configType,
) )
@@ -80,8 +148,8 @@ describe('parseAndInsertWithPayload', () => {
expect(modifiedConfigContent).toMatch(/withPayload\(someFunc\(\n {2}nextConfig\n\)\)/) expect(modifiedConfigContent).toMatch(/withPayload\(someFunc\(\n {2}nextConfig\n\)\)/)
}) })
it('should parse the config with a spread', () => { it('should parse the config with a spread', async () => {
const { modifiedConfigContent } = parseAndModifyConfigContent( const { modifiedConfigContent } = await parseAndModifyConfigContent(
esmConfigs.nextConfigWithSpread, esmConfigs.nextConfigWithSpread,
configType, configType,
) )
@@ -90,10 +158,10 @@ describe('parseAndInsertWithPayload', () => {
}) })
// Unsupported: export { wrapped as default } // Unsupported: export { wrapped as default }
it('should give warning with a named export as default', () => { it('should give warning with a named export as default', async () => {
const warnLogSpy = jest.spyOn(p.log, 'warn').mockImplementation(() => {}) const warnLogSpy = jest.spyOn(p.log, 'warn').mockImplementation(() => {})
const { modifiedConfigContent, success } = parseAndModifyConfigContent( const { modifiedConfigContent, success } = await parseAndModifyConfigContent(
esmConfigs.nextConfigExportNamedDefault, esmConfigs.nextConfigExportNamedDefault,
configType, configType,
) )
@@ -109,39 +177,39 @@ describe('parseAndInsertWithPayload', () => {
describe('cjs', () => { describe('cjs', () => {
const configType = 'cjs' const configType = 'cjs'
const requireStatement = withPayloadStatement[configType] const requireStatement = withPayloadStatement[configType]
it('should parse the default next config', () => { it('should parse the default next config', async () => {
const { modifiedConfigContent } = parseAndModifyConfigContent( const { modifiedConfigContent } = await parseAndModifyConfigContent(
cjsConfigs.defaultNextConfig, cjsConfigs.defaultNextConfig,
configType, configType,
) )
expect(modifiedConfigContent).toContain(requireStatement) expect(modifiedConfigContent).toContain(requireStatement)
expect(modifiedConfigContent).toContain('withPayload(nextConfig)') expect(modifiedConfigContent).toContain('withPayload(nextConfig)')
}) })
it('should parse anonymous default config', () => { it('should parse anonymous default config', async () => {
const { modifiedConfigContent } = parseAndModifyConfigContent( const { modifiedConfigContent } = await parseAndModifyConfigContent(
cjsConfigs.anonConfig, cjsConfigs.anonConfig,
configType, configType,
) )
expect(modifiedConfigContent).toContain(requireStatement) expect(modifiedConfigContent).toContain(requireStatement)
expect(modifiedConfigContent).toContain('withPayload({})') expect(modifiedConfigContent).toContain('withPayload({})')
}) })
it('should parse the config with a function', () => { it('should parse the config with a function', async () => {
const { modifiedConfigContent } = parseAndModifyConfigContent( const { modifiedConfigContent } = await parseAndModifyConfigContent(
cjsConfigs.nextConfigWithFunc, cjsConfigs.nextConfigWithFunc,
configType, configType,
) )
expect(modifiedConfigContent).toContain('withPayload(someFunc(nextConfig))') expect(modifiedConfigContent).toContain('withPayload(someFunc(nextConfig))')
}) })
it('should parse the config with a function on a new line', () => { it('should parse the config with a multi-lined function', async () => {
const { modifiedConfigContent } = parseAndModifyConfigContent( const { modifiedConfigContent } = await parseAndModifyConfigContent(
cjsConfigs.nextConfigWithFuncMultiline, cjsConfigs.nextConfigWithFuncMultiline,
configType, configType,
) )
expect(modifiedConfigContent).toContain(requireStatement) expect(modifiedConfigContent).toContain(requireStatement)
expect(modifiedConfigContent).toMatch(/withPayload\(someFunc\(\n {2}nextConfig\n\)\)/) expect(modifiedConfigContent).toMatch(/withPayload\(someFunc\(\n {2}nextConfig\n\)\)/)
}) })
it('should parse the config with a named export as default', () => { it('should parse the config with a named export as default', async () => {
const { modifiedConfigContent } = parseAndModifyConfigContent( const { modifiedConfigContent } = await parseAndModifyConfigContent(
cjsConfigs.nextConfigExportNamedDefault, cjsConfigs.nextConfigExportNamedDefault,
configType, configType,
) )
@@ -149,8 +217,8 @@ describe('parseAndInsertWithPayload', () => {
expect(modifiedConfigContent).toContain('withPayload(wrapped)') expect(modifiedConfigContent).toContain('withPayload(wrapped)')
}) })
it('should parse the config with a spread', () => { it('should parse the config with a spread', async () => {
const { modifiedConfigContent } = parseAndModifyConfigContent( const { modifiedConfigContent } = await parseAndModifyConfigContent(
cjsConfigs.nextConfigWithSpread, cjsConfigs.nextConfigWithSpread,
configType, configType,
) )

View File

@@ -1,25 +1,27 @@
import type { Program } from 'esprima-next' import type { ExportDefaultExpression, ModuleItem } from '@swc/core'
import swc from '@swc/core'
import chalk from 'chalk' import chalk from 'chalk'
import { Syntax, parseModule } from 'esprima-next' import { Syntax, parseModule } from 'esprima-next'
import fs from 'fs' import fs from 'fs'
import type { NextConfigType } from '../types.js'
import { log, warning } from '../utils/log.js' import { log, warning } from '../utils/log.js'
export const withPayloadStatement = { export const withPayloadStatement = {
cjs: `const { withPayload } = require('@payloadcms/next/withPayload')\n`, cjs: `const { withPayload } = require("@payloadcms/next/withPayload");`,
esm: `import { withPayload } from '@payloadcms/next/withPayload'\n`, esm: `import { withPayload } from "@payloadcms/next/withPayload";`,
ts: `import { withPayload } from "@payloadcms/next/withPayload";`,
} }
type NextConfigType = 'cjs' | 'esm' export const wrapNextConfig = async (args: {
export const wrapNextConfig = (args: {
nextConfigPath: string nextConfigPath: string
nextConfigType: NextConfigType nextConfigType: NextConfigType
}) => { }) => {
const { nextConfigPath, nextConfigType: configType } = args const { nextConfigPath, nextConfigType: configType } = args
const configContent = fs.readFileSync(nextConfigPath, 'utf8') const configContent = fs.readFileSync(nextConfigPath, 'utf8')
const { modifiedConfigContent: newConfig, success } = parseAndModifyConfigContent( const { modifiedConfigContent: newConfig, success } = await parseAndModifyConfigContent(
configContent, configContent,
configType, configType,
) )
@@ -34,27 +36,45 @@ export const wrapNextConfig = (args: {
/** /**
* Parses config content with AST and wraps it with withPayload function * Parses config content with AST and wraps it with withPayload function
*/ */
export function parseAndModifyConfigContent( export async function parseAndModifyConfigContent(
content: string, content: string,
configType: NextConfigType, configType: NextConfigType,
): { modifiedConfigContent: string; success: boolean } { ): Promise<{ modifiedConfigContent: string; success: boolean }> {
content = withPayloadStatement[configType] + content content = withPayloadStatement[configType] + '\n' + content
let ast: Program | undefined console.log({ configType, content })
if (configType === 'cjs' || configType === 'esm') {
try { try {
ast = parseModule(content, { loc: true }) const ast = parseModule(content, { loc: true })
} catch (error: unknown) {
if (error instanceof Error) { if (configType === 'cjs') {
warning(`Unable to parse Next config. Error: ${error.message} `) // Find `module.exports = X`
warnUserWrapNotSuccessful(configType) const moduleExports = ast.body.find(
(p) =>
p.type === Syntax.ExpressionStatement &&
p.expression?.type === Syntax.AssignmentExpression &&
p.expression.left?.type === Syntax.MemberExpression &&
p.expression.left.object?.type === Syntax.Identifier &&
p.expression.left.object.name === 'module' &&
p.expression.left.property?.type === Syntax.Identifier &&
p.expression.left.property.name === 'exports',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) as any
if (moduleExports && moduleExports.expression.right?.loc) {
const modifiedConfigContent = insertBeforeAndAfter(
content,
moduleExports.expression.right.loc,
)
return { modifiedConfigContent, success: true }
} }
return {
return Promise.resolve({
modifiedConfigContent: content, modifiedConfigContent: content,
success: false, success: false,
} })
} } else if (configType === 'esm') {
if (configType === 'esm') {
const exportDefaultDeclaration = ast.body.find( const exportDefaultDeclaration = ast.body.find(
(p) => p.type === Syntax.ExportDefaultDeclaration, (p) => p.type === Syntax.ExportDefaultDeclaration,
) as Directive | undefined ) as Directive | undefined
@@ -96,51 +116,62 @@ export function parseAndModifyConfigContent(
warning('Could not automatically wrap Next config with withPayload.') warning('Could not automatically wrap Next config with withPayload.')
warnUserWrapNotSuccessful(configType) warnUserWrapNotSuccessful(configType)
return Promise.resolve({
modifiedConfigContent: content,
success: false,
})
}
} catch (error: unknown) {
if (error instanceof Error) {
warning(`Unable to parse Next config. Error: ${error.message} `)
warnUserWrapNotSuccessful(configType)
}
return { return {
modifiedConfigContent: content, modifiedConfigContent: content,
success: false, success: false,
} }
} else if (configType === 'cjs') { }
// Find `module.exports = X` } else if (configType === 'ts') {
const moduleExports = ast.body.find( const { moduleItems, parseOffset } = await compileTypeScriptFileToAST(content)
(p) =>
p.type === Syntax.ExpressionStatement &&
p.expression?.type === Syntax.AssignmentExpression &&
p.expression.left?.type === Syntax.MemberExpression &&
p.expression.left.object?.type === Syntax.Identifier &&
p.expression.left.object.name === 'module' &&
p.expression.left.property?.type === Syntax.Identifier &&
p.expression.left.property.name === 'exports',
// eslint-disable-next-line @typescript-eslint/no-explicit-any
) as any
if (moduleExports && moduleExports.expression.right?.loc) { const exportDefaultDeclaration = moduleItems.find(
const modifiedConfigContent = insertBeforeAndAfter( (m) =>
m.type === 'ExportDefaultExpression' &&
(m.expression.type === 'Identifier' || m.expression.type === 'CallExpression'),
) as ExportDefaultExpression | undefined
if (exportDefaultDeclaration) {
if (!('span' in exportDefaultDeclaration.expression)) {
warning('Could not automatically wrap Next config with withPayload.')
warnUserWrapNotSuccessful(configType)
return Promise.resolve({
modifiedConfigContent: content,
success: false,
})
}
const modifiedConfigContent = insertBeforeAndAfterSWC(
content, content,
moduleExports.expression.right.loc, exportDefaultDeclaration.expression.span,
parseOffset,
) )
return { modifiedConfigContent, success: true } return { modifiedConfigContent, success: true }
} }
return {
modifiedConfigContent: content,
success: false,
}
} }
warning('Could not automatically wrap Next config with withPayload.') warning('Could not automatically wrap Next config with withPayload.')
warnUserWrapNotSuccessful(configType) warnUserWrapNotSuccessful(configType)
return { return Promise.resolve({
modifiedConfigContent: content, modifiedConfigContent: content,
success: false, success: false,
} })
} }
function warnUserWrapNotSuccessful(configType: NextConfigType) { function warnUserWrapNotSuccessful(configType: NextConfigType) {
// Output directions for user to update next.config.js // Output directions for user to update next.config.js
const withPayloadMessage = ` const withPayloadMessage = `
${chalk.bold(`Please manually wrap your existing next.config.js with the withPayload function. Here is an example:`)} ${chalk.bold(`Please manually wrap your existing Next config with the withPayload function. Here is an example:`)}
${withPayloadStatement[configType]} ${withPayloadStatement[configType]}
@@ -148,7 +179,7 @@ function warnUserWrapNotSuccessful(configType: NextConfigType) {
// Your Next.js config here // Your Next.js config here
} }
${configType === 'esm' ? 'export default withPayload(nextConfig)' : 'module.exports = withPayload(nextConfig)'} ${configType === 'cjs' ? 'module.exports = withPayload(nextConfig)' : 'export default withPayload(nextConfig)'}
` `
@@ -186,7 +217,7 @@ type Loc = {
start: { column: number; line: number } start: { column: number; line: number }
} }
function insertBeforeAndAfter(content: string, loc: Loc) { function insertBeforeAndAfter(content: string, loc: Loc): string {
const { end, start } = loc const { end, start } = loc
const lines = content.split('\n') const lines = content.split('\n')
@@ -205,3 +236,57 @@ function insertBeforeAndAfter(content: string, loc: Loc) {
return lines.join('\n') return lines.join('\n')
} }
function insertBeforeAndAfterSWC(
content: string,
span: ModuleItem['span'],
/**
* WARNING: This is ONLY for unit tests. Defaults to 0 otherwise.
*
* @see compileTypeScriptFileToAST
*/
parseOffset: number,
): string {
const { end: preOffsetEnd, start: preOffsetStart } = span
const start = preOffsetStart - parseOffset
const end = preOffsetEnd - parseOffset
const insert = (pos: number, text: string): string => {
return content.slice(0, pos) + text + content.slice(pos)
}
// insert ) after end
content = insert(end - 1, ')')
// insert withPayload before start
content = insert(start - 1, 'withPayload(')
return content
}
/**
* Compile typescript to AST using the swc compiler
*/
async function compileTypeScriptFileToAST(
fileContent: string,
): Promise<{ moduleItems: ModuleItem[]; parseOffset: number }> {
let parseOffset = 0
/**
* WARNING: This is ONLY for unit tests.
*
* Multiple instances of swc DO NOT reset the .span.end value.
* During unit tests, the .spawn.end value is read and accounted for.
*
* https://github.com/swc-project/swc/issues/1366
*/
if (process.env.NODE_ENV === 'test') {
parseOffset = (await swc.parse('')).span.end
}
const module = await swc.parse(fileContent, {
syntax: 'typescript',
})
return { moduleItems: module.body, parseOffset }
}

View File

@@ -75,6 +75,6 @@ export type NextAppDetails = {
nextConfigType?: NextConfigType nextConfigType?: NextConfigType
} }
export type NextConfigType = 'cjs' | 'esm' export type NextConfigType = 'cjs' | 'esm' | 'ts'
export type StorageAdapterType = 'localDisk' | 'payloadCloud' | 'vercelBlobStorage' export type StorageAdapterType = 'localDisk' | 'payloadCloud' | 'vercelBlobStorage'

3
pnpm-lock.yaml generated
View File

@@ -206,6 +206,9 @@ importers:
'@sindresorhus/slugify': '@sindresorhus/slugify':
specifier: ^1.1.0 specifier: ^1.1.0
version: 1.1.2 version: 1.1.2
'@swc/core':
specifier: ^1.6.13
version: 1.6.13
arg: arg:
specifier: ^5.0.0 specifier: ^5.0.0
version: 5.0.2 version: 5.0.2

View File

@@ -1,3 +1,4 @@
/* eslint-disable jest/no-conditional-in-test */
import type { CompilerOptions } from 'typescript' import type { CompilerOptions } from 'typescript'
import * as CommentJson from 'comment-json' import * as CommentJson from 'comment-json'
@@ -13,11 +14,12 @@ import { promisify } from 'util'
const readFile = promisify(fs.readFile) const readFile = promisify(fs.readFile)
const writeFile = promisify(fs.writeFile) const writeFile = promisify(fs.writeFile)
const commonNextCreateParams = '--typescript --eslint --no-tailwind --app --import-alias="@/*"' const commonNextCreateParams =
'--typescript --eslint --no-tailwind --app --import-alias="@/*" --turbo --yes'
const nextCreateCommands: Partial<Record<'noSrcDir' | 'srcDir', string>> = { const nextCreateCommands: Partial<Record<'noSrcDir' | 'srcDir', string>> = {
noSrcDir: `pnpm create next-app@latest . ${commonNextCreateParams} --no-src-dir`, noSrcDir: `pnpm create next-app@canary . ${commonNextCreateParams} --no-src-dir`,
srcDir: `pnpm create next-app@latest . ${commonNextCreateParams} --src-dir`, srcDir: `pnpm create next-app@canary . ${commonNextCreateParams} --src-dir`,
} }
describe('create-payload-app', () => { describe('create-payload-app', () => {
@@ -41,7 +43,11 @@ describe('create-payload-app', () => {
// Create a new Next.js project with default options // Create a new Next.js project with default options
console.log(`Running: ${nextCreateCommands[nextCmdKey]} in ${projectDir}`) console.log(`Running: ${nextCreateCommands[nextCmdKey]} in ${projectDir}`)
const [cmd, ...args] = nextCreateCommands[nextCmdKey].split(' ') const [cmd, ...args] = nextCreateCommands[nextCmdKey].split(' ')
const { exitCode, stderr } = await execa(cmd, [...args], { cwd: projectDir }) console.log(`Running: ${cmd} ${args.join(' ')}`)
const { exitCode, stderr } = await execa(cmd, [...args], {
cwd: projectDir,
stdio: 'inherit',
})
if (exitCode !== 0) { if (exitCode !== 0) {
console.error({ exitCode, stderr }) console.error({ exitCode, stderr })
} }