Skip to content

Commit

Permalink
feat(cpa): update existing payload installation (#6193)
Browse files Browse the repository at this point in the history
Updates create-payload-app to update an existing payload installation

- Detects existing Payload installation. Fixes #6517 
- If not latest, will install latest and grab the `(payload)` directory
structure (ripped from `templates/blank-3.0`
  • Loading branch information
denolfe authored May 28, 2024
1 parent ea48ca3 commit 10c94b3
Show file tree
Hide file tree
Showing 6 changed files with 237 additions and 66 deletions.
30 changes: 30 additions & 0 deletions packages/create-payload-app/src/lib/get-package-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// @ts-expect-error no types
import { detect } from 'detect-package-manager'

import type { CliArgs, PackageManager } from '../types.js'

export async function getPackageManager(args: {
cliArgs?: CliArgs
projectDir: string
}): Promise<PackageManager> {
const { cliArgs, projectDir } = args

if (!cliArgs) {
const detected = await detect({ cwd: projectDir })
return detected || 'npm'
}

let packageManager: PackageManager = 'npm'

if (cliArgs['--use-npm']) {
packageManager = 'npm'
} else if (cliArgs['--use-yarn']) {
packageManager = 'yarn'
} else if (cliArgs['--use-pnpm']) {
packageManager = 'pnpm'
} else {
const detected = await detect({ cwd: projectDir })
packageManager = detected || 'npm'
}
return packageManager
}
76 changes: 31 additions & 45 deletions packages/create-payload-app/src/lib/init-next.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,24 +6,24 @@ import execa from 'execa'
import fs from 'fs'
import fse from 'fs-extra'
import globby from 'globby'
import { fileURLToPath } from 'node:url'
import path from 'path'
import { promisify } from 'util'

const readFile = promisify(fs.readFile)
const writeFile = promisify(fs.writeFile)

const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)

import { fileURLToPath } from 'node:url'

import type { CliArgs, DbType, PackageManager } from '../types.js'
import type { CliArgs, DbType, NextAppDetails, NextConfigType, PackageManager } from '../types.js'

import { copyRecursiveSync } from '../utils/copy-recursive-sync.js'
import { debug as origDebug, warning } from '../utils/log.js'
import { moveMessage } from '../utils/messages.js'
import { installPackages } from './install-packages.js'
import { wrapNextConfig } from './wrap-next-config.js'

const readFile = promisify(fs.readFile)
const writeFile = promisify(fs.writeFile)

const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)

type InitNextArgs = Pick<CliArgs, '--debug'> & {
dbType: DbType
nextAppDetails?: NextAppDetails
Expand All @@ -32,8 +32,6 @@ type InitNextArgs = Pick<CliArgs, '--debug'> & {
useDistFiles?: boolean
}

type NextConfigType = 'cjs' | 'esm'

type InitNextResult =
| {
isSrcDir: boolean
Expand All @@ -55,7 +53,8 @@ export async function initNext(args: InitNextArgs): Promise<InitNextResult> {
nextAppDetails.nextAppDir = createdAppDir
}

const { hasTopLevelLayout, isSrcDir, nextAppDir, nextConfigType } = nextAppDetails
const { hasTopLevelLayout, isPayloadInstalled, isSrcDir, nextAppDir, nextConfigType } =
nextAppDetails

if (!nextConfigType) {
return {
Expand Down Expand Up @@ -228,37 +227,10 @@ async function installDeps(projectDir: string, packageManager: PackageManager, d

packagesToInstall.push(`@payloadcms/db-${dbType}@beta`)

let exitCode = 0
switch (packageManager) {
case 'npm': {
;({ exitCode } = await execa('npm', ['install', '--save', ...packagesToInstall], {
cwd: projectDir,
}))
break
}
case 'yarn':
case 'pnpm': {
;({ exitCode } = await execa(packageManager, ['add', ...packagesToInstall], {
cwd: projectDir,
}))
break
}
case 'bun': {
warning('Bun support is untested.')
;({ exitCode } = await execa('bun', ['add', ...packagesToInstall], { cwd: projectDir }))
break
}
}
// Match graphql version of @payloadcms/next
packagesToInstall.push('graphql@^16.8.1')

return { success: exitCode === 0 }
}

type NextAppDetails = {
hasTopLevelLayout: boolean
isSrcDir: boolean
nextAppDir?: string
nextConfigPath?: string
nextConfigType?: NextConfigType
return await installPackages({ packageManager, packagesToInstall, projectDir })
}

export async function getNextAppDetails(projectDir: string): Promise<NextAppDetails> {
Expand All @@ -267,6 +239,7 @@ export async function getNextAppDetails(projectDir: string): Promise<NextAppDeta
const nextConfigPath: string | undefined = (
await globby('next.config.*js', { absolute: true, cwd: projectDir })
)?.[0]

if (!nextConfigPath || nextConfigPath.length === 0) {
return {
hasTopLevelLayout: false,
Expand All @@ -275,6 +248,16 @@ export async function getNextAppDetails(projectDir: string): Promise<NextAppDeta
}
}

const packageObj = await fse.readJson(path.resolve(projectDir, 'package.json'))
if (packageObj.dependencies?.payload) {
return {
hasTopLevelLayout: false,
isPayloadInstalled: true,
isSrcDir,
nextConfigPath,
}
}

let nextAppDir: string | undefined = (
await globby(['**/app'], {
absolute: true,
Expand All @@ -288,7 +271,7 @@ export async function getNextAppDetails(projectDir: string): Promise<NextAppDeta
nextAppDir = undefined
}

const configType = await getProjectType(projectDir, nextConfigPath)
const configType = getProjectType({ nextConfigPath, packageObj })

const hasTopLevelLayout = nextAppDir
? fs.existsSync(path.resolve(nextAppDir, 'layout.tsx'))
Expand All @@ -297,15 +280,18 @@ export async function getNextAppDetails(projectDir: string): Promise<NextAppDeta
return { hasTopLevelLayout, isSrcDir, nextAppDir, nextConfigPath, nextConfigType: configType }
}

async function getProjectType(projectDir: string, nextConfigPath: string): Promise<'cjs' | 'esm'> {
function getProjectType(args: {
nextConfigPath: string
packageObj: Record<string, unknown>
}): 'cjs' | 'esm' {
const { nextConfigPath, packageObj } = args
if (nextConfigPath.endsWith('.mjs')) {
return 'esm'
}
if (nextConfigPath.endsWith('.cjs')) {
return 'cjs'
}

const packageObj = await fse.readJson(path.resolve(projectDir, 'package.json'))
const packageJsonType = packageObj.type
if (packageJsonType === 'module') {
return 'esm'
Expand Down
50 changes: 50 additions & 0 deletions packages/create-payload-app/src/lib/install-packages.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import execa from 'execa'

import type { PackageManager } from '../types.js'

import { error, warning } from '../utils/log.js'

export async function installPackages(args: {
packageManager: PackageManager
packagesToInstall: string[]
projectDir: string
}) {
const { packageManager, packagesToInstall, projectDir } = args

let exitCode = 0
let stdout = ''
let stderr = ''

switch (packageManager) {
case 'npm': {
;({ exitCode, stderr, stdout } = await execa(
'npm',
['install', '--save', ...packagesToInstall],
{
cwd: projectDir,
},
))
break
}
case 'yarn':
case 'pnpm': {
;({ exitCode, stderr, stdout } = await execa(packageManager, ['add', ...packagesToInstall], {
cwd: projectDir,
}))
break
}
case 'bun': {
warning('Bun support is untested.')
;({ exitCode, stderr, stdout } = await execa('bun', ['add', ...packagesToInstall], {
cwd: projectDir,
}))
break
}
}

if (exitCode !== 0) {
error(`Unable to install packages. Error: ${stderr}`)
}

return { success: exitCode === 0 }
}
89 changes: 89 additions & 0 deletions packages/create-payload-app/src/lib/update-payload-in-project.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import * as p from '@clack/prompts'
import execa from 'execa'
import fse from 'fs-extra'
import { fileURLToPath } from 'node:url'
import path from 'path'

const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)

import type { NextAppDetails } from '../types.js'

import { copyRecursiveSync } from '../utils/copy-recursive-sync.js'
import { info } from '../utils/log.js'
import { getPackageManager } from './get-package-manager.js'
import { installPackages } from './install-packages.js'

export async function updatePayloadInProject(
appDetails: NextAppDetails,
): Promise<{ message: string; success: boolean }> {
if (!appDetails.nextConfigPath) return { message: 'No Next.js config found', success: false }

const projectDir = path.dirname(appDetails.nextConfigPath)

const packageObj = (await fse.readJson(path.resolve(projectDir, 'package.json'))) as {
dependencies?: Record<string, string>
}
if (!packageObj?.dependencies) {
throw new Error('No package.json found in this project')
}

const payloadVersion = packageObj.dependencies?.payload
if (!payloadVersion) {
throw new Error('Payload is not installed in this project')
}

const packageManager = await getPackageManager({ projectDir })

// Fetch latest Payload version from npm
const { exitCode: getLatestVersionExitCode, stdout: latestPayloadVersion } = await execa('npm', [
'show',
'payload@beta',
'version',
])
if (getLatestVersionExitCode !== 0) {
throw new Error('Failed to fetch latest Payload version')
}

if (payloadVersion === latestPayloadVersion) {
return { message: `Payload v${payloadVersion} is already up to date.`, success: true }
}

// Update all existing Payload packages
const payloadPackages = Object.keys(packageObj.dependencies).filter((dep) =>
dep.startsWith('@payloadcms/'),
)

const packageNames = ['payload', ...payloadPackages]

const packagesToUpdate = packageNames.map((pkg) => `${pkg}@${latestPayloadVersion}`)

info(
`Updating ${packagesToUpdate.length} Payload packages to v${latestPayloadVersion}...\n\n${packageNames.map((p) => ` - ${p}`).join('\n')}`,
)

const { success: updateSuccess } = await installPackages({
packageManager,
packagesToInstall: packagesToUpdate,
projectDir,
})

if (!updateSuccess) {
throw new Error('Failed to update Payload packages')
}
info('Payload packages updated successfully.')

info(`Updating Payload Next.js files...`)
const templateFilesPath = dirname.endsWith('dist')
? path.resolve(dirname, '../..', 'dist/template')
: path.resolve(dirname, '../../../../templates/blank-3.0')

const templateSrcDir = path.resolve(templateFilesPath, 'src/app/(payload)')

copyRecursiveSync(
templateSrcDir,
path.resolve(projectDir, appDetails.isSrcDir ? 'src/app' : 'app', '(payload)'),
)

return { message: 'Payload updated successfully.', success: true }
}
Loading

0 comments on commit 10c94b3

Please sign in to comment.