Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cpa): update existing payload installation #6193

Merged
merged 3 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 }
}
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 }
}