Merge remote-tracking branch 'origin/feat/next-poc' into feat/next-poc

This commit is contained in:
Alessio Gravili
2024-03-04 16:56:22 -05:00
6 changed files with 285 additions and 54 deletions

View File

@@ -89,6 +89,7 @@
"@types/testing-library__jest-dom": "5.14.8",
"add-stream": "^1.0.0",
"chalk": "^4.1.2",
"comment-json": "^4.2.3",
"concat-stream": "^2.0.0",
"conventional-changelog": "^5.1.0",
"conventional-changelog-conventionalcommits": "^7.0.2",

View File

@@ -7,7 +7,7 @@
},
"scripts": {
"build": "pnpm copyfiles && pnpm build:swc",
"copyfiles": "copyfiles -u 4 \"../next/src/app/(payload)/**\" \"dist/app\"",
"copyfiles": "copyfiles -u 2 \"../../app/(payload)/**\" \"dist\"",
"build:swc": "swc ./src -d ./dist --config-file .swcrc",
"clean": "rimraf {dist,*.tsbuildinfo}",
"test": "jest",
@@ -23,7 +23,9 @@
"arg": "^5.0.0",
"chalk": "^4.1.0",
"command-exists": "^1.2.9",
"comment-json": "^4.2.3",
"degit": "^2.8.4",
"detect-package-manager": "^3.0.1",
"execa": "^5.0.0",
"figures": "^3.2.0",
"fs-extra": "^9.0.1",

View File

@@ -1,16 +1,88 @@
import type { CompilerOptions } from 'typescript'
import chalk from 'chalk'
import * as CommentJson from 'comment-json'
import { detect } from 'detect-package-manager'
import execa from 'execa'
import fs from 'fs'
import fse from 'fs-extra'
import globby from 'globby'
import path from 'path'
import type { CliArgs } from '../types'
import { copyRecursiveSync } from '../utils/copy-recursive-sync'
import { error, info, debug as origDebug, success } from '../utils/log'
import { error, info, debug as origDebug, success, warning } from '../utils/log'
export async function initNext(
args: Pick<CliArgs, '--debug'> & { nextDir?: string; useDistFiles?: boolean },
): Promise<{ success: boolean }> {
const { '--debug': debug, nextDir, useDistFiles } = args
type InitNextArgs = Pick<CliArgs, '--debug'> & {
projectDir?: string
useDistFiles?: boolean
}
type InitNextResult = { reason?: string; success: boolean; userAppDir?: string }
export async function initNext(args: InitNextArgs): Promise<InitNextResult> {
args.projectDir = args.projectDir || process.cwd()
const { projectDir } = args
const templateResult = await applyPayloadTemplateFiles(args)
if (!templateResult.success) return templateResult
const { success: installSuccess } = await installDeps(projectDir)
if (!installSuccess) {
return { ...templateResult, reason: 'Failed to install dependencies', success: false }
}
// Create or find payload.config.ts
const createConfigResult = findOrCreatePayloadConfig(projectDir)
if (!createConfigResult.success) {
return { ...templateResult, ...createConfigResult }
}
// Add `@payload-config` to tsconfig.json `paths`
await addPayloadConfigToTsConfig(projectDir)
// Output directions for user to update next.config.js
const withPayloadMessage = `
${chalk.bold(`Wrap your existing next.config.js with the withPayload function. Here is an example:`)}
const { withPayload } = require("@payloadcms/next");
const nextConfig = {
// Your Next.js config
};
module.exports = withPayload(nextConfig);
`
console.log(withPayloadMessage)
return templateResult
}
async function addPayloadConfigToTsConfig(projectDir: string) {
const tsConfigPath = path.resolve(projectDir, 'tsconfig.json')
const userTsConfigContent = await fse.readFile(tsConfigPath, {
encoding: 'utf8',
})
const userTsConfig = CommentJson.parse(userTsConfigContent) as {
compilerOptions?: CompilerOptions
}
if (!userTsConfig.compilerOptions && !('extends' in userTsConfig)) {
userTsConfig.compilerOptions = {}
}
if (!userTsConfig.compilerOptions.paths?.['@payload-config']) {
userTsConfig.compilerOptions.paths = {
...(userTsConfig.compilerOptions.paths || {}),
'@payload-config': ['./payload.config.ts'],
}
await fse.writeFile(tsConfigPath, CommentJson.stringify(userTsConfig, null, 2))
}
}
async function applyPayloadTemplateFiles(args: InitNextArgs): Promise<InitNextResult> {
const { '--debug': debug, projectDir, useDistFiles } = args
info('Initializing Payload app in Next.js project', 1)
@@ -18,24 +90,18 @@ export async function initNext(
if (debug) origDebug(message)
}
let projectDir = process.cwd()
if (nextDir) {
projectDir = path.resolve(projectDir, nextDir)
if (debug) logDebug(`Overriding project directory to ${projectDir}`)
}
if (!fs.existsSync(projectDir)) {
error(`Could not find specified project directory at ${projectDir}`)
return { success: false }
return { reason: `Could not find specified project directory at ${projectDir}`, success: false }
}
// Next.js configs can be next.config.js, next.config.mjs, etc.
const foundConfig = (await globby('next.config.*js', { cwd: projectDir }))?.[0]
const nextConfigPath = path.resolve(projectDir, foundConfig)
if (!fs.existsSync(nextConfigPath)) {
error(
`No next.config.js found at ${nextConfigPath}. Ensure you are in a Next.js project directory.`,
)
return { success: false }
return {
reason: `No next.config.js found at ${nextConfigPath}. Ensure you are in a Next.js project directory.`,
success: false,
}
} else {
if (debug) logDebug(`Found Next config at ${nextConfigPath}`)
}
@@ -43,21 +109,27 @@ export async function initNext(
const templateFilesPath =
__dirname.endsWith('dist') || useDistFiles
? path.resolve(__dirname, '../..', 'dist/app')
: path.resolve(__dirname, '../../../next/src/app')
: path.resolve(__dirname, '../../../../app')
if (debug) logDebug(`Using template files from: ${templateFilesPath}`)
if (!fs.existsSync(templateFilesPath)) {
error(`Could not find template source files from ${templateFilesPath}`)
return { success: false }
return {
reason: `Could not find template source files from ${templateFilesPath}`,
success: false,
}
} else {
if (debug) logDebug('Found template source files')
}
const userAppDir = path.resolve(projectDir, 'src/app')
// src/app or app
const userAppDirGlob = await globby(['**/app'], {
cwd: projectDir,
onlyDirectories: true,
})
const userAppDir = path.resolve(projectDir, userAppDirGlob?.[0])
if (!fs.existsSync(userAppDir)) {
error(`Could not find user app directory at ${userAppDir}`)
return { success: false }
return { reason: `Could not find user app directory inside ${projectDir}`, success: false }
} else {
logDebug(`Found user app directory: ${userAppDir}`)
}
@@ -65,5 +137,83 @@ export async function initNext(
logDebug(`Copying template files from ${templateFilesPath} to ${userAppDir}`)
copyRecursiveSync(templateFilesPath, userAppDir, debug)
success('Successfully initialized.')
return { success: true }
return { success: true, userAppDir }
}
async function installDeps(projectDir: string) {
const packageManager = await detect({ cwd: projectDir })
if (!packageManager) {
throw new Error('Could not detect package manager')
}
info(`Installing dependencies with ${packageManager}`, 1)
const packagesToInstall = [
'payload',
'@payloadcms/db-mongodb',
'@payloadcms/next',
'@payloadcms/richtext-slate',
'@payloadcms/ui',
].map((pkg) => `${pkg}@alpha`)
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
}
}
if (exitCode !== 0) {
error(`Failed to install dependencies with ${packageManager}`)
} else {
success(`Successfully installed dependencies`)
}
return { success: exitCode === 0 }
}
function findOrCreatePayloadConfig(projectDir: string) {
const configPath = path.resolve(projectDir, 'payload.config.ts')
if (fs.existsSync(configPath)) {
return { message: 'Found existing payload.config.ts', success: true }
} else {
// Create default config
// TODO: Pull this from templates
const defaultConfig = `import path from "path";
import { mongooseAdapter } from "@payloadcms/db-mongodb"; // database-adapter-import
import { slateEditor } from "@payloadcms/richtext-slate"; // editor-import
import { buildConfig } from "payload/config";
export default buildConfig({
editor: slateEditor({}), // editor-config
collections: [],
secret: "asdfasdf",
typescript: {
outputFile: path.resolve(__dirname, "payload-types.ts"),
},
graphQL: {
schemaOutputFile: path.resolve(__dirname, "generated-schema.graphql"),
},
db: mongooseAdapter({
url: "mongodb://localhost:27017/next-payload-3",
}),
});
`
fse.writeFileSync(configPath, defaultConfig)
return { message: 'Created default payload.config.ts', success: true }
}
}

View File

@@ -12,7 +12,7 @@ import { parseTemplate } from './lib/parse-template'
import { selectDb } from './lib/select-db'
import { getValidTemplates, validateTemplate } from './lib/templates'
import { writeEnvFile } from './lib/write-env-file'
import { success } from './utils/log'
import { error, success } from './utils/log'
import { helpMessage, successMessage, welcomeMessage } from './utils/messages'
export class Main {
@@ -61,6 +61,11 @@ export class Main {
if (this.args['--init-next']) {
const result = await initNext(this.args)
if (!result.success) {
error(result.reason || 'Failed to initialize Payload app in Next.js project')
} else {
success('Payload app successfully initialized in Next.js project')
}
process.exit(result.success ? 0 : 1)
}

56
pnpm-lock.yaml generated
View File

@@ -110,6 +110,9 @@ importers:
chalk:
specifier: ^4.1.2
version: 4.1.2
comment-json:
specifier: ^4.2.3
version: 4.2.3
concat-stream:
specifier: ^2.0.0
version: 2.0.0
@@ -278,9 +281,15 @@ importers:
command-exists:
specifier: ^1.2.9
version: 1.2.9
comment-json:
specifier: ^4.2.3
version: 4.2.3
degit:
specifier: ^2.8.4
version: 2.8.4
detect-package-manager:
specifier: ^3.0.1
version: 3.0.1
execa:
specifier: ^5.0.0
version: 5.1.1
@@ -4305,7 +4314,7 @@ packages:
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
'@jest/types': 29.6.3
'@types/node': 20.6.2
'@types/node': 16.18.85
chalk: 4.1.2
jest-message-util: 29.7.0
jest-util: 29.7.0
@@ -4366,7 +4375,7 @@ packages:
dependencies:
'@jest/fake-timers': 29.7.0
'@jest/types': 29.6.3
'@types/node': 20.6.2
'@types/node': 16.18.85
jest-mock: 29.7.0
/@jest/expect-utils@29.7.0:
@@ -4390,7 +4399,7 @@ packages:
dependencies:
'@jest/types': 29.6.3
'@sinonjs/fake-timers': 10.3.0
'@types/node': 20.6.2
'@types/node': 16.18.85
jest-message-util: 29.7.0
jest-mock: 29.7.0
jest-util: 29.7.0
@@ -4421,7 +4430,7 @@ packages:
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
'@jridgewell/trace-mapping': 0.3.23
'@types/node': 20.6.2
'@types/node': 16.18.85
chalk: 4.1.2
collect-v8-coverage: 1.0.2
exit: 0.1.2
@@ -7491,6 +7500,9 @@ packages:
is-string: 1.0.7
dev: false
/array-timsort@1.0.3:
resolution: {integrity: sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==}
/array-union@2.1.0:
resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
engines: {node: '>=8'}
@@ -8290,6 +8302,16 @@ packages:
engines: {node: ^12.20.0 || >=14}
dev: false
/comment-json@4.2.3:
resolution: {integrity: sha512-SsxdiOf064DWoZLH799Ata6u7iV658A11PlWtZATDlXPpKGJnbJZ5Z24ybixAi+LUUqJ/GKowAejtC5GFUG7Tw==}
engines: {node: '>= 6'}
dependencies:
array-timsort: 1.0.3
core-util-is: 1.0.3
esprima: 4.0.1
has-own-prop: 2.0.0
repeat-string: 1.6.1
/comment-parser@1.4.1:
resolution: {integrity: sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==}
engines: {node: '>= 12.0.0'}
@@ -8559,7 +8581,6 @@ packages:
/core-util-is@1.0.3:
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
dev: true
/cosmiconfig@7.1.0:
resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==}
@@ -9177,6 +9198,13 @@ packages:
resolution: {integrity: sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==}
engines: {node: '>=8'}
/detect-package-manager@3.0.1:
resolution: {integrity: sha512-qoHDH6+lMcpJPAScE7+5CYj91W0mxZNXTwZPrCqi1KMk+x+AoQScQ2V1QyqTln1rHU5Haq5fikvOGHv+leKD8A==}
engines: {node: '>=12'}
dependencies:
execa: 5.1.1
dev: false
/diff-sequences@27.5.1:
resolution: {integrity: sha512-k1gCAXAsNgLwEL+Y8Wvl+M6oEFj5bgazfZULpS5CneoPPXRaCCW7dm+q21Ky2VEE5X+VeRDBVg1Pcvvsr4TtNQ==}
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
@@ -11166,6 +11194,10 @@ packages:
resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==}
engines: {node: '>=8'}
/has-own-prop@2.0.0:
resolution: {integrity: sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==}
engines: {node: '>=8'}
/has-property-descriptors@1.0.2:
resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==}
dependencies:
@@ -12313,7 +12345,7 @@ packages:
dependencies:
'@jest/types': 29.6.3
'@types/graceful-fs': 4.1.9
'@types/node': 20.6.2
'@types/node': 16.18.85
anymatch: 3.1.3
fb-watchman: 2.0.2
graceful-fs: 4.2.11
@@ -12370,7 +12402,7 @@ packages:
engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0}
dependencies:
'@jest/types': 29.6.3
'@types/node': 20.6.2
'@types/node': 16.18.85
jest-util: 29.7.0
/jest-pnp-resolver@1.2.3(jest-resolve@29.7.0):
@@ -12420,7 +12452,7 @@ packages:
'@jest/test-result': 29.7.0
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
'@types/node': 20.6.2
'@types/node': 16.18.85
chalk: 4.1.2
emittery: 0.13.1
graceful-fs: 4.2.11
@@ -12450,7 +12482,7 @@ packages:
'@jest/test-result': 29.7.0
'@jest/transform': 29.7.0
'@jest/types': 29.6.3
'@types/node': 20.6.2
'@types/node': 16.18.85
chalk: 4.1.2
cjs-module-lexer: 1.2.3
collect-v8-coverage: 1.0.2
@@ -12523,7 +12555,7 @@ packages:
dependencies:
'@jest/test-result': 29.7.0
'@jest/types': 29.6.3
'@types/node': 20.6.2
'@types/node': 16.18.85
ansi-escapes: 4.3.2
chalk: 4.1.2
emittery: 0.13.1
@@ -15847,6 +15879,10 @@ packages:
- typescript
dev: true
/repeat-string@1.6.1:
resolution: {integrity: sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==}
engines: {node: '>=0.10'}
/require-directory@2.1.1:
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
engines: {node: '>=0.10.0'}

View File

@@ -1,48 +1,85 @@
import type { CompilerOptions } from 'typescript'
import * as CommentJson from 'comment-json'
import fs from 'fs'
import path from 'path'
import shelljs from 'shelljs'
import { promisify } from 'util'
import { initNext } from '../../packages/create-payload-app/src/lib/init-next'
const readFile = promisify(fs.readFile)
const nextCreateCommands: Partial<Record<'noSrcDir' | 'srcDir', string>> = {
srcDir:
'pnpm create next-app@latest . --typescript --eslint --no-tailwind --app --import-alias="@/*" --src-dir',
noSrcDir:
'pnpm create next-app@latest . --typescript --eslint --no-tailwind --app --import-alias="@/*" --no-src-dir',
}
describe('create-payload-app', () => {
describe('--init-next', () => {
const nextDir = path.resolve(__dirname, 'test-app')
beforeAll(() => {
// Runs copyfiles copy app/(payload) -> dist/app/(payload)
shelljs.exec('pnpm build:create-payload-app')
})
beforeAll(() => {
if (fs.existsSync(nextDir)) {
fs.rmdirSync(nextDir, { recursive: true })
describe('Next.js app template files', () => {
it('should exist in dist', () => {
const distPath = path.resolve(
__dirname,
'../../packages/create-payload-app/dist/app/(payload)',
)
expect(fs.existsSync(distPath)).toBe(true)
})
})
describe.each(Object.keys(nextCreateCommands))(`--init-next with %s`, (nextCmdKey) => {
const projectDir = path.resolve(__dirname, 'test-app')
beforeEach(() => {
if (fs.existsSync(projectDir)) {
fs.rmdirSync(projectDir, { recursive: true })
}
// Create dir for Next.js project
if (!fs.existsSync(nextDir)) {
fs.mkdirSync(nextDir)
if (!fs.existsSync(projectDir)) {
fs.mkdirSync(projectDir)
}
// Create a new Next.js project with default options
shelljs.exec(
'pnpm create next-app@latest . --typescript --eslint --no-tailwind --app --import-alias="@/*" --src-dir',
{ cwd: nextDir },
)
shelljs.exec(nextCreateCommands[nextCmdKey], { cwd: projectDir })
})
afterAll(() => {
if (fs.existsSync(nextDir)) {
fs.rmdirSync(nextDir, { recursive: true })
afterEach(() => {
if (fs.existsSync(projectDir)) {
fs.rmdirSync(projectDir, { recursive: true })
}
})
it('should install payload app in Next.js project', async () => {
expect(fs.existsSync(nextDir)).toBe(true)
expect(fs.existsSync(projectDir)).toBe(true)
const result = await initNext({
'--debug': true,
nextDir,
useDistFiles: true, // create-payload-app must be built
projectDir,
useDistFiles: true, // create-payload-app/dist/app/(payload)
})
expect(result.success).toBe(true)
const payloadFilesPath = path.resolve(nextDir, 'src/app/(payload)')
const payloadFilesPath = path.resolve(result.userAppDir, '(payload)')
expect(fs.existsSync(payloadFilesPath)).toBe(true)
const payloadConfig = path.resolve(projectDir, 'payload.config.ts')
expect(fs.existsSync(payloadConfig)).toBe(true)
const tsConfigPath = path.resolve(projectDir, 'tsconfig.json')
const userTsConfigContent = await readFile(tsConfigPath, { encoding: 'utf8' })
const userTsConfig = CommentJson.parse(userTsConfigContent) as {
compilerOptions?: CompilerOptions
}
expect(userTsConfig.compilerOptions.paths?.['@payload-config']).toEqual([
'./payload.config.ts',
])
})
})
})