feat(cpa): update existing payload installation (#6193)
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`
This commit is contained in:
30
packages/create-payload-app/src/lib/get-package-manager.ts
Normal file
30
packages/create-payload-app/src/lib/get-package-manager.ts
Normal file
@@ -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
|
||||
}
|
||||
@@ -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'
|
||||
|
||||
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)
|
||||
|
||||
import { fileURLToPath } from 'node:url'
|
||||
|
||||
import type { CliArgs, DbType, 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 { wrapNextConfig } from './wrap-next-config.js'
|
||||
|
||||
type InitNextArgs = Pick<CliArgs, '--debug'> & {
|
||||
dbType: DbType
|
||||
nextAppDetails?: NextAppDetails
|
||||
@@ -32,8 +32,6 @@ type InitNextArgs = Pick<CliArgs, '--debug'> & {
|
||||
useDistFiles?: boolean
|
||||
}
|
||||
|
||||
type NextConfigType = 'cjs' | 'esm'
|
||||
|
||||
type InitNextResult =
|
||||
| {
|
||||
isSrcDir: boolean
|
||||
@@ -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 {
|
||||
@@ -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> {
|
||||
@@ -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,
|
||||
@@ -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,
|
||||
@@ -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'))
|
||||
@@ -297,7 +280,11 @@ 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'
|
||||
}
|
||||
@@ -305,7 +292,6 @@ async function getProjectType(projectDir: string, nextConfigPath: string): Promi
|
||||
return 'cjs'
|
||||
}
|
||||
|
||||
const packageObj = await fse.readJson(path.resolve(projectDir, 'package.json'))
|
||||
const packageJsonType = packageObj.type
|
||||
if (packageJsonType === 'module') {
|
||||
return 'esm'
|
||||
|
||||
50
packages/create-payload-app/src/lib/install-packages.ts
Normal file
50
packages/create-payload-app/src/lib/install-packages.ts
Normal file
@@ -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 }
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -2,21 +2,21 @@ import * as p from '@clack/prompts'
|
||||
import slugify from '@sindresorhus/slugify'
|
||||
import arg from 'arg'
|
||||
import chalk from 'chalk'
|
||||
// @ts-expect-error no types
|
||||
import { detect } from 'detect-package-manager'
|
||||
import figures from 'figures'
|
||||
import path from 'path'
|
||||
|
||||
import type { CliArgs, PackageManager } from './types.js'
|
||||
import type { CliArgs } from './types.js'
|
||||
|
||||
import { configurePayloadConfig } from './lib/configure-payload-config.js'
|
||||
import { createProject } from './lib/create-project.js'
|
||||
import { generateSecret } from './lib/generate-secret.js'
|
||||
import { getPackageManager } from './lib/get-package-manager.js'
|
||||
import { getNextAppDetails, initNext } from './lib/init-next.js'
|
||||
import { parseProjectName } from './lib/parse-project-name.js'
|
||||
import { parseTemplate } from './lib/parse-template.js'
|
||||
import { selectDb } from './lib/select-db.js'
|
||||
import { getValidTemplates, validateTemplate } from './lib/templates.js'
|
||||
import { updatePayloadInProject } from './lib/update-payload-in-project.js'
|
||||
import { writeEnvFile } from './lib/write-env-file.js'
|
||||
import { error, info } from './utils/log.js'
|
||||
import {
|
||||
@@ -85,7 +85,28 @@ export class Main {
|
||||
|
||||
// Detect if inside Next.js project
|
||||
const nextAppDetails = await getNextAppDetails(process.cwd())
|
||||
const { hasTopLevelLayout, nextAppDir, nextConfigPath } = nextAppDetails
|
||||
const { hasTopLevelLayout, isPayloadInstalled, nextAppDir, nextConfigPath } = nextAppDetails
|
||||
|
||||
// Upgrade Payload in existing project
|
||||
if (isPayloadInstalled && nextConfigPath) {
|
||||
p.log.warn(`Payload installation detected in current project.`)
|
||||
const shouldUpdate = await p.confirm({
|
||||
initialValue: false,
|
||||
message: chalk.bold(`Upgrade Payload in this project?`),
|
||||
})
|
||||
|
||||
if (!p.isCancel(shouldUpdate) || shouldUpdate) {
|
||||
const { message, success: updateSuccess } = await updatePayloadInProject(nextAppDetails)
|
||||
if (updateSuccess) {
|
||||
info(message)
|
||||
} else {
|
||||
error(message)
|
||||
}
|
||||
}
|
||||
|
||||
p.outro(feedbackOutro())
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
if (nextConfigPath) {
|
||||
this.args['--name'] = slugify(path.basename(path.dirname(nextConfigPath)))
|
||||
@@ -96,7 +117,7 @@ export class Main {
|
||||
? path.dirname(nextConfigPath)
|
||||
: path.resolve(process.cwd(), slugify(projectName))
|
||||
|
||||
const packageManager = await getPackageManager(this.args, projectDir)
|
||||
const packageManager = await getPackageManager({ cliArgs: this.args, projectDir })
|
||||
|
||||
if (nextConfigPath) {
|
||||
p.log.step(
|
||||
@@ -212,19 +233,3 @@ export class Main {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function getPackageManager(args: CliArgs, projectDir: string): Promise<PackageManager> {
|
||||
let packageManager: PackageManager = 'npm'
|
||||
|
||||
if (args['--use-npm']) {
|
||||
packageManager = 'npm'
|
||||
} else if (args['--use-yarn']) {
|
||||
packageManager = 'yarn'
|
||||
} else if (args['--use-pnpm']) {
|
||||
packageManager = 'pnpm'
|
||||
} else {
|
||||
const detected = await detect({ cwd: projectDir })
|
||||
packageManager = detected || 'npm'
|
||||
}
|
||||
return packageManager
|
||||
}
|
||||
|
||||
@@ -65,3 +65,14 @@ export type DbDetails = {
|
||||
}
|
||||
|
||||
export type EditorType = 'lexical' | 'slate'
|
||||
|
||||
export type NextAppDetails = {
|
||||
hasTopLevelLayout: boolean
|
||||
isPayloadInstalled?: boolean
|
||||
isSrcDir: boolean
|
||||
nextAppDir?: string
|
||||
nextConfigPath?: string
|
||||
nextConfigType?: NextConfigType
|
||||
}
|
||||
|
||||
export type NextConfigType = 'cjs' | 'esm'
|
||||
|
||||
Reference in New Issue
Block a user