Compare commits

...

16 Commits

Author SHA1 Message Date
Elliot DeNolf
2497f2a65a 0.5.0-beta.3 2023-10-04 13:41:39 -04:00
Elliot DeNolf
90948146c6 chore: rename editors 2023-10-04 13:41:07 -04:00
Elliot DeNolf
e6a5132812 0.5.0-beta.2 2023-10-01 15:44:40 -04:00
Elliot DeNolf
53310281d0 feat: add editor import and replacement 2023-10-01 15:44:26 -04:00
Elliot DeNolf
3ac9dae1d2 0.5.0-beta.1 2023-09-29 12:13:02 -04:00
Elliot DeNolf
9c165a73cb chore: proper postgres adapter import replacement 2023-09-29 12:12:49 -04:00
Elliot DeNolf
9b58915aff 0.5.0-beta.0 2023-09-29 12:02:05 -04:00
Elliot DeNolf
c1daeb3432 feat: bump template branch to 2.0 2023-09-29 12:02:01 -04:00
Elliot DeNolf
ff8acde322 chore: check DATABASE_URI key 2023-09-19 15:53:06 -04:00
Elliot DeNolf
1bade389e4 test: reorganize tests 2023-09-19 15:42:04 -04:00
Elliot DeNolf
489de652a3 chore(templates): update branch on starter urls temporarily 2023-09-19 15:38:31 -04:00
Elliot DeNolf
166b06e5d8 chore: replace DATABASE_URI env value 2023-09-19 15:11:27 -04:00
Elliot DeNolf
ed143c7a67 test: add debug for cli 2023-09-19 14:58:24 -04:00
Elliot DeNolf
b29e2ae685 test: dependency and config replacement tests 2023-09-19 14:58:10 -04:00
Elliot DeNolf
9bafc0fcbf feat: update templates with bundler and db adapter 2023-09-19 14:23:47 -04:00
Elliot DeNolf
b3db078a5f feat: implement db selection 2023-09-18 12:13:50 -04:00
14 changed files with 450 additions and 78 deletions

13
.vscode/launch.json vendored Normal file
View File

@@ -0,0 +1,13 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
"configurations": [
{
"command": "ts-node -T ./src/index.ts -n asdf -t blank --db mongodb --no-deps",
"cwd": "${workspaceFolder}",
"name": "Debug",
"request": "launch",
"type": "node-terminal"
},
]
}

View File

@@ -9,15 +9,26 @@ CLI for easily starting new Payload project
USAGE
$ npx create-payload-app
$ npx create-payload-app my-project
$ npx create-payload-app -n my-project -t blog
OPTIONS
--name my-payload-app Set project name
--template template_name Choose specific template
-n my-payload-app Set project name
-t template_name Choose specific template
Available templates: blank, blog, todo
Available templates:
--use-npm Use npm to install dependencies
--no-deps Do not install any dependencies
--help Show help
blank Blank Template
website Website Template
ecommerce E-commerce Template
plugin Template for creating a Payload plugin
payload-demo Payload demo site at https://demo.payloadcms.com
payload-website Payload website CMS at https://payloadcms.com
--use-npm Use npm to install dependencies
--use-yarn Use yarn to install dependencies
--use-pnpm Use pnpm to install dependencies
--no-deps Do not install any dependencies
-h Show help
```

View File

@@ -37,7 +37,7 @@
"prompts": "^2.4.2",
"terminal-link": "^2.1.1"
},
"version": "0.4.2",
"version": "0.5.0-beta.3",
"devDependencies": {
"@types/command-exists": "^1.2.0",
"@types/degit": "^2.8.3",

View File

@@ -0,0 +1,107 @@
import fse from 'fs-extra'
import path from 'path'
import type { DbDetails } from '../types'
import { warning } from '../utils/log'
import { bundlerPackages, dbPackages, editorPackages } from './packages'
/** Update payload config with necessary imports and adapters */
export async function configurePayloadConfig(args: {
projectDir: string
dbDetails: DbDetails | undefined
}): Promise<void> {
if (!args.dbDetails) {
return
}
// Update package.json
const packageJsonPath = path.resolve(args.projectDir, 'package.json')
try {
const packageObj = await fse.readJson(packageJsonPath)
const dbPackage = dbPackages[args.dbDetails.type]
const bundlerPackage = bundlerPackages['webpack']
const editorPackage = editorPackages['lexical']
packageObj.dependencies[dbPackage.packageName] = 'latest'
packageObj.dependencies[bundlerPackage.packageName] = 'latest'
packageObj.dependencies[editorPackage.packageName] = 'latest'
await fse.writeJson(packageJsonPath, packageObj, { spaces: 2 })
} catch (err: unknown) {
warning('Unable to update name in package.json')
}
try {
const possiblePaths = [
path.resolve(args.projectDir, 'src/payload.config.ts'),
path.resolve(args.projectDir, 'src/payload/payload.config.ts'),
]
let payloadConfigPath: string | undefined
possiblePaths.forEach(p => {
if (fse.pathExistsSync(p) && !payloadConfigPath) {
payloadConfigPath = p
}
})
if (!payloadConfigPath) {
warning('Unable to update payload.config.ts with plugins')
return
}
const configContent = fse.readFileSync(payloadConfigPath, 'utf-8')
const configLines = configContent.split('\n')
const dbReplacement = dbPackages[args.dbDetails.type]
const bundlerReplacement = bundlerPackages['webpack']
const editorReplacement = editorPackages['lexical']
let dbConfigStartLineIndex: number | undefined
let dbConfigEndLineIndex: number | undefined
configLines.forEach((l, i) => {
if (l.includes('// database-adapter-import')) {
configLines[i] = dbReplacement.importReplacement
}
if (l.includes('// bundler-import')) {
configLines[i] = bundlerReplacement.importReplacement
}
if (l.includes('// bundler-config')) {
configLines[i] = bundlerReplacement.configReplacement
}
if (l.includes('// editor-import')) {
configLines[i] = editorReplacement.importReplacement
}
if (l.includes('// editor-config')) {
configLines[i] = editorReplacement.configReplacement
}
if (l.includes('// database-adapter-config-start')) {
dbConfigStartLineIndex = i
}
if (l.includes('// database-adapter-config-end')) {
dbConfigEndLineIndex = i
}
})
if (!dbConfigStartLineIndex || !dbConfigEndLineIndex) {
warning('Unable to update payload.config.ts with database adapter import')
} else {
// Replaces lines between `// database-adapter-config-start` and `// database-adapter-config-end`
configLines.splice(
dbConfigStartLineIndex,
dbConfigEndLineIndex - dbConfigStartLineIndex + 1,
...dbReplacement.configReplacement,
)
}
fse.writeFileSync(payloadConfigPath, configLines.join('\n'))
} catch (err: unknown) {
warning('Unable to update payload.config.ts with plugins')
}
}

View File

@@ -1,7 +1,8 @@
import fse from 'fs-extra'
import path from 'path'
import type { CliArgs, ProjectTemplate } from '../types'
import type { BundlerType, CliArgs, DbType, ProjectTemplate } from '../types'
import { createProject } from './create-project'
import { bundlerPackages, dbPackages } from './packages'
const projectDir = path.resolve(__dirname, './tmp')
describe('createProject', () => {
@@ -22,7 +23,11 @@ describe('createProject', () => {
describe('#createProject', () => {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const args = { _: ['project-name'], '--no-deps': true } as CliArgs
const args = {
_: ['project-name'],
'--db': 'mongodb',
'--no-deps': true,
} as CliArgs
const packageManager = 'yarn'
it('creates starter project', async () => {
@@ -70,5 +75,60 @@ describe('createProject', () => {
// Check package name and description
expect(packageJson.name).toEqual(projectName)
})
describe('db adapters and bundlers', () => {
it.each([
['mongodb', 'webpack'],
['postgres', 'webpack'],
])('update config and deps: %s, %s', async (db, bundler) => {
const projectName = 'starter-project'
const template: ProjectTemplate = {
name: 'blank',
type: 'starter',
url: 'https://github.com/payloadcms/payload/templates/blank#2.0',
description: 'Blank Template',
}
await createProject({
cliArgs: args,
projectName,
projectDir,
template,
packageManager,
dbDetails: {
dbUri: `${db}://localhost:27017/create-project-test`,
type: db as DbType,
},
})
const dbReplacement = dbPackages[db as DbType]
const bundlerReplacement = bundlerPackages[bundler as BundlerType]
const packageJsonPath = path.resolve(projectDir, 'package.json')
const packageJson = fse.readJsonSync(packageJsonPath)
// Check deps
expect(packageJson.dependencies[dbReplacement.packageName]).toBeDefined()
expect(
packageJson.dependencies[bundlerReplacement.packageName],
).toBeDefined()
const payloadConfigPath = path.resolve(projectDir, 'src/payload.config.ts')
const content = fse.readFileSync(payloadConfigPath, 'utf-8')
// Check payload.config.ts
expect(content).not.toContain('// database-adapter-import')
expect(content).toContain(dbReplacement.importReplacement)
expect(content).not.toContain('// database-adapter-config-start')
expect(content).not.toContain('// database-adapter-config-end')
expect(content).toContain(dbReplacement.configReplacement.join('\n'))
expect(content).not.toContain('// bundler-config-import')
expect(content).toContain(bundlerReplacement.importReplacement)
expect(content).not.toContain('// bundler-config')
expect(content).toContain(bundlerReplacement.configReplacement)
})
})
})
})

View File

@@ -6,7 +6,8 @@ import ora from 'ora'
import degit from 'degit'
import { success, error, warning } from '../utils/log'
import type { CliArgs, ProjectTemplate } from '../types'
import type { CliArgs, DbDetails, PackageManager, ProjectTemplate } from '../types'
import { configurePayloadConfig } from './configure-payload-config'
async function createOrFindProjectDir(projectDir: string): Promise<void> {
const pathExists = await fse.pathExists(projectDir)
@@ -18,16 +19,22 @@ async function createOrFindProjectDir(projectDir: string): Promise<void> {
async function installDeps(args: {
cliArgs: CliArgs
projectDir: string
packageManager: string
packageManager: PackageManager
}): Promise<boolean> {
const { cliArgs, projectDir, packageManager } = args
if (cliArgs['--no-deps']) {
return true
}
const cmd = packageManager === 'yarn' ? 'yarn' : 'npm install --legacy-peer-deps'
let installCmd = 'npm install --legacy-peer-deps'
if (packageManager === 'yarn') {
installCmd = 'yarn'
} else if (packageManager === 'pnpm') {
installCmd = 'pnpm install'
}
try {
await execa.command(cmd, {
await execa.command(installCmd, {
cwd: path.resolve(projectDir),
})
return true
@@ -37,29 +44,16 @@ async function installDeps(args: {
}
}
export async function updatePackageJSONName(args: {
projectName: string
projectDir: string
}): Promise<void> {
const { projectName, projectDir } = args
const packageJsonPath = path.resolve(projectDir, 'package.json')
try {
const packageObj = await fse.readJson(packageJsonPath)
packageObj.name = projectName
await fse.writeJson(packageJsonPath, packageObj, { spaces: 2 })
} catch (err: unknown) {
warning('Unable to update name in package.json')
}
}
export async function createProject(args: {
cliArgs: CliArgs
projectName: string
projectDir: string
template: ProjectTemplate
packageManager: string
packageManager: PackageManager
dbDetails?: DbDetails
}): Promise<void> {
const { cliArgs, projectName, projectDir, template, packageManager } = args
const { cliArgs, projectName, projectDir, template, packageManager, dbDetails } =
args
await createOrFindProjectDir(projectDir)
@@ -72,7 +66,8 @@ export async function createProject(args: {
const spinner = ora('Checking latest Payload version...').start()
await updatePackageJSONName({ projectName, projectDir })
await updatePackageJSON({ projectName, projectDir })
await configurePayloadConfig({ projectDir, dbDetails })
// Remove yarn.lock file. This is only desired in Payload Cloud.
const lockPath = path.resolve(projectDir, 'yarn.lock')
@@ -90,3 +85,18 @@ export async function createProject(args: {
error('Error installing dependencies')
}
}
export async function updatePackageJSON(args: {
projectName: string
projectDir: string
}): Promise<void> {
const { projectName, projectDir } = args
const packageJsonPath = path.resolve(projectDir, 'package.json')
try {
const packageObj = await fse.readJson(packageJsonPath)
packageObj.name = projectName
await fse.writeJson(packageJsonPath, packageObj, { spaces: 2 })
} catch (err: unknown) {
warning('Unable to update name in package.json')
}
}

View File

@@ -1,35 +0,0 @@
import prompts from 'prompts'
import slugify from '@sindresorhus/slugify'
import type { CliArgs } from '../types'
export async function getDatabaseConnection(
args: CliArgs,
projectName: string,
): Promise<string> {
if (args['--db']) return args['--db']
const response = await prompts(
{
type: 'text',
name: 'value',
message: 'Enter MongoDB connection',
initial: `mongodb://127.0.0.1/${
projectName === '.'
? `payload-${getRandomDigitSuffix()}`
: slugify(projectName)
}`,
validate: (value: string) => !!value.length,
},
{
onCancel: () => {
process.exit(0)
},
},
)
return response.value
}
function getRandomDigitSuffix(): string {
return (Math.random() * Math.pow(10, 6)).toFixed(0)
}

79
src/lib/packages.ts Normal file
View File

@@ -0,0 +1,79 @@
import type { BundlerType, DbType, EditorType } from '../types'
type DbAdapterReplacement = {
packageName: string
importReplacement: string
configReplacement: string[]
}
type BundlerReplacement = {
packageName: string
importReplacement: string
configReplacement: string
}
type EditorReplacement = {
packageName: string
importReplacement: string
configReplacement: string
}
const mongodbReplacement: DbAdapterReplacement = {
packageName: '@payloadcms/db-mongodb',
importReplacement: "import { mongooseAdapter } from '@payloadcms/db-mongodb'",
// Replacement between `// database-adapter-config-start` and `// database-adapter-config-end`
configReplacement: [
' db: mongooseAdapter({',
' url: process.env.DATABASE_URI,',
' }),',
],
}
const postgresReplacement: DbAdapterReplacement = {
packageName: '@payloadcms/db-postgres',
importReplacement: "import { postgresAdapter } from '@payloadcms/db-postgres'",
configReplacement: [
' db: postgresAdapter({',
' client: {',
' connectionString: process.env.DATABASE_URI,',
' },',
' }),',
],
}
export const dbPackages: Record<DbType, DbAdapterReplacement> = {
mongodb: mongodbReplacement,
postgres: postgresReplacement,
}
const webpackReplacement: BundlerReplacement = {
packageName: '@payloadcms/bundler-webpack',
importReplacement: "import { webpackBundler } from '@payloadcms/bundler-webpack'",
// Replacement of line containing `// bundler-config`
configReplacement: ' bundler: webpackBundler(),',
}
const viteReplacement: BundlerReplacement = {
packageName: '@payloadcms/bundler-vite',
importReplacement: "import { viteBundler } from '@payloadcms/bundler-vite'",
configReplacement: ' bundler: viteBundler(),',
}
export const bundlerPackages: Record<BundlerType, BundlerReplacement> = {
webpack: webpackReplacement,
vite: viteReplacement,
}
export const editorPackages: Record<EditorType, EditorReplacement> = {
slate: {
packageName: '@payloadcms/richtext-slate',
importReplacement: "import { slateEditor } from '@payloadcms/richtext-slate'",
configReplacement: ' editor: slateEditor({}),',
},
lexical: {
packageName: '@payloadcms/richtext-lexical',
importReplacement:
"import { lexicalEditor } from '@payloadcms/richtext-lexical'",
configReplacement: ' editor: lexicalEditor({}),',
},
}

96
src/lib/select-db.ts Normal file
View File

@@ -0,0 +1,96 @@
import prompts from 'prompts'
import slugify from '@sindresorhus/slugify'
import type { CliArgs, DbDetails, DbType } from '../types'
type DbChoice = {
value: DbType
title: string
dbConnectionPrefix: `${string}/`
}
const dbChoiceRecord: Record<DbType, DbChoice> = {
mongodb: {
value: 'mongodb',
title: 'MongoDB',
dbConnectionPrefix: 'mongodb://127.0.0.1/',
},
postgres: {
value: 'postgres',
title: 'PostgreSQL',
dbConnectionPrefix: 'postgres://127.0.0.1:5432/',
},
}
export async function selectDb(
args: CliArgs,
projectName: string,
): Promise<DbDetails> {
let dbType: DbType | undefined = undefined
if (args['--db']) {
if (
!Object.values(dbChoiceRecord).some(
dbChoice => dbChoice.value === args['--db'],
)
) {
throw new Error(
`Invalid database type given. Valid types are: ${Object.values(
dbChoiceRecord,
)
.map(dbChoice => dbChoice.value)
.join(', ')}`,
)
}
dbType = args['--db'] as DbType
} else {
const dbTypeRes = await prompts(
{
type: 'select',
name: 'value',
message: 'Select a database',
choices: Object.values(dbChoiceRecord).map(dbChoice => {
return {
title: dbChoice.title,
value: dbChoice.value,
}
}),
validate: (value: string) => !!value.length,
},
{
onCancel: () => {
process.exit(0)
},
},
)
dbType = dbTypeRes.value
}
const dbChoice = dbChoiceRecord[dbType as DbType]
const dbUriRes = await prompts(
{
type: 'text',
name: 'value',
message: `Enter ${dbChoice.title} connection string`,
initial: `${dbChoice.dbConnectionPrefix}${
projectName === '.'
? `payload-${getRandomDigitSuffix()}`
: slugify(projectName)
}`,
validate: (value: string) => !!value.length,
},
{
onCancel: () => {
process.exit(0)
},
},
)
return {
type: dbChoice.value,
dbUri: dbUriRes.value,
}
}
function getRandomDigitSuffix(): string {
return (Math.random() * Math.pow(10, 6)).toFixed(0)
}

View File

@@ -16,19 +16,19 @@ export async function getValidTemplates(): Promise<ProjectTemplate[]> {
{
name: 'blank',
type: 'starter',
url: 'https://github.com/payloadcms/payload/templates/blank',
url: 'https://github.com/payloadcms/payload/templates/blank#2.0',
description: 'Blank Template',
},
{
name: 'website',
type: 'starter',
url: 'https://github.com/payloadcms/payload/templates/website',
url: 'https://github.com/payloadcms/payload/templates/website#2.0',
description: 'Website Template',
},
{
name: 'ecommerce',
type: 'starter',
url: 'https://github.com/payloadcms/payload/templates/ecommerce',
url: 'https://github.com/payloadcms/payload/templates/ecommerce#2.0',
description: 'E-commerce Template',
},
{

View File

@@ -31,7 +31,11 @@ export async function writeEnvFile(args: {
const key = split[0]
let value = split[1]
if (key === 'MONGODB_URI' || key === 'MONGO_URL') {
if (
key === 'MONGODB_URI' ||
key === 'MONGO_URL' ||
key === 'DATABASE_URI'
) {
value = databaseUri
}
if (key === 'PAYLOAD_SECRET' || key === 'PAYLOAD_SECRET_KEY') {

View File

@@ -2,13 +2,13 @@ import slugify from '@sindresorhus/slugify'
import arg from 'arg'
import commandExists from 'command-exists'
import { createProject } from './lib/create-project'
import { getDatabaseConnection } from './lib/get-db-connection'
import { selectDb } from './lib/select-db'
import { generateSecret } from './lib/generate-secret'
import { parseProjectName } from './lib/parse-project-name'
import { parseTemplate } from './lib/parse-template'
import { getValidTemplates, validateTemplate } from './lib/templates'
import { writeEnvFile } from './lib/write-env-file'
import type { CliArgs } from './types'
import type { CliArgs, PackageManager } from './types'
import { success } from './utils/log'
import { helpMessage, successMessage, welcomeMessage } from './utils/messages'
@@ -25,6 +25,8 @@ export class Main {
'--db': String,
'--secret': String,
'--use-npm': Boolean,
'--use-yarn': Boolean,
'--use-pnpm': Boolean,
'--no-deps': Boolean,
'--dry-run': Boolean,
'--beta': Boolean,
@@ -62,7 +64,7 @@ export class Main {
const packageManager = await getPackageManager(this.args)
if (template.type !== 'plugin') {
const databaseUri = await getDatabaseConnection(this.args, projectName)
const dbDetails = await selectDb(this.args, projectName)
const payloadSecret = await generateSecret()
if (!this.args['--dry-run']) {
await createProject({
@@ -71,9 +73,10 @@ export class Main {
projectDir,
template,
packageManager,
dbDetails,
})
await writeEnvFile({
databaseUri,
databaseUri: dbDetails.dbUri,
payloadSecret,
template,
projectDir,
@@ -99,14 +102,22 @@ export class Main {
}
}
async function getPackageManager(args: CliArgs): Promise<string> {
let packageManager: string
async function getPackageManager(args: CliArgs): 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 {
try {
await commandExists('yarn')
packageManager = 'yarn'
if (await commandExists('yarn')) {
packageManager = 'yarn'
} else if (await commandExists('pnpm')) {
packageManager = 'pnpm'
}
} catch (error: unknown) {
packageManager = 'npm'
}

View File

@@ -7,6 +7,8 @@ export interface Args extends arg.Spec {
'--db': StringConstructor
'--secret': StringConstructor
'--use-npm': BooleanConstructor
'--use-yarn': BooleanConstructor
'--use-pnpm': BooleanConstructor
'--no-deps': BooleanConstructor
'--dry-run': BooleanConstructor
'--beta': BooleanConstructor
@@ -42,3 +44,15 @@ interface Template {
type: ProjectTemplate['type']
description?: string
}
export type PackageManager = 'npm' | 'yarn' | 'pnpm'
export type DbType = 'mongodb' | 'postgres'
export type DbDetails = {
type: DbType
dbUri: string
}
export type BundlerType = 'webpack' | 'vite'
export type EditorType = 'lexical' | 'slate'

View File

@@ -31,6 +31,8 @@ export async function helpMessage(): Promise<string> {
{dim Available templates: ${formatTemplates(validTemplates)}}
--use-npm Use npm to install dependencies
--use-yarn Use yarn to install dependencies
--use-pnpm Use pnpm to install dependencies
--no-deps Do not install any dependencies
-h Show help
`