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", "detect-package-manager": "^3.0.1",
"esprima": "^4.0.1", "esprima": "^4.0.1",
"execa": "^5.0.0", "execa": "^5.0.0",
"figures": "^3.2.0", "figures": "^6.1.0",
"fs-extra": "^9.0.1", "fs-extra": "^9.0.1",
"globby": "11.1.0", "globby": "11.1.0",
"terminal-link": "^2.1.1" "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'> & { type InitNextArgs = Pick<CliArgs, '--debug'> & {
dbType: DbType dbType: DbType
nextConfigPath: string nextAppDetails?: NextAppDetails
packageManager: PackageManager packageManager: PackageManager
projectDir: string projectDir: string
useDistFiles?: boolean useDistFiles?: boolean
@@ -43,24 +43,16 @@ type InitNextResult =
export async function initNext(args: InitNextArgs): Promise<InitNextResult> { export async function initNext(args: InitNextArgs): Promise<InitNextResult> {
const { dbType: dbType, packageManager, projectDir } = args 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 { hasTopLevelLayout, isSrcDir, nextAppDir } =
const nextAppDir = ( nextAppDetails || (await getNextAppDetails(projectDir))
await globby(['**/app'], {
absolute: true,
cwd: projectDir,
onlyDirectories: true,
})
)?.[0]
if (!nextAppDir) { if (!nextAppDir) {
return { isSrcDir, reason: `Could not find app directory in ${projectDir}`, success: false } return { isSrcDir, reason: `Could not find app directory in ${projectDir}`, success: false }
} }
// Check for top-level layout.tsx if (hasTopLevelLayout) {
const layoutPath = path.resolve(nextAppDir, 'layout.tsx')
if (fs.existsSync(layoutPath)) {
// Output directions for user to move all files from app to top-level directory named `(app)` // Output directions for user to move all files from app to top-level directory named `(app)`
p.log.warn(moveMessage({ nextAppDir, projectDir })) p.log.warn(moveMessage({ nextAppDir, projectDir }))
return { return {
@@ -76,8 +68,7 @@ export async function initNext(args: InitNextArgs): Promise<InitNextResult> {
const configurationResult = installAndConfigurePayload({ const configurationResult = installAndConfigurePayload({
...args, ...args,
isSrcDir, nextAppDetails,
nextAppDir,
useDistFiles: true, // Requires running 'pnpm pack-template-files' in cpa useDistFiles: true, // Requires running 'pnpm pack-template-files' in cpa
}) })
@@ -115,7 +106,10 @@ async function addPayloadConfigToTsConfig(projectDir: string, isSrcDir: boolean)
userTsConfig.compilerOptions = {} userTsConfig.compilerOptions = {}
} }
if (!userTsConfig.compilerOptions.paths?.['@payload-config']) { if (
!userTsConfig.compilerOptions?.paths?.['@payload-config'] &&
userTsConfig.compilerOptions?.paths
) {
userTsConfig.compilerOptions.paths = { userTsConfig.compilerOptions.paths = {
...(userTsConfig.compilerOptions.paths || {}), ...(userTsConfig.compilerOptions.paths || {}),
'@payload-config': [`./${isSrcDir ? 'src/' : ''}payload.config.ts`], '@payload-config': [`./${isSrcDir ? 'src/' : ''}payload.config.ts`],
@@ -125,14 +119,23 @@ async function addPayloadConfigToTsConfig(projectDir: string, isSrcDir: boolean)
} }
function installAndConfigurePayload( function installAndConfigurePayload(
args: InitNextArgs & { args: InitNextArgs & { nextAppDetails: NextAppDetails; useDistFiles?: boolean },
isSrcDir: boolean
nextAppDir: string
},
): ):
| { payloadConfigPath: string; success: true } | { payloadConfigPath: string; success: true }
| { payloadConfigPath?: string; reason: string; success: false } { | { 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) => { const logDebug = (message: string) => {
if (debug) origDebug(message) if (debug) origDebug(message)
@@ -164,7 +167,6 @@ function installAndConfigurePayload(
logDebug(`Copying template files from ${templateFilesPath} to ${nextAppDir}`) logDebug(`Copying template files from ${templateFilesPath} to ${nextAppDir}`)
const templateSrcDir = path.resolve(templateFilesPath, isSrcDir ? '' : 'src') const templateSrcDir = path.resolve(templateFilesPath, isSrcDir ? '' : 'src')
// const templateSrcDir = path.resolve(templateFilesPath)
logDebug(`templateSrcDir: ${templateSrcDir}`) logDebug(`templateSrcDir: ${templateSrcDir}`)
logDebug(`nextAppDir: ${nextAppDir}`) logDebug(`nextAppDir: ${nextAppDir}`)
@@ -218,3 +220,43 @@ async function installDeps(projectDir: string, packageManager: PackageManager, d
return { success: exitCode === 0 } 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') throw new Error('Could not find ExportDefaultDeclaration in next.config.js')
} }
if (exportDefaultDeclaration) { if (exportDefaultDeclaration && exportDefaultDeclaration.declaration?.loc) {
const modifiedConfigContent = insertBeforeAndAfter( const modifiedConfigContent = insertBeforeAndAfter(
content, content,
exportDefaultDeclaration.declaration?.loc, exportDefaultDeclaration.declaration.loc,
) )
return { modifiedConfigContent, success: true } return { modifiedConfigContent, success: true }
} else if (exportNamedDeclaration) { } else if (exportNamedDeclaration) {
@@ -65,7 +65,8 @@ export function parseAndModifyConfigContent(content: string): {
success: false, success: false,
} }
} }
} else { }
warning('Could not automatically wrap next.config.js with withPayload.') warning('Could not automatically wrap next.config.js with withPayload.')
warnUserWrapNotSuccessful() warnUserWrapNotSuccessful()
return { return {
@@ -73,7 +74,6 @@ export function parseAndModifyConfigContent(content: string): {
success: false, success: false,
} }
} }
}
function warnUserWrapNotSuccessful() { function warnUserWrapNotSuccessful() {
// Output directions for user to update next.config.js // Output directions for user to update next.config.js

View File

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

View File

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

View File

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

View File

@@ -84,18 +84,18 @@ export function moveMessage(args: { nextAppDir: string; projectDir: string }): s
return ` return `
${header('Next Steps:')} ${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:')} ${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. Once moved, rerun the create-payload-app command again.
` `
} }
export function feedbackOutro(): string { 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 // Create terminalLink with fallback for unsupported terminals

View File

@@ -5,7 +5,8 @@
"noEmit": false /* Do not emit outputs. */, "noEmit": false /* Do not emit outputs. */,
"emitDeclarationOnly": true, "emitDeclarationOnly": true,
"outDir": "./dist" /* Specify an output folder for all emitted files. */, "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"], "exclude": ["dist", "build", "tests", "test", "node_modules", ".eslintrc.js"],
"include": ["src/**/*.ts", "src/**/*.spec.ts", "src/**/*.tsx", "src/**/*.d.ts", "src/**/*.json"] "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 specifier: ^5.0.0
version: 5.1.1 version: 5.1.1
figures: figures:
specifier: ^3.2.0 specifier: ^6.1.0
version: 3.2.0 version: 6.1.0
fs-extra: fs-extra:
specifier: ^9.0.1 specifier: ^9.0.1
version: 9.1.0 version: 9.1.0
@@ -10041,6 +10041,14 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
dependencies: dependencies:
escape-string-regexp: 1.0.5 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: /file-entry-cache@6.0.1:
resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==} resolution: {integrity: sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==}
@@ -11409,7 +11417,6 @@ packages:
/is-unicode-supported@2.0.0: /is-unicode-supported@2.0.0:
resolution: {integrity: sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q==} resolution: {integrity: sha512-FRdAyx5lusK1iHG0TWpVtk9+1i+GjrzRffhDg4ovQ7mcidMQ6mj+MhKPmvh7Xwyv5gIS06ns49CA7Sqg7lC22Q==}
engines: {node: '>=18'} engines: {node: '>=18'}
dev: true
/is-weakmap@2.0.1: /is-weakmap@2.0.1:
resolution: {integrity: sha512-NSBR4kH5oVj1Uwvv970ruUkCV7O1mzgVFO4/rev2cLRda9Tm9HrL70ZPut4rOHgY0FNrUu9BCbXA2sdQ+x0chA==} 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 { initNext } from 'create-payload-app/commands'
import execa from 'execa' import execa from 'execa'
import fs from 'fs' import fs from 'fs'
import fse from 'fs-extra'
import path from 'path' import path from 'path'
import shelljs from 'shelljs' import shelljs from 'shelljs'
import tempy from 'tempy' import tempy from 'tempy'
@@ -66,9 +67,8 @@ describe('create-payload-app', () => {
const firstResult = await initNext({ const firstResult = await initNext({
'--debug': true, '--debug': true,
projectDir, projectDir,
nextConfigPath: path.resolve(projectDir, 'next.config.mjs'),
dbType: 'mongodb', dbType: 'mongodb',
useDistFiles: true, // create-payload-app/dist/app/(payload) useDistFiles: true, // create-payload-app/dist/template
packageManager: 'pnpm', packageManager: 'pnpm',
}) })
@@ -91,7 +91,6 @@ describe('create-payload-app', () => {
const result = await initNext({ const result = await initNext({
'--debug': true, '--debug': true,
projectDir, projectDir,
nextConfigPath: path.resolve(projectDir, 'next.config.mjs'),
dbType: 'mongodb', dbType: 'mongodb',
useDistFiles: true, // create-payload-app/dist/app/(payload) useDistFiles: true, // create-payload-app/dist/app/(payload)
packageManager: 'pnpm', packageManager: 'pnpm',
@@ -117,11 +116,22 @@ describe('create-payload-app', () => {
const userTsConfig = CommentJson.parse(userTsConfigContent) as { const userTsConfig = CommentJson.parse(userTsConfigContent) as {
compilerOptions?: CompilerOptions compilerOptions?: CompilerOptions
} }
// Check that `@payload-config` path is added to tsconfig
expect(userTsConfig.compilerOptions.paths?.['@payload-config']).toStrictEqual([ expect(userTsConfig.compilerOptions.paths?.['@payload-config']).toStrictEqual([
`./${result.isSrcDir ? 'src/' : ''}payload.config.ts`, `./${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),
})
}) })
}) })
}) })