feat(cpa): strict true 😈 (#5587)

This commit is contained in:
Elliot DeNolf
2024-04-01 23:05:57 -04:00
committed by GitHub
parent 799370f753
commit b26117a65d
11 changed files with 148 additions and 74 deletions

View File

@@ -31,7 +31,7 @@
"detect-package-manager": "^3.0.1",
"esprima": "^4.0.1",
"execa": "^5.0.0",
"figures": "^3.2.0",
"figures": "^6.1.0",
"fs-extra": "^9.0.1",
"globby": "11.1.0",
"terminal-link": "^2.1.1"

View File

@@ -128,9 +128,4 @@ describe('createProject', () => {
})
})
})
describe('Templates', () => {
it.todo('Verify that all templates are valid')
// Loop through all templates.ts that should have replacement comments, and verify that they are present
})
})

View File

@@ -25,7 +25,7 @@ import { wrapNextConfig } from './wrap-next-config.js'
type InitNextArgs = Pick<CliArgs, '--debug'> & {
dbType: DbType
nextConfigPath: string
nextAppDetails?: NextAppDetails
packageManager: PackageManager
projectDir: string
useDistFiles?: boolean
@@ -43,24 +43,16 @@ type InitNextResult =
export async function initNext(args: InitNextArgs): Promise<InitNextResult> {
const { dbType: dbType, packageManager, projectDir } = args
const isSrcDir = fs.existsSync(path.resolve(projectDir, 'src'))
const nextAppDetails = args.nextAppDetails || (await getNextAppDetails(projectDir))
// Get app directory. Could be top-level or src/app
const nextAppDir = (
await globby(['**/app'], {
absolute: true,
cwd: projectDir,
onlyDirectories: true,
})
)?.[0]
const { hasTopLevelLayout, isSrcDir, nextAppDir } =
nextAppDetails || (await getNextAppDetails(projectDir))
if (!nextAppDir) {
return { isSrcDir, reason: `Could not find app directory in ${projectDir}`, success: false }
}
// Check for top-level layout.tsx
const layoutPath = path.resolve(nextAppDir, 'layout.tsx')
if (fs.existsSync(layoutPath)) {
if (hasTopLevelLayout) {
// Output directions for user to move all files from app to top-level directory named `(app)`
p.log.warn(moveMessage({ nextAppDir, projectDir }))
return {
@@ -76,8 +68,7 @@ export async function initNext(args: InitNextArgs): Promise<InitNextResult> {
const configurationResult = installAndConfigurePayload({
...args,
isSrcDir,
nextAppDir,
nextAppDetails,
useDistFiles: true, // Requires running 'pnpm pack-template-files' in cpa
})
@@ -115,7 +106,10 @@ async function addPayloadConfigToTsConfig(projectDir: string, isSrcDir: boolean)
userTsConfig.compilerOptions = {}
}
if (!userTsConfig.compilerOptions.paths?.['@payload-config']) {
if (
!userTsConfig.compilerOptions?.paths?.['@payload-config'] &&
userTsConfig.compilerOptions?.paths
) {
userTsConfig.compilerOptions.paths = {
...(userTsConfig.compilerOptions.paths || {}),
'@payload-config': [`./${isSrcDir ? 'src/' : ''}payload.config.ts`],
@@ -125,14 +119,23 @@ async function addPayloadConfigToTsConfig(projectDir: string, isSrcDir: boolean)
}
function installAndConfigurePayload(
args: InitNextArgs & {
isSrcDir: boolean
nextAppDir: string
},
args: InitNextArgs & { nextAppDetails: NextAppDetails; useDistFiles?: boolean },
):
| { payloadConfigPath: string; success: true }
| { payloadConfigPath?: string; reason: string; success: false } {
const { '--debug': debug, isSrcDir, nextAppDir, nextConfigPath, projectDir, useDistFiles } = args
const {
'--debug': debug,
nextAppDetails: { isSrcDir, nextAppDir, nextConfigPath } = {},
projectDir,
useDistFiles,
} = args
if (!nextAppDir || !nextConfigPath) {
return {
reason: 'Could not find app directory or next.config.js',
success: false,
}
}
const logDebug = (message: string) => {
if (debug) origDebug(message)
@@ -164,7 +167,6 @@ function installAndConfigurePayload(
logDebug(`Copying template files from ${templateFilesPath} to ${nextAppDir}`)
const templateSrcDir = path.resolve(templateFilesPath, isSrcDir ? '' : 'src')
// const templateSrcDir = path.resolve(templateFilesPath)
logDebug(`templateSrcDir: ${templateSrcDir}`)
logDebug(`nextAppDir: ${nextAppDir}`)
@@ -218,3 +220,43 @@ async function installDeps(projectDir: string, packageManager: PackageManager, d
return { success: exitCode === 0 }
}
type NextAppDetails = {
hasTopLevelLayout: boolean
isSrcDir: boolean
nextAppDir?: string
nextConfigPath?: string
}
export async function getNextAppDetails(projectDir: string): Promise<NextAppDetails> {
const isSrcDir = fs.existsSync(path.resolve(projectDir, 'src'))
const nextConfigPath: string | undefined = (
await globby('next.config.*js', { absolute: true, cwd: projectDir })
)?.[0]
if (!nextConfigPath || nextConfigPath.length === 0) {
return {
hasTopLevelLayout: false,
isSrcDir,
nextConfigPath: undefined,
}
}
let nextAppDir: string | undefined = (
await globby(['**/app'], {
absolute: true,
cwd: projectDir,
onlyDirectories: true,
})
)?.[0]
if (!nextAppDir || nextAppDir.length === 0) {
nextAppDir = undefined
}
const hasTopLevelLayout = nextAppDir
? fs.existsSync(path.resolve(nextAppDir, 'layout.tsx'))
: false
return { hasTopLevelLayout, isSrcDir, nextAppDir, nextConfigPath }
}

View File

@@ -40,10 +40,10 @@ export function parseAndModifyConfigContent(content: string): {
throw new Error('Could not find ExportDefaultDeclaration in next.config.js')
}
if (exportDefaultDeclaration) {
if (exportDefaultDeclaration && exportDefaultDeclaration.declaration?.loc) {
const modifiedConfigContent = insertBeforeAndAfter(
content,
exportDefaultDeclaration.declaration?.loc,
exportDefaultDeclaration.declaration.loc,
)
return { modifiedConfigContent, success: true }
} else if (exportNamedDeclaration) {
@@ -65,13 +65,13 @@ export function parseAndModifyConfigContent(content: string): {
success: false,
}
}
} else {
warning('Could not automatically wrap next.config.js with withPayload.')
warnUserWrapNotSuccessful()
return {
modifiedConfigContent: content,
success: false,
}
}
warning('Could not automatically wrap next.config.js with withPayload.')
warnUserWrapNotSuccessful()
return {
modifiedConfigContent: content,
success: false,
}
}

View File

@@ -11,7 +11,7 @@ export async function writeEnvFile(args: {
databaseUri: string
payloadSecret: string
projectDir: string
template: ProjectTemplate
template?: ProjectTemplate
}): Promise<void> {
const { cliArgs, databaseUri, payloadSecret, projectDir, template } = args
@@ -21,7 +21,7 @@ export async function writeEnvFile(args: {
}
try {
if (template.type === 'starter' && fs.existsSync(path.join(projectDir, '.env.example'))) {
if (template?.type === 'starter' && fs.existsSync(path.join(projectDir, '.env.example'))) {
// Parse .env file into key/value pairs
const envFile = await fs.readFile(path.join(projectDir, '.env.example'), 'utf8')
const envWithValues: string[] = envFile
@@ -47,7 +47,7 @@ export async function writeEnvFile(args: {
// Write new .env file
await fs.writeFile(path.join(projectDir, '.env'), envWithValues.join('\n'))
} else {
const content = `MONGODB_URI=${databaseUri}\nPAYLOAD_SECRET=${payloadSecret}`
const content = `DATABASE_URI=${databaseUri}\nPAYLOAD_SECRET=${payloadSecret}`
await fs.outputFile(`${projectDir}/.env`, content)
}
} catch (err: unknown) {

View File

@@ -2,8 +2,9 @@ import * as p from '@clack/prompts'
import slugify from '@sindresorhus/slugify'
import arg from 'arg'
import chalk from 'chalk'
// @ts-expect-error no types
import { detect } from 'detect-package-manager'
import globby from 'globby'
import figures from 'figures'
import path from 'path'
import type { CliArgs, PackageManager } from './types.js'
@@ -11,14 +12,20 @@ import type { CliArgs, PackageManager } from './types.js'
import { configurePayloadConfig } from './lib/configure-payload-config.js'
import { createProject } from './lib/create-project.js'
import { generateSecret } from './lib/generate-secret.js'
import { initNext } from './lib/init-next.js'
import { getNextAppDetails, initNext } from './lib/init-next.js'
import { parseProjectName } from './lib/parse-project-name.js'
import { parseTemplate } from './lib/parse-template.js'
import { selectDb } from './lib/select-db.js'
import { getValidTemplates, validateTemplate } from './lib/templates.js'
import { writeEnvFile } from './lib/write-env-file.js'
import { error, info } from './utils/log.js'
import { feedbackOutro, helpMessage, successMessage, successfulNextInit } from './utils/messages.js'
import {
feedbackOutro,
helpMessage,
moveMessage,
successMessage,
successfulNextInit,
} from './utils/messages.js'
export class Main {
args: CliArgs
@@ -62,12 +69,6 @@ export class Main {
}
async init(): Promise<void> {
const initContext: {
nextConfigPath: string | undefined
} = {
nextConfigPath: undefined,
}
try {
if (this.args['--help']) {
helpMessage()
@@ -77,15 +78,14 @@ export class Main {
// eslint-disable-next-line no-console
console.log('\n')
p.intro(chalk.bgCyan(chalk.black(' create-payload-app ')))
p.log.message("Welcome to Payload. Let's create a project!")
// Detect if inside Next.js projeckpt
const nextConfigPath = (
await globby('next.config.*js', { absolute: true, cwd: process.cwd() })
)?.[0]
initContext.nextConfigPath = nextConfigPath
p.note("Welcome to Payload. Let's create a project!")
if (initContext.nextConfigPath) {
this.args['--name'] = slugify(path.basename(path.dirname(initContext.nextConfigPath)))
// Detect if inside Next.js project
const nextAppDetails = await getNextAppDetails(process.cwd())
const { hasTopLevelLayout, nextAppDir, nextConfigPath } = nextAppDetails
if (nextConfigPath) {
this.args['--name'] = slugify(path.basename(path.dirname(nextConfigPath)))
}
const projectName = await parseProjectName(this.args)
@@ -96,13 +96,23 @@ export class Main {
const packageManager = await getPackageManager(this.args, projectDir)
if (nextConfigPath) {
// p.note('Detected existing Next.js project.')
p.log.step(chalk.bold('Detected existing Next.js project.'))
p.log.step(
chalk.bold(`${chalk.bgBlack(` ${figures.triangleUp} Next.js `)} project detected!`),
)
const proceed = await p.confirm({
initialValue: true,
message: 'Install Payload in this project?',
message: chalk.bold(`Install ${chalk.green('Payload')} in this project?`),
})
if (p.isCancel(proceed) || !proceed) {
p.outro(feedbackOutro())
process.exit(0)
}
// Check for top-level layout.tsx
if (nextAppDir && hasTopLevelLayout) {
p.log.warn(moveMessage({ nextAppDir, projectDir }))
p.outro(feedbackOutro())
process.exit(0)
}
@@ -111,12 +121,13 @@ export class Main {
const result = await initNext({
...this.args,
dbType: dbDetails.type,
nextConfigPath,
nextAppDetails,
packageManager,
projectDir,
})
if (result.success === false) {
p.outro(feedbackOutro())
process.exit(1)
}
@@ -127,7 +138,14 @@ export class Main {
},
})
info('Payload project successfully created!')
await writeEnvFile({
cliArgs: this.args,
databaseUri: dbDetails.dbUri,
payloadSecret: generateSecret(),
projectDir,
})
info('Payload project successfully initialized!')
p.note(successfulNextInit(), chalk.bgGreen(chalk.black(' Documentation ')))
p.outro(feedbackOutro())
return
@@ -146,6 +164,7 @@ export class Main {
const template = await parseTemplate(this.args, validTemplates)
if (!template) {
p.log.error('Invalid template given')
p.outro(feedbackOutro())
process.exit(1)
}

View File

@@ -7,7 +7,7 @@ import path from 'path'
export function copyRecursiveSync(src: string, dest: string, debug?: boolean) {
const exists = fs.existsSync(src)
const stats = exists && fs.statSync(src)
const isDirectory = exists && stats.isDirectory()
const isDirectory = exists && stats !== false && stats.isDirectory()
if (isDirectory) {
fs.mkdirSync(dest, { recursive: true })
fs.readdirSync(src).forEach((childItemName) => {

View File

@@ -84,18 +84,18 @@ export function moveMessage(args: { nextAppDir: string; projectDir: string }): s
return `
${header('Next Steps:')}
Payload does not support a top-level layout.tsx file in your Next.js app directory.
Payload does not support a top-level layout.tsx file in the app directory.
${chalk.bold('To continue:')}
Move all files from ./${relativePath} to a named directory such as ${chalk.bold('(app)')}
Move all files from ./${relativePath} to a named directory such as ./${relativePath}/${chalk.bold('(app)')}
Once moved, rerun the create-payload-app command again.
`
}
export function feedbackOutro(): string {
return `${chalk.bgCyan(chalk.black(' Have feedback? '))} Visit ${createTerminalLink('GitHub', 'https://github.com/payloadcms/payload')}`
return `${chalk.bgCyan(chalk.black(' Have feedback? '))} Visit us on ${createTerminalLink('GitHub', 'https://github.com/payloadcms/payload')}.`
}
// Create terminalLink with fallback for unsupported terminals

View File

@@ -5,7 +5,8 @@
"noEmit": false /* Do not emit outputs. */,
"emitDeclarationOnly": true,
"outDir": "./dist" /* Specify an output folder for all emitted files. */,
"rootDir": "./src" /* Specify the root folder within your source files. */
"rootDir": "./src" /* Specify the root folder within your source files. */,
"strict": true,
},
"exclude": ["dist", "build", "tests", "test", "node_modules", ".eslintrc.js"],
"include": ["src/**/*.ts", "src/**/*.spec.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"]

13
pnpm-lock.yaml generated
View File

@@ -326,8 +326,8 @@ importers:
specifier: ^5.0.0
version: 5.1.1
figures:
specifier: ^3.2.0
version: 3.2.0
specifier: ^6.1.0
version: 6.1.0
fs-extra:
specifier: ^9.0.1
version: 9.1.0
@@ -10041,6 +10041,14 @@ packages:
engines: {node: '>=8'}
dependencies:
escape-string-regexp: 1.0.5
dev: true
/figures@6.1.0:
resolution: {integrity: sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==}
engines: {node: '>=18'}
dependencies:
is-unicode-supported: 2.0.0
dev: false
/file-entry-cache@6.0.1:
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
@@ -11409,7 +11417,6 @@ packages:
/is-unicode-supported@2.0.0:
resolution: {integrity: sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q==}
engines: {node: '>=18'}
dev: true
/is-weakmap@2.0.1:
resolution: {integrity: sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==}

View File

@@ -4,6 +4,7 @@ import * as CommentJson from 'comment-json'
import { initNext } from 'create-payload-app/commands'
import execa from 'execa'
import fs from 'fs'
import fse from 'fs-extra'
import path from 'path'
import shelljs from 'shelljs'
import tempy from 'tempy'
@@ -66,9 +67,8 @@ describe('create-payload-app', () => {
const firstResult = await initNext({
'--debug': true,
projectDir,
nextConfigPath: path.resolve(projectDir, 'next.config.mjs'),
dbType: 'mongodb',
useDistFiles: true, // create-payload-app/dist/app/(payload)
useDistFiles: true, // create-payload-app/dist/template
packageManager: 'pnpm',
})
@@ -91,7 +91,6 @@ describe('create-payload-app', () => {
const result = await initNext({
'--debug': true,
projectDir,
nextConfigPath: path.resolve(projectDir, 'next.config.mjs'),
dbType: 'mongodb',
useDistFiles: true, // create-payload-app/dist/app/(payload)
packageManager: 'pnpm',
@@ -117,11 +116,22 @@ describe('create-payload-app', () => {
const userTsConfig = CommentJson.parse(userTsConfigContent) as {
compilerOptions?: CompilerOptions
}
// Check that `@payload-config` path is added to tsconfig
expect(userTsConfig.compilerOptions.paths?.['@payload-config']).toStrictEqual([
`./${result.isSrcDir ? 'src/' : ''}payload.config.ts`,
])
// TODO: Start the Next.js app and check if it runs
// Payload dependencies should be installed
const packageJson = fse.readJsonSync(path.resolve(projectDir, 'package.json')) as {
dependencies: Record<string, string>
}
expect(packageJson.dependencies).toMatchObject({
payload: expect.any(String),
'@payloadcms/db-mongodb': expect.any(String),
'@payloadcms/richtext-lexical': expect.any(String),
'@payloadcms/next': expect.any(String),
})
})
})
})