From 7619fb4753b071e7c58f0faf2e29f4985969dd9e Mon Sep 17 00:00:00 2001 From: Elliot DeNolf Date: Fri, 29 Mar 2024 16:45:45 -0400 Subject: [PATCH] feat(cpa): handle next.js app with and without src dir --- .../src/lib/create-project.spec.ts | 14 ++-- .../create-payload-app/src/lib/init-next.ts | 64 ++++++++++++++----- .../src/lib/wrap-next-config.spec.ts | 6 ++ test/create-payload-app/int.spec.ts | 53 ++++++++++----- 4 files changed, 101 insertions(+), 36 deletions(-) diff --git a/packages/create-payload-app/src/lib/create-project.spec.ts b/packages/create-payload-app/src/lib/create-project.spec.ts index 0ed41dfcd..af5b84029 100644 --- a/packages/create-payload-app/src/lib/create-project.spec.ts +++ b/packages/create-payload-app/src/lib/create-project.spec.ts @@ -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 diff --git a/packages/create-payload-app/src/lib/init-next.ts b/packages/create-payload-app/src/lib/init-next.ts index 7bf310c01..20edfed65 100644 --- a/packages/create-payload-app/src/lib/init-next.ts +++ b/packages/create-payload-app/src/lib/init-next.ts @@ -29,17 +29,21 @@ type InitNextArgs = Pick & { 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 { 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 { )?.[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 { 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, } diff --git a/packages/create-payload-app/src/lib/wrap-next-config.spec.ts b/packages/create-payload-app/src/lib/wrap-next-config.spec.ts index aac5208e6..bdccc93d7 100644 --- a/packages/create-payload-app/src/lib/wrap-next-config.spec.ts +++ b/packages/create-payload-app/src/lib/wrap-next-config.spec.ts @@ -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'), + ) }) }) diff --git a/test/create-payload-app/int.spec.ts b/test/create-payload-app/int.spec.ts index 27d7996a8..f9ae715a3 100644 --- a/test/create-payload-app/int.spec.ts +++ b/test/create-payload-app/int.spec.ts @@ -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