feat(cpa): handle next.js app with and without src dir
This commit is contained in:
@@ -5,6 +5,7 @@ import { createProject } from './create-project.js'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { dbReplacements } from './packages.js'
|
||||
import { getValidTemplates } from './templates.js'
|
||||
import globby from 'globby'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
@@ -104,12 +105,17 @@ describe('createProject', () => {
|
||||
Object.keys(packageJson.dependencies).filter((n) => n.startsWith('@payloadcms/db-')),
|
||||
).toHaveLength(1)
|
||||
|
||||
let payloadConfigPath = path.resolve(projectDir, 'payload.config.ts')
|
||||
const payloadConfigPath = (
|
||||
await globby('**/payload.config.ts', {
|
||||
absolute: true,
|
||||
cwd: projectDir,
|
||||
})
|
||||
)?.[0]
|
||||
|
||||
// Website and ecommerce templates have payload.config.ts in src/payload
|
||||
if (!fse.existsSync(payloadConfigPath)) {
|
||||
payloadConfigPath = path.resolve(projectDir, 'src/payload/payload.config.ts')
|
||||
if (!payloadConfigPath) {
|
||||
throw new Error(`Could not find payload.config.ts inside ${projectDir}`)
|
||||
}
|
||||
|
||||
const content = fse.readFileSync(payloadConfigPath, 'utf-8')
|
||||
|
||||
// Check payload.config.ts
|
||||
|
||||
@@ -29,17 +29,21 @@ type InitNextArgs = Pick<CliArgs, '--debug'> & {
|
||||
projectDir: string
|
||||
useDistFiles?: boolean
|
||||
}
|
||||
|
||||
type InitNextResult =
|
||||
| {
|
||||
isSrcDir: boolean
|
||||
nextAppDir: string
|
||||
payloadConfigPath: string
|
||||
success: true
|
||||
}
|
||||
| { reason: string; success: false }
|
||||
| { isSrcDir: boolean; nextAppDir?: string; reason: string; success: false }
|
||||
|
||||
export async function initNext(args: InitNextArgs): Promise<InitNextResult> {
|
||||
const { packageManager, projectDir } = args
|
||||
|
||||
const isSrcDir = fs.existsSync(path.resolve(projectDir, 'src'))
|
||||
|
||||
// Get app directory. Could be top-level or src/app
|
||||
const nextAppDir = (
|
||||
await globby(['**/app'], {
|
||||
@@ -50,7 +54,7 @@ export async function initNext(args: InitNextArgs): Promise<InitNextResult> {
|
||||
)?.[0]
|
||||
|
||||
if (!nextAppDir) {
|
||||
return { reason: `Could not find app directory in ${projectDir}`, success: false }
|
||||
return { isSrcDir, reason: `Could not find app directory in ${projectDir}`, success: false }
|
||||
}
|
||||
|
||||
// Check for top-level layout.tsx
|
||||
@@ -58,29 +62,42 @@ export async function initNext(args: InitNextArgs): Promise<InitNextResult> {
|
||||
if (fs.existsSync(layoutPath)) {
|
||||
// Output directions for user to move all files from app to top-level directory named `(app)`
|
||||
log(moveMessage({ nextAppDir, projectDir }))
|
||||
return { reason: 'Found existing layout.tsx in app directory', success: false }
|
||||
return {
|
||||
isSrcDir,
|
||||
nextAppDir,
|
||||
reason: 'Found existing layout.tsx in app directory',
|
||||
success: false,
|
||||
}
|
||||
}
|
||||
|
||||
const configurationResult = installAndConfigurePayload({
|
||||
...args,
|
||||
isSrcDir,
|
||||
nextAppDir,
|
||||
useDistFiles: true, // Requires running 'pnpm pack-template-files' in cpa
|
||||
})
|
||||
|
||||
if (!configurationResult.success) return configurationResult
|
||||
if (configurationResult.success === false) {
|
||||
return { ...configurationResult, isSrcDir, success: false }
|
||||
}
|
||||
|
||||
const { success: installSuccess } = await installDeps(projectDir, packageManager)
|
||||
if (!installSuccess) {
|
||||
return { ...configurationResult, reason: 'Failed to install dependencies', success: false }
|
||||
return {
|
||||
...configurationResult,
|
||||
isSrcDir,
|
||||
reason: 'Failed to install dependencies',
|
||||
success: false,
|
||||
}
|
||||
}
|
||||
|
||||
// Add `@payload-config` to tsconfig.json `paths`
|
||||
await addPayloadConfigToTsConfig(projectDir)
|
||||
await addPayloadConfigToTsConfig(projectDir, isSrcDir)
|
||||
|
||||
return configurationResult
|
||||
return { ...configurationResult, isSrcDir, nextAppDir, success: true }
|
||||
}
|
||||
|
||||
async function addPayloadConfigToTsConfig(projectDir: string) {
|
||||
async function addPayloadConfigToTsConfig(projectDir: string, isSrcDir: boolean) {
|
||||
const tsConfigPath = path.resolve(projectDir, 'tsconfig.json')
|
||||
const userTsConfigContent = await readFile(tsConfigPath, {
|
||||
encoding: 'utf8',
|
||||
@@ -95,14 +112,18 @@ async function addPayloadConfigToTsConfig(projectDir: string) {
|
||||
if (!userTsConfig.compilerOptions.paths?.['@payload-config']) {
|
||||
userTsConfig.compilerOptions.paths = {
|
||||
...(userTsConfig.compilerOptions.paths || {}),
|
||||
'@payload-config': ['./src/payload.config.ts'], // TODO: Account for srcDir
|
||||
'@payload-config': [`./${isSrcDir ? 'src/' : ''}payload.config.ts`],
|
||||
}
|
||||
await writeFile(tsConfigPath, stringify(userTsConfig, null, 2), { encoding: 'utf8' })
|
||||
}
|
||||
}
|
||||
|
||||
function installAndConfigurePayload(args: InitNextArgs & { nextAppDir: string }): InitNextResult {
|
||||
const { '--debug': debug, nextAppDir, nextConfigPath, projectDir, useDistFiles } = args
|
||||
function installAndConfigurePayload(
|
||||
args: InitNextArgs & { isSrcDir: boolean; nextAppDir: string },
|
||||
):
|
||||
| { payloadConfigPath: string; success: true }
|
||||
| { payloadConfigPath?: string; reason: string; success: false } {
|
||||
const { '--debug': debug, isSrcDir, nextAppDir, nextConfigPath, projectDir, useDistFiles } = args
|
||||
|
||||
info('Initializing Payload app in Next.js project', 1)
|
||||
|
||||
@@ -111,7 +132,10 @@ function installAndConfigurePayload(args: InitNextArgs & { nextAppDir: string })
|
||||
}
|
||||
|
||||
if (!fs.existsSync(projectDir)) {
|
||||
return { reason: `Could not find specified project directory at ${projectDir}`, success: false }
|
||||
return {
|
||||
reason: `Could not find specified project directory at ${projectDir}`,
|
||||
success: false,
|
||||
}
|
||||
}
|
||||
|
||||
const templateFilesPath =
|
||||
@@ -132,18 +156,26 @@ function installAndConfigurePayload(args: InitNextArgs & { nextAppDir: string })
|
||||
|
||||
logDebug(`Copying template files from ${templateFilesPath} to ${nextAppDir}`)
|
||||
|
||||
// TODO: Account for isSrcDir or not. Assuming src exists right now.
|
||||
const templateSrcDir = path.resolve(templateFilesPath, 'src')
|
||||
const templateSrcDir = path.resolve(templateFilesPath, isSrcDir ? '' : 'src')
|
||||
// const templateSrcDir = path.resolve(templateFilesPath)
|
||||
|
||||
logDebug(`templateSrcDir: ${templateSrcDir}`)
|
||||
logDebug(`nextAppDir: ${nextAppDir}`)
|
||||
logDebug(`projectDir: ${projectDir}`)
|
||||
logDebug(`nextConfigPath: ${nextConfigPath}`)
|
||||
|
||||
logDebug(
|
||||
`isSrcDir: ${isSrcDir}. source: ${templateSrcDir}. dest: ${path.dirname(nextConfigPath)}`,
|
||||
)
|
||||
|
||||
// This is a little clunky and needs to account for isSrcDir
|
||||
copyRecursiveSync(templateSrcDir, path.dirname(nextAppDir), debug)
|
||||
copyRecursiveSync(templateSrcDir, path.dirname(nextConfigPath), debug)
|
||||
|
||||
// Wrap next.config.js with withPayload
|
||||
wrapNextConfig({ nextConfigPath })
|
||||
|
||||
success('Successfully initialized.')
|
||||
return {
|
||||
nextAppDir,
|
||||
payloadConfigPath: path.resolve(nextAppDir, '../payload.config.ts'),
|
||||
success: true,
|
||||
}
|
||||
|
||||
@@ -47,10 +47,16 @@ describe('parseAndInsertWithPayload', () => {
|
||||
|
||||
// Unsupported: export { wrapped as default }
|
||||
it('should give warning with a named export as default', () => {
|
||||
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {})
|
||||
|
||||
const { modifiedConfigContent, success } = parseAndModifyConfigContent(
|
||||
nextConfigExportNamedDefault,
|
||||
)
|
||||
expect(modifiedConfigContent).toContain(withPayloadImportStatement)
|
||||
expect(success).toBe(false)
|
||||
|
||||
expect(consoleLogSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('Could not automatically wrap'),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,12 +7,8 @@ import fs from 'fs'
|
||||
import path from 'path'
|
||||
import shelljs from 'shelljs'
|
||||
import tempy from 'tempy'
|
||||
import { fileURLToPath } from 'url'
|
||||
import { promisify } from 'util'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
const readFile = promisify(fs.readFile)
|
||||
const writeFile = promisify(fs.writeFile)
|
||||
|
||||
@@ -29,13 +25,6 @@ describe('create-payload-app', () => {
|
||||
shelljs.exec('pnpm build:create-payload-app')
|
||||
})
|
||||
|
||||
describe('Next.js app template files', () => {
|
||||
it('should exist in dist', () => {
|
||||
const distPath = path.resolve(dirname, '../../packages/create-payload-app/dist/app/(payload)')
|
||||
expect(fs.existsSync(distPath)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe.each(Object.keys(nextCreateCommands))(`--init-next with %s`, (nextCmdKey) => {
|
||||
const projectDir = tempy.directory()
|
||||
beforeEach(async () => {
|
||||
@@ -74,19 +63,51 @@ describe('create-payload-app', () => {
|
||||
it('should install payload app in Next.js project', async () => {
|
||||
expect(fs.existsSync(projectDir)).toBe(true)
|
||||
|
||||
const result = await initNext({
|
||||
const firstResult = await initNext({
|
||||
'--debug': true,
|
||||
projectDir,
|
||||
nextConfigPath: path.resolve(projectDir, 'next.config.mjs'),
|
||||
useDistFiles: true, // create-payload-app/dist/app/(payload)
|
||||
packageManager: 'pnpm',
|
||||
})
|
||||
|
||||
expect(result.success).toBe(true)
|
||||
// Will fail because we detect top-level layout.tsx file
|
||||
expect(firstResult.success).toEqual(false)
|
||||
|
||||
const payloadFilesPath = path.resolve(result.userAppDir, '(payload)')
|
||||
// Move all files from app to top-level directory named `(app)`
|
||||
if (firstResult.success === false && 'nextAppDir' in firstResult) {
|
||||
fs.mkdirSync(path.resolve(firstResult.nextAppDir, '(app)'))
|
||||
fs.readdirSync(path.resolve(firstResult.nextAppDir)).forEach((file) => {
|
||||
if (file === '(app)') return
|
||||
fs.renameSync(
|
||||
path.resolve(firstResult.nextAppDir, file),
|
||||
path.resolve(firstResult.nextAppDir, '(app)', file),
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
// Rerun after moving files
|
||||
const result = await initNext({
|
||||
'--debug': true,
|
||||
projectDir,
|
||||
nextConfigPath: path.resolve(projectDir, 'next.config.mjs'),
|
||||
useDistFiles: true, // create-payload-app/dist/app/(payload)
|
||||
packageManager: 'pnpm',
|
||||
})
|
||||
|
||||
expect(result.success).toEqual(true)
|
||||
expect(result.nextAppDir).toEqual(
|
||||
path.resolve(projectDir, result.isSrcDir ? 'src/app' : 'app'),
|
||||
)
|
||||
|
||||
const payloadFilesPath = path.resolve(result.nextAppDir, '(payload)')
|
||||
// shelljs.exec(`tree ${projectDir}`)
|
||||
expect(fs.existsSync(payloadFilesPath)).toBe(true)
|
||||
|
||||
const payloadConfig = path.resolve(projectDir, 'payload.config.ts')
|
||||
const payloadConfig = path.resolve(
|
||||
projectDir,
|
||||
result.isSrcDir ? 'src/payload.config.ts' : 'payload.config.ts',
|
||||
)
|
||||
expect(fs.existsSync(payloadConfig)).toBe(true)
|
||||
|
||||
const tsConfigPath = path.resolve(projectDir, 'tsconfig.json')
|
||||
@@ -95,7 +116,7 @@ describe('create-payload-app', () => {
|
||||
compilerOptions?: CompilerOptions
|
||||
}
|
||||
expect(userTsConfig.compilerOptions.paths?.['@payload-config']).toStrictEqual([
|
||||
'./payload.config.ts',
|
||||
`./${result.isSrcDir ? 'src/' : ''}payload.config.ts`,
|
||||
])
|
||||
|
||||
// TODO: Start the Next.js app and check if it runs
|
||||
|
||||
Reference in New Issue
Block a user