chore: implement AST parsing of next config

This commit is contained in:
Elliot DeNolf
2024-03-25 23:26:05 -04:00
parent 0651daa1d4
commit 5cf49aa166
6 changed files with 128 additions and 15 deletions

View File

@@ -27,6 +27,7 @@
"comment-json": "^4.2.3",
"degit": "^2.8.4",
"detect-package-manager": "^3.0.1",
"esprima": "^4.0.1",
"execa": "^5.0.0",
"figures": "^3.2.0",
"fs-extra": "^9.0.1",
@@ -39,9 +40,17 @@
"devDependencies": {
"@types/command-exists": "^1.2.0",
"@types/degit": "^2.8.3",
"@types/esprima": "^4.0.6",
"@types/fs-extra": "^9.0.12",
"@types/jest": "^27.0.3",
"@types/node": "^16.6.2",
"@types/prompts": "^2.4.1"
},
"exports": {
"./commands": {
"import": "./src/lib/init-next.ts",
"require": "./src/lib/init-next.ts",
"types": "./src/lib/init-next.ts"
}
}
}

View File

@@ -0,0 +1,38 @@
import { parseAndInsertWithPayload } from './wrap-next-config'
const defaultNextConfig = `/** @type {import('next').NextConfig} */
const nextConfig = {};
export default nextConfig;
`
const nextConfigWithFunc = `const nextConfig = {
// Your Next.js config here
}
export default someFunc(nextConfig)
`
const nextConfigWithFuncMultiline = `const nextConfig = {
// Your Next.js config here
}
export default someFunc(
nextConfig
)
`
describe('parseAndInsertWithPayload', () => {
it('should parse the default next config', () => {
const modifiedConfig = parseAndInsertWithPayload(defaultNextConfig)
expect(modifiedConfig).toMatch(/withPayload\(nextConfig\)/)
})
it('should parse the config with a function', () => {
const modifiedConfig = parseAndInsertWithPayload(nextConfigWithFunc)
expect(modifiedConfig).toMatch(/withPayload\(someFunc\(nextConfig\)\)/)
})
it('should parse the config with a function on a new line', () => {
const modifiedConfig = parseAndInsertWithPayload(nextConfigWithFuncMultiline)
expect(modifiedConfig).toMatch(/withPayload\(someFunc\(\n nextConfig\n\)\)/)
})
})

View File

@@ -0,0 +1,60 @@
import { parseModule } from 'esprima'
import fs from 'fs'
import globby from 'globby'
import path from 'path'
export const wrapNextConfig = async (args: { projectDir: string }): Promise<void> => {
const foundConfig = (await globby('next.config.*js', { cwd: args.projectDir }))?.[0]
if (!foundConfig) {
throw new Error(`No next.config.js found at ${args.projectDir}`)
}
const configPath = path.resolve(args.projectDir, foundConfig)
const configContent = fs.readFileSync(configPath, 'utf8')
const newConfig = parseAndInsertWithPayload(configContent)
fs.writeFileSync(configPath, newConfig)
}
export function parseAndInsertWithPayload(content: string) {
const ast = parseModule(content, { loc: true })
const statement = ast.body.find((p) => p.type === 'ExportDefaultDeclaration') as
| Directive
| undefined
if (!statement) {
throw new Error('Could not find ExportDefaultDeclaration in next.config.js')
}
return insertBeforeAndAfter(content, statement.declaration?.loc)
}
type Directive = {
declaration?: {
loc: Loc
}
}
type Loc = {
end: { column: number; line: number }
start: { column: number; line: number }
}
function insertBeforeAndAfter(content: string, loc: Loc) {
const { end, start } = loc
const lines = content.split('\n')
const insert = (line: string, column: number, text: string) => {
return line.slice(0, column) + text + line.slice(column)
}
// insert ) after end
lines[end.line - 1] = insert(lines[end.line - 1], end.column, ')')
// insert withPayload before start
if (start.line === end.line) {
lines[end.line - 1] = insert(lines[end.line - 1], start.column, 'withPayload(')
} else {
lines[start.line - 1] = insert(lines[start.line - 1], start.column, 'withPayload(')
}
return lines.join('\n')
}

View File

@@ -1,7 +1,8 @@
/* eslint-disable no-console */
import slugify from '@sindresorhus/slugify'
import arg from 'arg'
import commandExists from 'command-exists'
import { detect } from 'detect-package-manager'
import path from 'path'
import type { CliArgs, PackageManager } from './types.js'
@@ -84,8 +85,9 @@ export class Main {
const validTemplates = getValidTemplates()
const template = await parseTemplate(this.args, validTemplates)
const projectDir = projectName === '.' ? process.cwd() : `./${slugify(projectName)}`
const packageManager = await getPackageManager(this.args)
const projectDir =
projectName === '.' ? path.basename(process.cwd()) : `./${slugify(projectName)}`
const packageManager = await getPackageManager(this.args, projectDir)
if (template.type !== 'plugin') {
const dbDetails = await selectDb(this.args, projectName)
@@ -126,7 +128,7 @@ export class Main {
}
}
async function getPackageManager(args: CliArgs): Promise<PackageManager> {
async function getPackageManager(args: CliArgs, projectDir: string): Promise<PackageManager> {
let packageManager: PackageManager = 'npm'
if (args['--use-npm']) {
@@ -136,15 +138,8 @@ async function getPackageManager(args: CliArgs): Promise<PackageManager> {
} else if (args['--use-pnpm']) {
packageManager = 'pnpm'
} else {
try {
if (await commandExists('yarn')) {
packageManager = 'yarn'
} else if (await commandExists('pnpm')) {
packageManager = 'pnpm'
}
} catch (error: unknown) {
packageManager = 'npm'
}
const detected = await detect({ cwd: projectDir })
packageManager = detected || 'npm'
}
return packageManager
}

12
pnpm-lock.yaml generated
View File

@@ -316,6 +316,9 @@ importers:
detect-package-manager:
specifier: ^3.0.1
version: 3.0.1
esprima:
specifier: ^4.0.1
version: 4.0.1
execa:
specifier: ^5.0.0
version: 5.1.1
@@ -347,6 +350,9 @@ importers:
'@types/degit':
specifier: ^2.8.3
version: 2.8.6
'@types/esprima':
specifier: ^4.0.6
version: 4.0.6
'@types/fs-extra':
specifier: ^9.0.12
version: 9.0.13
@@ -5950,6 +5956,12 @@ packages:
'@types/estree': 1.0.5
'@types/json-schema': 7.0.15
/@types/esprima@4.0.6:
resolution: {integrity: sha512-lIk+kSt9lGv5hxK6aZNjiUEGZqKmOTpmg0tKiJQI+Ow98fLillxsiZNik5+RcP7mXL929KiTH/D9jGtpDlMbVw==}
dependencies:
'@types/estree': 1.0.5
dev: true
/@types/estree@1.0.5:
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}

View File

@@ -1,6 +1,7 @@
import type { CompilerOptions } from 'typescript'
import * as CommentJson from 'comment-json'
import { initNext } from 'create-payload-app/commands'
import execa from 'execa'
import fs from 'fs'
import path from 'path'
@@ -9,8 +10,6 @@ import tempy from 'tempy'
import { fileURLToPath } from 'url'
import { promisify } from 'util'
import { initNext } from '../../packages/create-payload-app/src/lib/init-next.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)