refactor: rewrite in typescript (#7)

This commit is contained in:
Elliot DeNolf
2021-09-10 16:56:37 -04:00
committed by GitHub
parent 64d0bc7a16
commit a1a4765a94
80 changed files with 3999 additions and 609 deletions

15
src/index.ts Normal file
View File

@@ -0,0 +1,15 @@
import { Main } from './main'
import { init, handleException } from './utils/usage'
import { error } from './utils/log'
;(async () => {
const trx = init()
const main = new Main()
try {
await main.init()
} catch (e) {
handleException(e)
error(`An error has occurred: ${e && e.message}`)
} finally {
trx.finish()
}
})()

View File

@@ -0,0 +1,49 @@
import fse from 'fs-extra'
import path from 'path'
import type { ProjectTemplate } from '../types'
import {
createProject,
getLatestPayloadVersion,
updatePayloadVersion,
} from './createProject'
describe('createProject', () => {
const projectDir = path.resolve(__dirname, './tmp')
beforeAll(() => {
console.log = jest.fn()
fse.rmdirSync(projectDir, { recursive: true })
})
afterEach(() => {
fse.rmdirSync(projectDir, { recursive: true })
})
describe('#createProject', () => {
const args = { _: ['project-name'], '--no-deps': true }
const packageManager = 'yarn'
it('creates static project', async () => {
const expectedPayloadVersion = await getLatestPayloadVersion()
const template: ProjectTemplate = { name: 'ts-todo', type: 'static' }
await createProject(args, projectDir, template, packageManager)
const packageJsonPath = path.resolve(projectDir, 'package.json')
const packageJson = fse.readJsonSync(packageJsonPath)
expect(packageJson.dependencies.payload).toBe(expectedPayloadVersion)
})
})
describe('#updatePayloadVersion', () => {
it('updates payload version in package.json', async () => {
const packageJsonPath = path.resolve(projectDir, 'package.json')
await fse.mkdir(projectDir)
await fse.writeJson(
packageJsonPath,
{ dependencies: { payload: '0.0.1' } },
{ spaces: 2 },
)
await updatePayloadVersion(projectDir)
const modified = await fse.readJson(packageJsonPath)
expect(modified.dependencies.payload).not.toBe('0.0.1')
})
})
})

112
src/lib/createProject.ts Normal file
View File

@@ -0,0 +1,112 @@
import path from 'path'
import chalk from 'chalk'
import fse from 'fs-extra'
import execa from 'execa'
import ora from 'ora'
import degit from 'degit'
import { success, error, warning } from '../utils/log'
import { setTags } from '../utils/usage'
import type { CliArgs, ProjectTemplate } from '../types'
async function createProjectDir(projectDir: string): Promise<void> {
await fse.mkdir(projectDir)
const readDir = await fse.readdir(projectDir)
if (readDir && readDir.length > 0) {
error(`The project directory '${projectDir}' is not empty`)
process.exit(1)
}
}
async function installDeps(
args: CliArgs,
dir: string,
packageManager: string,
): Promise<boolean> {
if (args['--no-deps']) {
return true
}
const cmd = packageManager === 'yarn' ? 'yarn' : 'npm install --legacy-peer-deps'
try {
await execa.command(cmd, {
cwd: path.resolve(dir),
})
return true
} catch (error: unknown) {
return false
}
}
export async function getLatestPayloadVersion(): Promise<false | string> {
try {
const { stdout } = await execa('npm info payload version', [], { shell: true })
return `^${stdout}`
} catch (error: unknown) {
return false
}
}
export async function updatePayloadVersion(projectDir: string): Promise<void> {
const payloadVersion = await getLatestPayloadVersion()
if (!payloadVersion) {
warning(
'Error retrieving latest Payload version. Please update your package.json manually.',
)
return
}
setTags({ payload_version: payloadVersion })
const packageJsonPath = path.resolve(projectDir, 'package.json')
try {
const packageObj = await fse.readJson(packageJsonPath)
packageObj.dependencies.payload = payloadVersion
await fse.writeJson(packageJsonPath, packageObj, { spaces: 2 })
} catch (err) {
warning(
'Unable to write Payload version to package.json. Please update your package.json manually.',
)
}
}
export async function createProject(
args: CliArgs,
projectDir: string,
template: ProjectTemplate,
packageManager: string,
): Promise<void> {
createProjectDir(projectDir)
const templateDir = path.resolve(__dirname, `../templates/${template.name}`)
console.log(
`\n Creating a new Payload app in ${chalk.green(path.resolve(projectDir))}\n`,
)
if (template.type === 'starter') {
const emitter = degit(template.url)
await emitter.clone(projectDir)
} else {
try {
await fse.copy(templateDir, projectDir, { recursive: true })
success('Project directory created')
} catch (err) {
const msg =
'Unable to copy template files. Please check template name or directory permissions.'
error(msg)
process.exit(1)
}
}
const spinner = ora('Checking latest Payload version...').start()
await updatePayloadVersion(projectDir)
spinner.text = 'Installing dependencies...'
const result = await installDeps(args, projectDir, packageManager)
spinner.stop()
spinner.clear()
if (result) {
success('Dependencies installed')
} else {
error('Error installing dependencies')
}
}

View File

@@ -0,0 +1,27 @@
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://localhost/${slugify(projectName)}`,
validate: (value: string) => value.length,
},
{
onCancel: () => {
process.exit(0)
},
},
)
return response.value
}

View File

@@ -0,0 +1,22 @@
import prompts from 'prompts'
import type { CliArgs } from '../types'
export async function getPayloadSecret(args: CliArgs): Promise<string> {
if (args['--secret']) return args['--secret']
const response = await prompts(
{
type: 'password',
name: 'value',
message: 'Enter a long, complex string for Payloads encryption key',
validate: (value: string) => value.length,
},
{
onCancel: () => {
process.exit(0)
},
},
)
return response.value
}

32
src/lib/parseLanguage.ts Normal file
View File

@@ -0,0 +1,32 @@
import prompts from 'prompts'
import type { CliArgs } from '../types'
export async function parseLanguage(args: CliArgs): Promise<string> {
if (args['--template']) return args['--template']
const response = await prompts(
{
type: 'select',
name: 'value',
message: 'Choose language',
choices: [
{
title: 'javascript',
value: 'js',
},
{
title: 'typescript',
value: 'ts',
},
],
validate: (value: string) => value.length,
},
{
onCancel: () => {
process.exit(0)
},
},
)
return response.value
}

View File

@@ -0,0 +1,23 @@
import prompts from 'prompts'
import type { CliArgs } from '../types'
export async function parseProjectName(args: CliArgs): Promise<string> {
if (args['--name']) return args['--name']
if (args._[0]) return args._[0]
const response = await prompts(
{
type: 'text',
name: 'value',
message: 'Project name?',
validate: (value: string) => value.length,
},
{
onCancel: () => {
process.exit(0)
},
},
)
return response.value
}

45
src/lib/parseTemplate.ts Normal file
View File

@@ -0,0 +1,45 @@
import prompts from 'prompts'
import type { CliArgs, ProjectTemplate } from '../types'
import { setTags } from '../utils/usage'
export async function parseTemplate(
args: CliArgs,
validTemplates: ProjectTemplate[],
language: string,
): Promise<ProjectTemplate> {
if (args['--template']) {
const templateName = args['--template']
const template = validTemplates.find(template => template.name === templateName)
if (!template) throw new Error('Invalid template given')
setTags({ template: template.name })
return template
}
const filteredTemplates = validTemplates
.filter(d => d.name.startsWith(language))
.map(t => t.name.replace(`${language}-`, ''))
const response = await prompts(
{
type: 'select',
name: 'value',
message: 'Choose project template',
choices: filteredTemplates.map(p => {
return { title: p, value: `${language}-${p}` }
}),
validate: (value: string) => value.length,
},
{
onCancel: () => {
process.exit(0)
},
},
)
// const template = `${language}-${response.value}`
const template = validTemplates.find(t => t.name === response.value)
if (!template) throw new Error('Template is undefined')
setTags({ template: template.name })
return template
}

30
src/lib/templates.ts Normal file
View File

@@ -0,0 +1,30 @@
import path from 'path'
import fs from 'fs'
import { error, info } from '../utils/log'
import type { ProjectTemplate } from '../types'
export async function validateTemplate(templateName: string): Promise<boolean> {
const validTemplates = await getValidTemplates()
if (!validTemplates.map(t => t.name).includes(templateName)) {
error(`'${templateName}' is not a valid template.`)
info(`Valid templates: ${validTemplates.join(', ')}`)
return false
}
return true
}
export async function getValidTemplates(): Promise<ProjectTemplate[]> {
const templateDir = path.resolve(__dirname, '../templates')
const dirs = getDirectories(templateDir)
const templates: ProjectTemplate[] = dirs.map(name => {
return { name, type: 'static' }
})
return templates
}
function getDirectories(dir: string) {
return fs.readdirSync(dir).filter(file => {
return fs.statSync(dir + '/' + file).isDirectory()
})
}

21
src/lib/writeEnvFile.ts Normal file
View File

@@ -0,0 +1,21 @@
import slugify from '@sindresorhus/slugify'
import fs from 'fs-extra'
import { error, success } from '../utils/log'
export async function writeEnvFile(
projectName: string,
databaseUri: string,
payloadSecret: string,
): Promise<void> {
const content = `MONGODB_URI=${databaseUri}\nPAYLOAD_SECRET=${payloadSecret}`
try {
const projectDir = `./${slugify(projectName)}`
await fs.outputFile(`${projectDir}/.env`, content)
success('.env file created')
} catch (err) {
error('Unable to write .env file')
error(err)
process.exit(1)
}
}

92
src/main.ts Normal file
View File

@@ -0,0 +1,92 @@
import slugify from '@sindresorhus/slugify'
import arg from 'arg'
import commandExists from 'command-exists'
import { createProject } from './lib/createProject'
import { getDatabaseConnection } from './lib/getDatabaseConnection'
import { getPayloadSecret } from './lib/getPayloadSecret'
import { parseLanguage } from './lib/parseLanguage'
import { parseProjectName } from './lib/parseProjectName'
import { parseTemplate } from './lib/parseTemplate'
import { getValidTemplates, validateTemplate } from './lib/templates'
import { writeEnvFile } from './lib/writeEnvFile'
import type { CliArgs } from './types'
import { success } from './utils/log'
import { helpMessage, successMessage, welcomeMessage } from './utils/messages'
import { setTags } from './utils/usage'
export class Main {
args: CliArgs
constructor() {
this.args = arg(
{
'--help': Boolean,
'--name': String,
'--template': String,
'--db': String,
'--secret': String,
'--use-npm': Boolean,
'--no-deps': Boolean,
'--dry-run': Boolean,
'-h': '--help',
'-n': '--name',
'-t': '--template',
},
{ permissive: true },
)
}
async init(): Promise<void> {
try {
if (this.args['--help']) {
console.log(await helpMessage())
process.exit(0)
}
const templateArg = this.args['--template']
if (templateArg) {
const valid = await validateTemplate(templateArg)
if (!valid) {
console.log(await helpMessage())
process.exit(1)
}
}
console.log(welcomeMessage)
const projectName = await parseProjectName(this.args)
const language = await parseLanguage(this.args)
const validTemplates = await getValidTemplates()
const template = await parseTemplate(this.args, validTemplates, language)
const databaseUri = await getDatabaseConnection(this.args, projectName)
const payloadSecret = await getPayloadSecret(this.args)
const projectDir = `./${slugify(projectName)}`
const packageManager = await getPackageManager(this.args)
if (!this.args['--dry-run']) {
await createProject(this.args, projectDir, template, packageManager)
await writeEnvFile(projectName, databaseUri, payloadSecret)
}
success('Payload project successfully created')
console.log(await successMessage(projectDir, packageManager))
} catch (error) {
console.log(error)
}
}
}
async function getPackageManager(args: CliArgs): Promise<string> {
let packageManager: string
if (args['--use-npm']) {
packageManager = 'npm'
} else {
try {
await commandExists('yarn')
packageManager = 'yarn'
} catch (error) {
packageManager = 'npm'
}
}
setTags({ package_manager: packageManager })
return packageManager
}

View File

@@ -0,0 +1,15 @@
// Example Collection - For reference only, this must be added to payload.config.js to be used.
const Examples = {
slug: 'examples',
admin: {
useAsTitle: 'someField',
},
fields: [
{
name: 'someField',
type: 'text',
},
],
}
export default Examples;

View File

@@ -0,0 +1,16 @@
const Users = {
slug: 'users',
auth: true,
admin: {
useAsTitle: 'email',
},
access: {
read: () => true,
},
fields: [
// Email added by default
// Add more fields as needed
],
};
export default Users;

View File

@@ -0,0 +1,3 @@
{
"exec": "node server.js"
}

View File

@@ -0,0 +1,20 @@
{
"name": "payload-starter-javascript",
"description": "Blank template - no collections",
"version": "1.0.0",
"main": "server.js",
"license": "MIT",
"scripts": {
"dev": "nodemon",
"build": "payload build",
"serve": "NODE_ENV=production node server.js"
},
"dependencies": {
"payload": "0.1.145",
"dotenv": "^8.2.0",
"express": "^4.17.1"
},
"devDependencies": {
"nodemon": "^2.0.6"
}
}

View File

@@ -0,0 +1,15 @@
import { buildConfig } from 'payload/config';
import Examples from './collections/Examples';
import Users from './collections/Users';
export default buildConfig({
serverURL: 'http://localhost:3000',
admin: {
user: Users.slug,
},
collections: [
Users,
// Add Collections here
// Examples
],
});

View File

@@ -0,0 +1,19 @@
const express = require('express');
const payload = require('payload');
require('dotenv').config();
const app = express();
// Initialize Payload
payload.init({
secret: process.env.PAYLOAD_SECRET,
mongoURL: process.env.MONGODB_URI,
express: app,
onInit: () => {
payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`);
},
});
// Add your own express routes here
app.listen(3000);

View File

@@ -0,0 +1,18 @@
const Categories = {
slug: 'categories',
admin: {
useAsTitle: 'name',
},
access: {
read: () => true,
},
fields: [
{
name: 'name',
type: 'text',
},
],
timestamps: false,
}
export default Categories;

View File

@@ -0,0 +1,60 @@
const Posts = {
slug: 'posts',
admin: {
defaultColumns: ['title', 'author', 'category', 'tags', 'status'],
useAsTitle: 'title',
},
access: {
read: () => true,
},
fields: [
{
name: 'title',
type: 'text',
},
{
name: 'author',
type: 'relationship',
relationTo: 'users',
},
{
name: 'publishedDate',
type: 'date',
},
{
name: 'category',
type: 'relationship',
relationTo: 'categories'
},
{
name: 'tags',
type: 'relationship',
relationTo: 'tags',
hasMany: true,
},
{
name: 'content',
type: 'richText'
},
{
name: 'status',
type: 'select',
options: [
{
value: 'draft',
label: 'Draft',
},
{
value: 'published',
label: 'Published',
},
],
defaultValue: 'draft',
admin: {
position: 'sidebar',
}
}
],
}
export default Posts;

View File

@@ -0,0 +1,18 @@
const Tags = {
slug: 'tags',
admin: {
useAsTitle: 'name',
},
access: {
read: () => true,
},
fields: [
{
name: 'name',
type: 'text',
},
],
timestamps: false,
}
export default Tags;

View File

@@ -0,0 +1,30 @@
const onlyNameIfPublic = ({ req: { user }, doc }) => {
// Only return name if not logged in
if (!user) {
return { name: doc.name };
}
return doc;
};
const Users = {
slug: 'users',
auth: true,
admin: {
useAsTitle: 'email',
},
access: {
read: () => true,
},
hooks: {
beforeRead: [onlyNameIfPublic]
},
fields: [
// Email added by default
{
name: 'name',
type: 'text',
}
],
};
export default Users;

View File

@@ -0,0 +1,3 @@
{
"exec": "node server.js"
}

View File

@@ -0,0 +1,20 @@
{
"name": "payload-blog",
"description": "Blog template",
"version": "1.0.0",
"main": "server.js",
"license": "MIT",
"scripts": {
"dev": "nodemon",
"build": "payload build",
"serve": "NODE_ENV=production node server.js"
},
"dependencies": {
"payload": "0.1.145",
"dotenv": "^8.2.0",
"express": "^4.17.1"
},
"devDependencies": {
"nodemon": "^2.0.6"
}
}

View File

@@ -0,0 +1,18 @@
import { buildConfig } from 'payload/config';
import Categories from './collections/Categories';
import Posts from './collections/Posts';
import Tags from './collections/Tags';
import Users from './collections/Users';
export default buildConfig({
serverURL: 'http://localhost:3000',
admin: {
user: Users.slug,
},
collections: [
Categories,
Posts,
Tags,
Users,
],
});

View File

@@ -0,0 +1,19 @@
import express from 'express'
import payload from 'payload'
require('dotenv').config()
const app = express()
// Initialize Payload
payload.init({
secret: process.env.PAYLOAD_SECRET,
mongoURL: process.env.MONGODB_URI,
express: app,
onInit: () => {
payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`)
},
})
// Add your own express routes here
app.listen(3000)

View File

@@ -0,0 +1,36 @@
const Todo = {
slug: 'todos',
admin: {
defaultColumns: ['listName', 'tasks', 'updatedAt'],
useAsTitle: 'listName',
},
access: {
create: () => true,
read: () => true,
update: () => true,
delete: () => true,
},
fields: [
{
name: 'listName',
type: 'text',
},
{
name: 'tasks',
type: 'array',
fields: [
{
name: 'name',
type: 'text',
},
{
name: 'complete',
type: 'checkbox',
defaultValue: false,
}
]
},
],
}
export default Todo;

View File

@@ -0,0 +1,16 @@
const Users = {
slug: 'users',
auth: true,
admin: {
useAsTitle: 'email',
},
access: {
read: () => true,
},
fields: [
// Email added by default
// Add more fields as needed
],
};
export default Users;

View File

@@ -0,0 +1,3 @@
{
"exec": "node server.js"
}

View File

@@ -0,0 +1,20 @@
{
"name": "payload-starter-javascript",
"description": "Simple to-do list example",
"version": "1.0.0",
"main": "server.js",
"license": "MIT",
"scripts": {
"dev": "nodemon",
"build": "payload build",
"serve": "NODE_ENV=production node server.js"
},
"dependencies": {
"payload": "0.1.145",
"dotenv": "^8.2.0",
"express": "^4.17.1"
},
"devDependencies": {
"nodemon": "^2.0.6"
}
}

View File

@@ -0,0 +1,14 @@
import { buildConfig } from 'payload/config';
import TodoLists from './collections/TodoLists';
import Users from './collections/Users';
export default buildConfig({
serverURL: 'http://localhost:3000',
admin: {
user: Users.slug,
},
collections: [
TodoLists,
Users,
],
});

View File

@@ -0,0 +1,19 @@
import express from 'express'
import payload from 'payload'
require('dotenv').config()
const app = express()
// Initialize Payload
payload.init({
secret: process.env.PAYLOAD_SECRET,
mongoURL: process.env.MONGODB_URI,
express: app,
onInit: () => {
payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`)
},
})
// Add your own express routes here
app.listen(3000)

View File

@@ -0,0 +1,4 @@
{
"ext": "ts",
"exec": "ts-node src/server.ts"
}

View File

@@ -0,0 +1,26 @@
{
"name": "payload-starter-typescript",
"description": "Blank template - no collections",
"version": "1.0.0",
"main": "dist/server.js",
"license": "MIT",
"scripts": {
"dev": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon",
"build:payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build",
"build:server": "tsc",
"build": "yarn build:payload && yarn build:server",
"serve": "cross-env PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production node dist/server.js"
},
"dependencies": {
"payload": "0.1.145",
"dotenv": "^8.2.0",
"express": "^4.17.1"
},
"devDependencies": {
"@types/express": "^4.17.9",
"cross-env": "^7.0.3",
"nodemon": "^2.0.6",
"ts-node": "^9.1.1",
"typescript": "^4.1.3"
}
}

View File

@@ -0,0 +1,17 @@
import { CollectionConfig } from 'payload/types';
// Example Collection - For reference only, this must be added to payload.config.ts to be used.
const Examples: CollectionConfig = {
slug: 'examples',
admin: {
useAsTitle: 'someField',
},
fields: [
{
name: 'someField',
type: 'text',
},
],
}
export default Examples;

View File

@@ -0,0 +1,18 @@
import { CollectionConfig } from 'payload/types';
const Users: CollectionConfig = {
slug: 'users',
auth: true,
admin: {
useAsTitle: 'email',
},
access: {
read: () => true,
},
fields: [
// Email added by default
// Add more fields as needed
],
};
export default Users;

View File

@@ -0,0 +1,15 @@
import { buildConfig } from 'payload/config';
// import Examples from './collections/Examples';
import Users from './collections/Users';
export default buildConfig({
serverURL: 'http://localhost:3000',
admin: {
user: Users.slug,
},
collections: [
Users,
// Add Collections here
// Examples,
],
});

View File

@@ -0,0 +1,19 @@
import express from 'express';
import payload from 'payload';
require('dotenv').config();
const app = express();
// Initialize Payload
payload.init({
secret: process.env.PAYLOAD_SECRET,
mongoURL: process.env.MONGODB_URI,
express: app,
onInit: () => {
payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`);
},
});
// Add your own express routes here
app.listen(3000);

View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"strict": false,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "./dist",
"rootDir": "./src",
"jsx": "react",
},
"ts-node": {
"transpileOnly": true
}
}

View File

@@ -0,0 +1,4 @@
{
"ext": "ts",
"exec": "ts-node src/server.ts"
}

View File

@@ -0,0 +1,26 @@
{
"name": "payload-blog-typescript",
"description": "Blog template",
"version": "1.0.0",
"main": "dist/server.js",
"license": "MIT",
"scripts": {
"dev": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon",
"build:payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build",
"build:server": "tsc",
"build": "yarn build:payload && yarn build:server",
"serve": "cross-env PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production node dist/server.js"
},
"dependencies": {
"payload": "0.1.144",
"dotenv": "^8.2.0",
"express": "^4.17.1"
},
"devDependencies": {
"@types/express": "^4.17.9",
"cross-env": "^7.0.3",
"nodemon": "^2.0.6",
"ts-node": "^9.1.1",
"typescript": "^4.1.3"
}
}

View File

@@ -0,0 +1,20 @@
import { CollectionConfig } from 'payload/types';
const Categories: CollectionConfig = {
slug: 'categories',
admin: {
useAsTitle: 'name',
},
access: {
read: () => true,
},
fields: [
{
name: 'name',
type: 'text',
},
],
timestamps: false,
}
export default Categories;

View File

@@ -0,0 +1,62 @@
import { CollectionConfig } from 'payload/types';
const Posts: CollectionConfig = {
slug: 'posts',
admin: {
defaultColumns: ['title', 'author', 'category', 'tags', 'status'],
useAsTitle: 'title',
},
access: {
read: () => true,
},
fields: [
{
name: 'title',
type: 'text',
},
{
name: 'author',
type: 'relationship',
relationTo: 'users',
},
{
name: 'publishedDate',
type: 'date',
},
{
name: 'category',
type: 'relationship',
relationTo: 'categories'
},
{
name: 'tags',
type: 'relationship',
relationTo: 'tags',
hasMany: true,
},
{
name: 'content',
type: 'richText'
},
{
name: 'status',
type: 'select',
options: [
{
value: 'draft',
label: 'Draft',
},
{
value: 'published',
label: 'Published',
},
],
defaultValue: 'draft',
admin: {
position: 'sidebar',
}
}
],
}
export default Posts;

View File

@@ -0,0 +1,20 @@
import { CollectionConfig } from 'payload/types';
const Tags: CollectionConfig = {
slug: 'tags',
admin: {
useAsTitle: 'name',
},
access: {
read: () => true,
},
fields: [
{
name: 'name',
type: 'text',
},
],
timestamps: false,
}
export default Tags;

View File

@@ -0,0 +1,33 @@
import { BeforeReadHook } from 'payload/dist/collections/config/types';
import { CollectionConfig } from 'payload/types';
const onlyNameIfPublic: BeforeReadHook = ({ req: { user }, doc }) => {
// Only return name if not logged in
if (!user) {
return { name: doc.name };
}
return doc;
};
const Users: CollectionConfig = {
slug: 'users',
auth: true,
admin: {
useAsTitle: 'email',
},
access: {
read: () => true,
},
hooks: {
beforeRead: [onlyNameIfPublic]
},
fields: [
// Email added by default
{
name: 'name',
type: 'text',
}
],
};
export default Users;

View File

@@ -0,0 +1,18 @@
import { buildConfig } from 'payload/config';
import Categories from './collections/Categories';
import Posts from './collections/Posts';
import Tags from './collections/Tags';
import Users from './collections/Users';
export default buildConfig({
serverURL: 'http://localhost:3000',
admin: {
user: Users.slug,
},
collections: [
Categories,
Posts,
Tags,
Users,
],
});

View File

@@ -0,0 +1,19 @@
import express from 'express';
import payload from 'payload';
require('dotenv').config();
const app = express();
// Initialize Payload
payload.init({
secret: process.env.PAYLOAD_SECRET,
mongoURL: process.env.MONGODB_URI,
express: app,
onInit: () => {
payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`);
},
});
// Add your own express routes here
app.listen(3000);

View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"strict": false,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "./dist",
"rootDir": "./src",
"jsx": "react",
},
"ts-node": {
"transpileOnly": true
}
}

View File

@@ -0,0 +1,4 @@
{
"ext": "ts",
"exec": "ts-node src/server.ts"
}

View File

@@ -0,0 +1,26 @@
{
"name": "payload-starter-typescript",
"description": "Simple to-do list example",
"version": "1.0.0",
"main": "dist/server.js",
"license": "MIT",
"scripts": {
"dev": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts nodemon",
"build:payload": "cross-env PAYLOAD_CONFIG_PATH=src/payload.config.ts payload build",
"build:server": "tsc",
"build": "yarn build:payload && yarn build:server",
"serve": "cross-env PAYLOAD_CONFIG_PATH=dist/payload.config.js NODE_ENV=production node dist/server.js"
},
"dependencies": {
"payload": "0.1.145",
"dotenv": "^8.2.0",
"express": "^4.17.1"
},
"devDependencies": {
"@types/express": "^4.17.9",
"cross-env": "^7.0.3",
"nodemon": "^2.0.6",
"ts-node": "^9.1.1",
"typescript": "^4.1.3"
}
}

View File

@@ -0,0 +1,38 @@
import { CollectionConfig } from 'payload/types';
const Todo: CollectionConfig = {
slug: 'todos',
admin: {
defaultColumns: ['listName', 'tasks', 'updatedAt'],
useAsTitle: 'listName',
},
access: {
create: () => true,
read: () => true,
update: () => true,
delete: () => true,
},
fields: [
{
name: 'listName',
type: 'text',
},
{
name: 'tasks',
type: 'array',
fields: [
{
name: 'name',
type: 'text',
},
{
name: 'complete',
type: 'checkbox',
defaultValue: false,
}
]
},
],
}
export default Todo;

View File

@@ -0,0 +1,18 @@
import { CollectionConfig } from 'payload/types';
const Users: CollectionConfig = {
slug: 'users',
auth: true,
admin: {
useAsTitle: 'email',
},
access: {
read: () => true,
},
fields: [
// Email added by default
// Add more fields as needed
],
};
export default Users;

View File

@@ -0,0 +1,14 @@
import { buildConfig } from 'payload/config';
import TodoLists from './collections/TodoLists';
import Users from './collections/Users';
export default buildConfig({
serverURL: 'http://localhost:3000',
admin: {
user: Users.slug,
},
collections: [
TodoLists,
Users,
],
});

View File

@@ -0,0 +1,19 @@
import express from 'express';
import payload from 'payload';
require('dotenv').config();
const app = express();
// Initialize Payload
payload.init({
secret: process.env.PAYLOAD_SECRET,
mongoURL: process.env.MONGODB_URI,
express: app,
onInit: () => {
payload.logger.info(`Payload Admin URL: ${payload.getAdminURL()}`);
},
});
// Add your own express routes here
app.listen(3000);

View File

@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"strict": false,
"esModuleInterop": true,
"skipLibCheck": true,
"outDir": "./dist",
"rootDir": "./src",
"jsx": "react",
},
"ts-node": {
"transpileOnly": true
}
}

30
src/types.ts Normal file
View File

@@ -0,0 +1,30 @@
import type arg from 'arg'
export type Args = {
'--help': BooleanConstructor
'--name': StringConstructor
'--template': StringConstructor
'--db': StringConstructor
'--secret': StringConstructor
'--use-npm': BooleanConstructor
'--no-deps': BooleanConstructor
'--dry-run': BooleanConstructor
'-h': string
'-n': string
'-t': string
}
export type CliArgs = arg.Result<Args>
export type ProjectTemplate = StaticTemplate | GitTemplate
export type StaticTemplate = {
name: string
type: 'static'
}
export type GitTemplate = {
name: string
type: 'starter'
url: string
}

18
src/utils/log.ts Normal file
View File

@@ -0,0 +1,18 @@
import chalk from 'chalk'
import figures from 'figures'
export const success = (message: string): void => {
console.log(chalk.green(figures.tick) + ' ' + chalk.bold(message))
}
export const warning = (message: string): void => {
console.log(chalk.yellow('? ') + chalk.bold(message))
}
export const info = (message: string): void => {
console.log(chalk.yellow(figures.info) + ' ' + chalk.bold(message))
}
export const error = (message: string): void => {
console.log(chalk.red(figures.cross) + ' ' + chalk.bold(message))
}

52
src/utils/messages.ts Normal file
View File

@@ -0,0 +1,52 @@
import chalk from 'chalk'
import figures from 'figures'
import terminalLink from 'terminal-link'
import { getValidTemplates } from '../lib/templates'
const header = (message: string) =>
chalk.yellow(figures.star) + ' ' + chalk.bold(message)
export const welcomeMessage = chalk`
{green Welcome to Payload. Let's create a project! }
`
export async function helpMessage(): Promise<string> {
const validTemplates = await getValidTemplates()
return chalk`
{bold USAGE}
{dim $} {bold npx create-payload-app}
{bold OPTIONS}
--name {underline my-payload-app} Set project name
--template {underline template_name} Choose specific template
{dim Available templates: ${validTemplates.join(', ')}}
--use-npm Use npm to install dependencies
--no-deps Do not install any dependencies
--help Show help
`
}
export function successMessage(projectDir: string, packageManager: string): string {
return `
${header('Launch Application:')}
- cd ${projectDir}
- ${packageManager === 'yarn' ? 'yarn' : 'npm run'} dev
${header('Documentation:')}
- ${terminalLink(
'Getting Started',
'https://payloadcms.com/docs/getting-started/what-is-payload',
)}
- ${terminalLink(
'Configuration',
'https://payloadcms.com/docs/configuration/overview',
)}
`
}

32
src/utils/usage.ts Normal file
View File

@@ -0,0 +1,32 @@
import * as Sentry from '@sentry/node'
import type { Primitive, Transaction } from '@sentry/types'
import os from 'os'
type SentryTags = { [key: string]: Primitive }
export const init = (): Transaction => {
Sentry.init({
dsn: 'https://139de3d0197f464082d5715a0c48a497@o589961.ingest.sentry.io/5739829',
tracesSampleRate: 1.0,
})
Sentry.setTags({
os_type: os.type(),
os_platform: os.platform(),
os_release: os.release(),
node_version: process.version,
})
return Sentry.startTransaction({
op: 'create-payload-app',
name: 'New Project',
})
}
export const setTags = (tags: SentryTags): void => {
Sentry.setTags({ ...tags })
}
export const handleException = (e: unknown): void => {
Sentry.captureException(e)
}