feat(cpa): handle next.js app with and without src dir

This commit is contained in:
Elliot DeNolf
2024-03-29 16:45:45 -04:00
parent 403a86feca
commit 7619fb4753
4 changed files with 101 additions and 36 deletions

View File

@@ -5,6 +5,7 @@ import { createProject } from './create-project.js'
import { fileURLToPath } from 'node:url' import { fileURLToPath } from 'node:url'
import { dbReplacements } from './packages.js' import { dbReplacements } from './packages.js'
import { getValidTemplates } from './templates.js' import { getValidTemplates } from './templates.js'
import globby from 'globby'
const filename = fileURLToPath(import.meta.url) const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename) const dirname = path.dirname(filename)
@@ -104,12 +105,17 @@ describe('createProject', () => {
Object.keys(packageJson.dependencies).filter((n) => n.startsWith('@payloadcms/db-')), Object.keys(packageJson.dependencies).filter((n) => n.startsWith('@payloadcms/db-')),
).toHaveLength(1) ).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 (!payloadConfigPath) {
if (!fse.existsSync(payloadConfigPath)) { throw new Error(`Could not find payload.config.ts inside ${projectDir}`)
payloadConfigPath = path.resolve(projectDir, 'src/payload/payload.config.ts')
} }
const content = fse.readFileSync(payloadConfigPath, 'utf-8') const content = fse.readFileSync(payloadConfigPath, 'utf-8')
// Check payload.config.ts // Check payload.config.ts

View File

@@ -29,17 +29,21 @@ type InitNextArgs = Pick<CliArgs, '--debug'> & {
projectDir: string projectDir: string
useDistFiles?: boolean useDistFiles?: boolean
} }
type InitNextResult = type InitNextResult =
| { | {
isSrcDir: boolean
nextAppDir: string nextAppDir: string
payloadConfigPath: string payloadConfigPath: string
success: true success: true
} }
| { reason: string; success: false } | { isSrcDir: boolean; nextAppDir?: string; reason: string; success: false }
export async function initNext(args: InitNextArgs): Promise<InitNextResult> { export async function initNext(args: InitNextArgs): Promise<InitNextResult> {
const { packageManager, projectDir } = args const { packageManager, projectDir } = args
const isSrcDir = fs.existsSync(path.resolve(projectDir, 'src'))
// Get app directory. Could be top-level or src/app // Get app directory. Could be top-level or src/app
const nextAppDir = ( const nextAppDir = (
await globby(['**/app'], { await globby(['**/app'], {
@@ -50,7 +54,7 @@ export async function initNext(args: InitNextArgs): Promise<InitNextResult> {
)?.[0] )?.[0]
if (!nextAppDir) { 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 // Check for top-level layout.tsx
@@ -58,29 +62,42 @@ export async function initNext(args: InitNextArgs): Promise<InitNextResult> {
if (fs.existsSync(layoutPath)) { if (fs.existsSync(layoutPath)) {
// Output directions for user to move all files from app to top-level directory named `(app)` // Output directions for user to move all files from app to top-level directory named `(app)`
log(moveMessage({ nextAppDir, projectDir })) 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({ const configurationResult = installAndConfigurePayload({
...args, ...args,
isSrcDir,
nextAppDir, nextAppDir,
useDistFiles: true, // Requires running 'pnpm pack-template-files' in cpa 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) const { success: installSuccess } = await installDeps(projectDir, packageManager)
if (!installSuccess) { 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` // 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 tsConfigPath = path.resolve(projectDir, 'tsconfig.json')
const userTsConfigContent = await readFile(tsConfigPath, { const userTsConfigContent = await readFile(tsConfigPath, {
encoding: 'utf8', encoding: 'utf8',
@@ -95,14 +112,18 @@ async function addPayloadConfigToTsConfig(projectDir: string) {
if (!userTsConfig.compilerOptions.paths?.['@payload-config']) { if (!userTsConfig.compilerOptions.paths?.['@payload-config']) {
userTsConfig.compilerOptions.paths = { userTsConfig.compilerOptions.paths = {
...(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' }) await writeFile(tsConfigPath, stringify(userTsConfig, null, 2), { encoding: 'utf8' })
} }
} }
function installAndConfigurePayload(args: InitNextArgs & { nextAppDir: string }): InitNextResult { function installAndConfigurePayload(
const { '--debug': debug, nextAppDir, nextConfigPath, projectDir, useDistFiles } = args 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) info('Initializing Payload app in Next.js project', 1)
@@ -111,7 +132,10 @@ function installAndConfigurePayload(args: InitNextArgs & { nextAppDir: string })
} }
if (!fs.existsSync(projectDir)) { 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 = const templateFilesPath =
@@ -132,18 +156,26 @@ function installAndConfigurePayload(args: InitNextArgs & { nextAppDir: string })
logDebug(`Copying template files from ${templateFilesPath} to ${nextAppDir}`) logDebug(`Copying template files from ${templateFilesPath} to ${nextAppDir}`)
// TODO: Account for isSrcDir or not. Assuming src exists right now. const templateSrcDir = path.resolve(templateFilesPath, isSrcDir ? '' : 'src')
const templateSrcDir = path.resolve(templateFilesPath, '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 // 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 // Wrap next.config.js with withPayload
wrapNextConfig({ nextConfigPath }) wrapNextConfig({ nextConfigPath })
success('Successfully initialized.') success('Successfully initialized.')
return { return {
nextAppDir,
payloadConfigPath: path.resolve(nextAppDir, '../payload.config.ts'), payloadConfigPath: path.resolve(nextAppDir, '../payload.config.ts'),
success: true, success: true,
} }

View File

@@ -47,10 +47,16 @@ 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', () => {
const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation(() => {})
const { modifiedConfigContent, success } = parseAndModifyConfigContent( const { modifiedConfigContent, success } = parseAndModifyConfigContent(
nextConfigExportNamedDefault, nextConfigExportNamedDefault,
) )
expect(modifiedConfigContent).toContain(withPayloadImportStatement) expect(modifiedConfigContent).toContain(withPayloadImportStatement)
expect(success).toBe(false) expect(success).toBe(false)
expect(consoleLogSpy).toHaveBeenCalledWith(
expect.stringContaining('Could not automatically wrap'),
)
}) })
}) })

View File

@@ -7,12 +7,8 @@ import fs from 'fs'
import path from 'path' import path from 'path'
import shelljs from 'shelljs' import shelljs from 'shelljs'
import tempy from 'tempy' import tempy from 'tempy'
import { fileURLToPath } from 'url'
import { promisify } from 'util' import { promisify } from 'util'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
const readFile = promisify(fs.readFile) const readFile = promisify(fs.readFile)
const writeFile = promisify(fs.writeFile) const writeFile = promisify(fs.writeFile)
@@ -29,13 +25,6 @@ describe('create-payload-app', () => {
shelljs.exec('pnpm build: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) => { describe.each(Object.keys(nextCreateCommands))(`--init-next with %s`, (nextCmdKey) => {
const projectDir = tempy.directory() const projectDir = tempy.directory()
beforeEach(async () => { beforeEach(async () => {
@@ -74,19 +63,51 @@ describe('create-payload-app', () => {
it('should install payload app in Next.js project', async () => { it('should install payload app in Next.js project', async () => {
expect(fs.existsSync(projectDir)).toBe(true) expect(fs.existsSync(projectDir)).toBe(true)
const result = await initNext({ const firstResult = await initNext({
'--debug': true, '--debug': true,
projectDir, projectDir,
nextConfigPath: path.resolve(projectDir, 'next.config.mjs'),
useDistFiles: true, // create-payload-app/dist/app/(payload) useDistFiles: true, // create-payload-app/dist/app/(payload)
packageManager: 'pnpm', 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) 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) expect(fs.existsSync(payloadConfig)).toBe(true)
const tsConfigPath = path.resolve(projectDir, 'tsconfig.json') const tsConfigPath = path.resolve(projectDir, 'tsconfig.json')
@@ -95,7 +116,7 @@ describe('create-payload-app', () => {
compilerOptions?: CompilerOptions compilerOptions?: CompilerOptions
} }
expect(userTsConfig.compilerOptions.paths?.['@payload-config']).toStrictEqual([ 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 // TODO: Start the Next.js app and check if it runs