feat(cpa): create project from example using --example CLI arg (#10172)

Adds the ability to create a project using an existing in the Payload
repo example through `create-payload-app`:

For example:
`pnpx create-payload-app --example custom-server` - creates a project
from the
[custom-server](https://github.com/payloadcms/payload/tree/main/examples/custom-server)
example.

This is much easier and faster then downloading the whole repo and
copying the example to another folder.
Note that we don't configure the payload config with the storage / DB
adapter there because examples can be very specific.
This commit is contained in:
Sasha
2024-12-27 20:16:34 +02:00
committed by GitHub
parent 7e0975f970
commit 6b4842d44d
22 changed files with 325 additions and 123 deletions

View File

@@ -96,7 +96,11 @@ If you want to add contributions to this repository, please follow the instructi
The [Examples Directory](./examples) is a great resource for learning how to setup Payload in a variety of different ways, but you can also find great examples in our blog and throughout our social media.
If you'd like to run the examples, you can either copy them to a folder outside this repo or run them directly by (1) navigating to the example's subfolder (`cd examples/your-example-folder`) and (2) using the `--ignore-workspace` flag to bypass workspace restrictions (e.g., `pnpm --ignore-workspace install` or `pnpm --ignore-workspace dev`).
If you'd like to run the examples, you can use `create-payload-app` to create a project from one:
```sh
npx create-payload-app --example example_name
```
You can see more examples at:

View File

@@ -19,4 +19,10 @@ Payload provides a vast array of examples to help you get started with your proj
- [Tests](https://github.com/payloadcms/payload/tree/main/examples/testing)
- [White-label Admin UI](https://github.com/payloadcms/payload/tree/main/examples/whitelabel)
If you'd like to run the examples, you can use `create-payload-app` to create a project from one:
```sh
npx create-payload-app --example example_name
```
We are adding new examples every day, so if your particular use case is not demonstrated in any existing example, please feel free to start a new [Discussion](https://github.com/payloadcms/payload/discussions) or open a new [PR](https://github.com/payloadcms/payload/pulls) to add it yourself.

View File

@@ -6,20 +6,17 @@ This [Payload Auth Example](https://github.com/payloadcms/payload/tree/main/exam
To spin up this example locally, follow the steps below:
1. Clone this repo
1. Navigate into the project directory and install dependencies using your preferred package manager:
1. Run the following command to create a project from the example:
- `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
- `npx create-payload-app --example auth`
> \*NOTE: The --ignore-workspace flag is needed if you are running this example within the Payload monorepo to avoid workspace conflicts.
1. Start the server:
2. Start the server:
- Depending on your package manager, run `pnpm dev`, `yarn dev` or `npm run dev`
- When prompted, type `y` then `enter` to seed the database with sample data
1. Access the application:
3. Access the application:
- Open your browser and navigate to `http://localhost:3000` to access the homepage.
- Open `http://localhost:3000/admin` to access the admin panel.
1. Login:
4. Login:
- Use the following credentials to log into the admin panel:
> `Email: demo@payloadcms.com` > `Password: demo`

View File

@@ -6,20 +6,17 @@ This example demonstrates how to use Custom Components in the [Payload](https://
To spin up this example locally, follow the steps below:
1. Clone this repo
1. Navigate into the project directory and install dependencies using your preferred package manager:
1. Run the following command to create a project from the example:
- `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
- `npx create-payload-app --example custom-components`
> \*NOTE: The --ignore-workspace flag is needed if you are running this example within the Payload monorepo to avoid workspace conflicts.
1. Start the server:
2. Start the server:
- Depending on your package manager, run `pnpm dev`, `yarn dev` or `npm run dev`
- When prompted, type `y` then `enter` to seed the database with sample data
1. Access the application:
3. Access the application:
- Open your browser and navigate to `http://localhost:3000` to access the homepage.
- Open `http://localhost:3000/admin` to access the admin panel.
1. Login:
4. Login:
- Use the following credentials to log into the admin panel:
> `Email: demo@payloadcms.com` > `Password: demo`

View File

@@ -1,5 +1,9 @@
# Payload 3 with Custom Server
Run the following command to create a project from the example:
- `npx create-payload-app --example custom-server`
Uses a [Next.js Custom Server](https://nextjs.org/docs/pages/building-your-application/configuring/custom-server) with express.
Made from official [examples/custom-server](https://github.com/vercel/next.js/tree/canary/examples/custom-server) from Next.js repository.

View File

@@ -6,15 +6,14 @@ The [Payload Draft Preview Example](https://github.com/payloadcms/payload/tree/m
To spin up this example locally, follow these steps:
1. Clone this repo
2. `cd` into this directory and run `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
1. Run the following command to create a project from the example:
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
- `npx create-payload-app --example draft-preview`
3. `cp .env.example .env` to copy the example environment variables
4. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
5. `open http://localhost:3000/admin` to access the admin panel
6. Login with email `demo@payloadcms.com` and password `demo`
2. `cp .env.example .env` to copy the example environment variables
3. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
4. `open http://localhost:3000/admin` to access the admin panel
5. Login with email `demo@payloadcms.com` and password `demo`
That's it! Changes made in `./src` will be reflected in your app. See the [Development](#development) section for more details.

View File

@@ -6,7 +6,10 @@ This example demonstrates how to integrate email functionality into Payload.
To spin up this example locally, follow these steps:
1. Clone this repo
1. Run the following command to create a project from the example:
- `npx create-payload-app --example email`
2. `cp .env.example .env` to copy the example environment variables
3. `pnpm install && pnpm dev` to install dependencies and start the dev server
4. open `http://localhost:3000/admin` to access the admin panel

View File

@@ -6,17 +6,16 @@ The [Payload Form Builder Example](https://github.com/payloadcms/payload/tree/ma
## Quick Start
1. Clone this repo
2. `cd` into this directory and run `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
1. Run the following command to create a project from the example:
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
- `npx create-payload-app --example form-builder`
3. `cp .env.example .env` to copy the example environment variables
2. `cp .env.example .env` to copy the example environment variables
4. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
3. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
- Press `y` when prompted to seed the database
5. `open http://localhost:3000` to access the home page
6. `open http://localhost:3000/admin` to access the admin panel
4. `open http://localhost:3000` to access the home page
5. `open http://localhost:3000/admin` to access the admin panel
- Login with email `demo@payloadcms.com` and password `demo`
That's it! Changes made in `./src` will be

View File

@@ -6,17 +6,16 @@ The [Payload Live Preview Example](https://github.com/payloadcms/payload/tree/ma
## Quick Start
1. Clone this repo
2. `cd` into this directory and run `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
1. Run the following command to create a project from the example:
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
- `npx create-payload-app --example live-preview`
3. `cp .env.example .env` to copy the example environment variables
2. `cp .env.example .env` to copy the example environment variables
4. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
3. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
- Press `y` when prompted to seed the database
5. `open http://localhost:3000` to access the home page
6. `open http://localhost:3000/admin` to access the admin panel
4. `open http://localhost:3000` to access the home page
5. `open http://localhost:3000/admin` to access the admin panel
- Login with email `demo@payloadcms.com` and password `demo`
That's it! Changes made in `./src` will be reflected in your app. See the [Development](#development) section for more details.

View File

@@ -8,10 +8,14 @@ To facilitate the localization process, this example uses the next-intl library.
## Setup
1. `cp .env.example .env` (copy the .env.example file to .env)
2. `pnpm install` (`pnpm i --ignore-workspaces` if you are running from the monorepo)
3. `pnpm run dev`
4. Seed your database in the admin panel (see below)
1. Run the following command to create a project from the example:
- `npx create-payload-app --example localization`
2. `cp .env.example .env` (copy the .env.example file to .env)
3. `pnpm install`
4. `pnpm run dev`
5. Seed your database in the admin panel (see below)
## Seed

View File

@@ -6,15 +6,14 @@ This example demonstrates how to achieve a multi-tenancy in [Payload](https://gi
To spin up this example locally, follow these steps:
1. Clone this repo
1. `cd` into this directory and run `pnpm i --ignore-workspace`\*, `yarn`, or `npm install`
1. Run the following command to create a project from the example:
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
- `npx create-payload-app --example multi-tenant`
1. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
2. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
- Press `y` when prompted to seed the database
1. `open http://localhost:3000` to access the home page
1. `open http://localhost:3000/admin` to access the admin panel
3. `open http://localhost:3000` to access the home page
4. `open http://localhost:3000/admin` to access the admin panel
- Login with email `demo@payloadcms.com` and password `demo`
## How it works

View File

@@ -8,18 +8,17 @@ Checkout our [tutorial](https://payloadcms.com/blog/how-to-setup-tailwindcss-and
To spin up this example locally, follow these steps:
1. Clone this repo
1. `cd` into this directory and run `pnpm i --ignore-workspace`\*, or `npm install`
1. Run the following command to create a project from the example:
> \*If you are running using pnpm within the Payload Monorepo, the `--ignore-workspace` flag is needed so that pnpm generates a lockfile in this example's directory despite the fact that one exists in root.
- `npx create-payload-app --example tailwind-shadcn-ui`
1. `cp .env.example .env` to copy the example environment variables
2. `cp .env.example .env` to copy the example environment variables
> Adjust `PAYLOAD_PUBLIC_SITE_URL` in the `.env` if your front-end is running on a separate domain or port.
1. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
3. `pnpm dev`, `yarn dev` or `npm run dev` to start the server
- Press `y` when prompted to seed the database
1. `open http://localhost:3000` to access the home page
1. `open http://localhost:3000/admin` to access the admin panel
4. `open http://localhost:3000` to access the home page
5. `open http://localhost:3000/admin` to access the admin panel
That's it! Changes made in `./src` will be reflected in your app. See the [Development](#development) section for more details.

View File

@@ -2,6 +2,12 @@
This example demonstrates how to get started with testing Payload using [Jest](https://jestjs.io/). You can clone this down and use it as a starting point for your own Payload projects, or you can follow the steps below to add testing to your existing Payload project.
## Spin up locally:
Run the following command to create a project from the example:
- `npx create-payload-app --example testing`
## Add testing to your existing Payload project
1. Initial setup:

View File

@@ -6,7 +6,10 @@ This example demonstrates how to re-brand or white-label the [Payload Admin Pane
To spin up this example locally, follow these steps:
1. Clone this repo
1. Run the following command to create a project from the example:
- `npx create-payload-app --example whitelabel`
2. `cp .env.example .env` to copy the example environment variables
3. `pnpm install && pnpm dev` to install dependencies and start the dev server
4. `open http://localhost:3000/admin` to access the admin panel

View File

@@ -5,7 +5,7 @@ import globby from 'globby'
import * as os from 'node:os'
import path from 'path'
import type { CliArgs, DbType, ProjectTemplate } from '../types.js'
import type { CliArgs, DbType, ProjectExample, ProjectTemplate } from '../types.js'
import { createProject } from './create-project.js'
import { dbReplacements } from './replacements.js'
@@ -62,6 +62,32 @@ describe('createProject', () => {
expect(packageJson.name).toStrictEqual(projectName)
})
it('creates example', async () => {
const projectName = 'custom-server-example'
const example: ProjectExample = {
name: 'custom-server',
url: 'https://github.com/payloadcms/payload/examples/custom-server#main',
}
await createProject({
cliArgs: {
...args,
'--local-template': undefined,
'--local-example': 'custom-server',
} as CliArgs,
packageManager,
projectDir,
projectName,
example,
})
const packageJsonPath = path.resolve(projectDir, 'package.json')
const packageJson = fse.readJsonSync(packageJsonPath)
// Check package name and description
expect(packageJson.name).toStrictEqual(projectName)
})
describe('creates project from template', () => {
const templates = getValidTemplates()

View File

@@ -5,12 +5,19 @@ import fse from 'fs-extra'
import { fileURLToPath } from 'node:url'
import path from 'path'
import type { CliArgs, DbDetails, PackageManager, ProjectTemplate } from '../types.js'
import type {
CliArgs,
DbDetails,
PackageManager,
ProjectExample,
ProjectTemplate,
} from '../types.js'
import { tryInitRepoAndCommit } from '../utils/git.js'
import { debug, error, info, warning } from '../utils/log.js'
import { configurePayloadConfig } from './configure-payload-config.js'
import { configurePluginProject } from './configure-plugin-project.js'
import { downloadExample } from './download-example.js'
import { downloadTemplate } from './download-template.js'
const filename = fileURLToPath(import.meta.url)
@@ -53,15 +60,24 @@ async function installDeps(args: {
}
}
export async function createProject(args: {
cliArgs: CliArgs
dbDetails?: DbDetails
packageManager: PackageManager
projectDir: string
projectName: string
template: ProjectTemplate
}): Promise<void> {
const { cliArgs, dbDetails, packageManager, projectDir, projectName, template } = args
type TemplateOrExample =
| {
example: ProjectExample
}
| {
template: ProjectTemplate
}
export async function createProject(
args: {
cliArgs: CliArgs
dbDetails?: DbDetails
packageManager: PackageManager
projectDir: string
projectName: string
} & TemplateOrExample,
): Promise<void> {
const { cliArgs, dbDetails, packageManager, projectDir, projectName } = args
if (cliArgs['--dry-run']) {
debug(`Dry run: Creating project in ${chalk.green(projectDir)}`)
@@ -70,6 +86,12 @@ export async function createProject(args: {
await createOrFindProjectDir(projectDir)
if (cliArgs['--local-example']) {
// Copy example from local path. For development purposes.
const localExample = path.resolve(dirname, '../../../../examples/', cliArgs['--local-example'])
await fse.copy(localExample, projectDir)
}
if (cliArgs['--local-template']) {
// Copy template from local path. For development purposes.
const localTemplate = path.resolve(
@@ -78,9 +100,10 @@ export async function createProject(args: {
cliArgs['--local-template'],
)
await fse.copy(localTemplate, projectDir)
} else if ('url' in template) {
if (cliArgs['--template-branch']) {
template.url = `${template.url.split('#')?.[0]}#${cliArgs['--template-branch']}`
} else if ('template' in args && 'url' in args.template) {
const { template } = args
if (cliArgs['--branch']) {
template.url = `${template.url.split('#')?.[0]}#${cliArgs['--branch']}`
}
await downloadTemplate({
@@ -88,6 +111,17 @@ export async function createProject(args: {
projectDir,
template,
})
} else if ('example' in args && 'url' in args.example) {
const { example } = args
if (cliArgs['--branch']) {
example.url = `${example.url.split('#')?.[0]}#${cliArgs['--branch']}`
}
await downloadExample({
debug: cliArgs['--debug'],
example,
projectDir,
})
}
const spinner = p.spinner()
@@ -95,15 +129,17 @@ export async function createProject(args: {
await updatePackageJSON({ projectDir, projectName })
if (template.type === 'plugin') {
spinner.message('Configuring Plugin...')
configurePluginProject({ projectDirPath: projectDir, projectName })
} else {
spinner.message('Configuring Payload...')
await configurePayloadConfig({
dbType: dbDetails?.type,
projectDirOrConfigPath: { projectDir },
})
if ('template' in args) {
if (args.template.type === 'plugin') {
spinner.message('Configuring Plugin...')
configurePluginProject({ projectDirPath: projectDir, projectName })
} else {
spinner.message('Configuring Payload...')
await configurePayloadConfig({
dbType: dbDetails?.type,
projectDirOrConfigPath: { projectDir },
})
}
}
// Remove yarn.lock file. This is only desired in Payload Cloud.

View File

@@ -0,0 +1,46 @@
import { Readable } from 'node:stream'
import { pipeline } from 'node:stream/promises'
import { x } from 'tar'
import type { ProjectExample } from '../types.js'
import { debug as debugLog } from '../utils/log.js'
export async function downloadExample({
debug,
example,
projectDir,
}: {
debug?: boolean
example: ProjectExample
projectDir: string
}) {
const branchOrTag = example.url.split('#')?.[1] || 'latest'
const url = `https://codeload.github.com/payloadcms/payload/tar.gz/${branchOrTag}`
const filter = `payload-${branchOrTag.replace(/^v/, '')}/examples/${example.name}/`
if (debug) {
debugLog(`Using example url: ${example.url}`)
debugLog(`Codeload url: ${url}`)
debugLog(`Filter: ${filter}`)
}
await pipeline(
await downloadTarStream(url),
x({
cwd: projectDir,
filter: (p) => p.includes(filter),
strip: 2 + example.name.split('/').length,
}),
)
}
async function downloadTarStream(url: string) {
const res = await fetch(url)
if (!res.body) {
throw new Error(`Failed to download: ${url}`)
}
return Readable.from(res.body as unknown as NodeJS.ReadableStream)
}

View File

@@ -0,0 +1,38 @@
import type { ProjectExample } from '../types.js'
import { error, info } from '../utils/log.js'
export async function getExamples({ branch }: { branch: string }): Promise<ProjectExample[]> {
const url = `https://api.github.com/repos/payloadcms/payload/contents/examples?ref=${branch}`
const response = await fetch(url)
const examplesResponseList: { name: string; path: string }[] = await response.json()
const examples: ProjectExample[] = examplesResponseList.map((example) => ({
name: example.name,
url: `https://github.com/payloadcms/payload/examples/${example.name}#${branch}`,
}))
return examples
}
export async function parseExample({
name,
branch,
}: {
branch: string
name: string
}): Promise<false | ProjectExample> {
const examples = await getExamples({ branch })
const example = examples.find((e) => e.name === name)
if (!example) {
error(`'${name}' is not a valid example name.`)
info(`Valid examples: ${examples.map((e) => e.name).join(', ')}`)
return false
}
return example
}

View File

@@ -10,6 +10,7 @@ import type { CliArgs } from './types.js'
import { configurePayloadConfig } from './lib/configure-payload-config.js'
import { PACKAGE_VERSION } from './lib/constants.js'
import { createProject } from './lib/create-project.js'
import { parseExample } from './lib/examples.js'
import { generateSecret } from './lib/generate-secret.js'
import { getPackageManager } from './lib/get-package-manager.js'
import { getNextAppDetails, initNext } from './lib/init-next.js'
@@ -35,15 +36,16 @@ export class Main {
// @ts-expect-error bad typings
this.args = arg(
{
'--branch': String,
'--db': String,
'--db-accept-recommended': Boolean,
'--db-connection-string': String,
'--example': String,
'--help': Boolean,
'--local-template': String,
'--name': String,
'--secret': String,
'--template': String,
'--template-branch': String,
// Next.js
'--init-next': Boolean, // TODO: Is this needed if we detect if inside Next.js project?
@@ -65,6 +67,7 @@ export class Main {
// Aliases
'-d': '--db',
'-e': '--example',
'-h': '--help',
'-n': '--name',
'-t': '--template',
@@ -204,52 +207,76 @@ export class Main {
}
}
if (debugFlag) {
debug(`Using templates from git tag: v${PACKAGE_VERSION}`)
}
const exampleArg = this.args['--example']
const validTemplates = getValidTemplates()
const template = await parseTemplate(this.args, validTemplates)
if (!template) {
p.log.error('Invalid template given')
p.outro(feedbackOutro())
process.exit(1)
}
if (exampleArg) {
const example = await parseExample({
name: exampleArg,
branch: this.args['--branch'] ?? 'main',
})
switch (template.type) {
case 'plugin': {
await createProject({
cliArgs: this.args,
packageManager,
projectDir,
projectName,
template,
})
break
if (!example) {
helpMessage()
process.exit(1)
}
case 'starter': {
const dbDetails = await selectDb(this.args, projectName)
const payloadSecret = generateSecret()
await createProject({
cliArgs: this.args,
dbDetails,
packageManager,
projectDir,
projectName,
template,
})
await createProject({
cliArgs: this.args,
example,
packageManager,
projectDir,
projectName,
})
}
await manageEnvFiles({
cliArgs: this.args,
databaseType: dbDetails.type,
databaseUri: dbDetails.dbUri,
payloadSecret,
projectDir,
template,
})
if (debugFlag) {
debug(`Using ${exampleArg ? 'examples' : 'templates'} from git tag: v${PACKAGE_VERSION}`)
}
break
if (!exampleArg) {
const validTemplates = getValidTemplates()
const template = await parseTemplate(this.args, validTemplates)
if (!template) {
p.log.error('Invalid template given')
p.outro(feedbackOutro())
process.exit(1)
}
switch (template.type) {
case 'plugin': {
await createProject({
cliArgs: this.args,
packageManager,
projectDir,
projectName,
template,
})
break
}
case 'starter': {
const dbDetails = await selectDb(this.args, projectName)
const payloadSecret = generateSecret()
await createProject({
cliArgs: this.args,
dbDetails,
packageManager,
projectDir,
projectName,
template,
})
await manageEnvFiles({
cliArgs: this.args,
databaseType: dbDetails.type,
databaseUri: dbDetails.dbUri,
payloadSecret,
projectDir,
template,
})
break
}
}
}

View File

@@ -2,20 +2,23 @@ import type arg from 'arg'
export interface Args extends arg.Spec {
'--beta': BooleanConstructor
'--branch': StringConstructor
'--db': StringConstructor
'--db-accept-recommended': BooleanConstructor
'--db-connection-string': StringConstructor
'--debug': BooleanConstructor
'--dry-run': BooleanConstructor
'--example': StringConstructor
'--help': BooleanConstructor
'--init-next': BooleanConstructor
'--local-example': StringConstructor
'--local-template': StringConstructor
'--name': StringConstructor
'--no-deps': BooleanConstructor
'--no-git': BooleanConstructor
'--secret': StringConstructor
'--template': StringConstructor
'--template-branch': StringConstructor
'--use-bun': BooleanConstructor
'--use-npm': BooleanConstructor
'--use-pnpm': BooleanConstructor
@@ -23,6 +26,7 @@ export interface Args extends arg.Spec {
// Aliases
'-e': string
'-h': string
'-n': string
'-t': string
@@ -32,6 +36,11 @@ export type CliArgs = arg.Result<Args>
export type ProjectTemplate = GitTemplate | PluginTemplate
export type ProjectExample = {
name: string
url: string
}
/**
* Template that is cloned verbatim from a git repo
* Performs .env manipulation based upon input

View File

@@ -34,6 +34,7 @@ export function helpMessage(): void {
-n {underline my-payload-app} Set project name
-t {underline template_name} Choose specific template
-e {underline example_name} Choose specific exmaple
{dim Available templates: ${formatTemplates(validTemplates)}}

View File

@@ -28,7 +28,7 @@
}
],
"paths": {
"@payload-config": ["./test/access-control/config.ts"],
"@payload-config": ["./test/_community/config.ts"],
"@payloadcms/live-preview": ["./packages/live-preview/src"],
"@payloadcms/live-preview-react": ["./packages/live-preview-react/src/index.ts"],
"@payloadcms/live-preview-vue": ["./packages/live-preview-vue/src/index.ts"],