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 { 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
|
||||||
|
|||||||
@@ -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,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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'),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user