feat(cpa): support next.config.ts (#7367)
Support new `next.config.ts` config file. Had to do some weird gymnastics around `swc` in order to use it within unit tests. Had to pass through the `parsed.span.end` value of any previous iteration and account for it. Looks to be an open issue here: https://github.com/swc-project/swc/issues/1366 Fixes #7318
This commit is contained in:
@@ -50,6 +50,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@clack/prompts": "^0.7.0",
|
"@clack/prompts": "^0.7.0",
|
||||||
"@sindresorhus/slugify": "^1.1.0",
|
"@sindresorhus/slugify": "^1.1.0",
|
||||||
|
"@swc/core": "^1.6.13",
|
||||||
"arg": "^5.0.0",
|
"arg": "^5.0.0",
|
||||||
"chalk": "^4.1.0",
|
"chalk": "^4.1.0",
|
||||||
"comment-json": "^4.2.3",
|
"comment-json": "^4.2.3",
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ export async function initNext(args: InitNextArgs): Promise<InitNextResult> {
|
|||||||
const installSpinner = p.spinner()
|
const installSpinner = p.spinner()
|
||||||
installSpinner.start('Installing Payload and dependencies...')
|
installSpinner.start('Installing Payload and dependencies...')
|
||||||
|
|
||||||
const configurationResult = installAndConfigurePayload({
|
const configurationResult = await installAndConfigurePayload({
|
||||||
...args,
|
...args,
|
||||||
nextAppDetails,
|
nextAppDetails,
|
||||||
nextConfigType,
|
nextConfigType,
|
||||||
@@ -143,15 +143,16 @@ async function addPayloadConfigToTsConfig(projectDir: string, isSrcDir: boolean)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function installAndConfigurePayload(
|
async function installAndConfigurePayload(
|
||||||
args: {
|
args: {
|
||||||
nextAppDetails: NextAppDetails
|
nextAppDetails: NextAppDetails
|
||||||
nextConfigType: NextConfigType
|
nextConfigType: NextConfigType
|
||||||
useDistFiles?: boolean
|
useDistFiles?: boolean
|
||||||
} & InitNextArgs,
|
} & InitNextArgs,
|
||||||
):
|
): Promise<
|
||||||
| { payloadConfigPath: string; success: true }
|
| { payloadConfigPath: string; success: true }
|
||||||
| { payloadConfigPath?: string; reason: string; success: false } {
|
| { payloadConfigPath?: string; reason: string; success: false }
|
||||||
|
> {
|
||||||
const {
|
const {
|
||||||
'--debug': debug,
|
'--debug': debug,
|
||||||
nextAppDetails: { isSrcDir, nextAppDir, nextConfigPath } = {},
|
nextAppDetails: { isSrcDir, nextAppDir, nextConfigPath } = {},
|
||||||
@@ -212,7 +213,7 @@ function installAndConfigurePayload(
|
|||||||
copyRecursiveSync(templateSrcDir, path.dirname(nextConfigPath), debug)
|
copyRecursiveSync(templateSrcDir, path.dirname(nextConfigPath), debug)
|
||||||
|
|
||||||
// Wrap next.config.js with withPayload
|
// Wrap next.config.js with withPayload
|
||||||
wrapNextConfig({ nextConfigPath, nextConfigType })
|
await wrapNextConfig({ nextConfigPath, nextConfigType })
|
||||||
|
|
||||||
return {
|
return {
|
||||||
payloadConfigPath: path.resolve(nextAppDir, '../payload.config.ts'),
|
payloadConfigPath: path.resolve(nextAppDir, '../payload.config.ts'),
|
||||||
@@ -240,7 +241,7 @@ export async function getNextAppDetails(projectDir: string): Promise<NextAppDeta
|
|||||||
const isSrcDir = fs.existsSync(path.resolve(projectDir, 'src'))
|
const isSrcDir = fs.existsSync(path.resolve(projectDir, 'src'))
|
||||||
|
|
||||||
const nextConfigPath: string | undefined = (
|
const nextConfigPath: string | undefined = (
|
||||||
await globby('next.config.*js', { absolute: true, cwd: projectDir })
|
await globby('next.config.*(t|j)s', { absolute: true, cwd: projectDir })
|
||||||
)?.[0]
|
)?.[0]
|
||||||
|
|
||||||
if (!nextConfigPath || nextConfigPath.length === 0) {
|
if (!nextConfigPath || nextConfigPath.length === 0) {
|
||||||
@@ -286,8 +287,13 @@ export async function getNextAppDetails(projectDir: string): Promise<NextAppDeta
|
|||||||
function getProjectType(args: {
|
function getProjectType(args: {
|
||||||
nextConfigPath: string
|
nextConfigPath: string
|
||||||
packageObj: Record<string, unknown>
|
packageObj: Record<string, unknown>
|
||||||
}): 'cjs' | 'esm' {
|
}): NextConfigType {
|
||||||
const { nextConfigPath, packageObj } = args
|
const { nextConfigPath, packageObj } = args
|
||||||
|
|
||||||
|
if (nextConfigPath.endsWith('.ts')) {
|
||||||
|
return 'ts'
|
||||||
|
}
|
||||||
|
|
||||||
if (nextConfigPath.endsWith('.mjs')) {
|
if (nextConfigPath.endsWith('.mjs')) {
|
||||||
return 'esm'
|
return 'esm'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,35 @@ import { jest } from '@jest/globals'
|
|||||||
|
|
||||||
import { parseAndModifyConfigContent, withPayloadStatement } from './wrap-next-config.js'
|
import { parseAndModifyConfigContent, withPayloadStatement } from './wrap-next-config.js'
|
||||||
|
|
||||||
|
const tsConfigs = {
|
||||||
|
defaultNextConfig: `import type { NextConfig } from "next";
|
||||||
|
|
||||||
|
const nextConfig: NextConfig = {};
|
||||||
|
export default nextConfig;`,
|
||||||
|
|
||||||
|
nextConfigExportNamedDefault: `import type { NextConfig } from "next";
|
||||||
|
const nextConfig: NextConfig = {};
|
||||||
|
const wrapped = someFunc(asdf);
|
||||||
|
export { wrapped as default };
|
||||||
|
`,
|
||||||
|
nextConfigWithFunc: `import type { NextConfig } from "next";
|
||||||
|
const nextConfig: NextConfig = {};
|
||||||
|
export default someFunc(nextConfig);
|
||||||
|
`,
|
||||||
|
nextConfigWithFuncMultiline: `import type { NextConfig } from "next";
|
||||||
|
const nextConfig: NextConfig = {};
|
||||||
|
export default someFunc(
|
||||||
|
nextConfig
|
||||||
|
);
|
||||||
|
`,
|
||||||
|
nextConfigWithSpread: `import type { NextConfig } from "next";
|
||||||
|
const nextConfig: NextConfig = {
|
||||||
|
...someConfig,
|
||||||
|
};
|
||||||
|
export default nextConfig;
|
||||||
|
`,
|
||||||
|
}
|
||||||
|
|
||||||
const esmConfigs = {
|
const esmConfigs = {
|
||||||
defaultNextConfig: `/** @type {import('next').NextConfig} */
|
defaultNextConfig: `/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {};
|
const nextConfig = {};
|
||||||
@@ -52,27 +81,66 @@ module.exports = nextConfig;
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('parseAndInsertWithPayload', () => {
|
describe('parseAndInsertWithPayload', () => {
|
||||||
|
describe('ts', () => {
|
||||||
|
const configType = 'ts'
|
||||||
|
const importStatement = withPayloadStatement[configType]
|
||||||
|
|
||||||
|
it('should parse the default next config', async () => {
|
||||||
|
const { modifiedConfigContent } = await parseAndModifyConfigContent(
|
||||||
|
tsConfigs.defaultNextConfig,
|
||||||
|
configType,
|
||||||
|
)
|
||||||
|
expect(modifiedConfigContent).toContain(importStatement)
|
||||||
|
expect(modifiedConfigContent).toContain('withPayload(nextConfig)')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse the config with a function', async () => {
|
||||||
|
const { modifiedConfigContent: modifiedConfigContent2 } = await parseAndModifyConfigContent(
|
||||||
|
tsConfigs.nextConfigWithFunc,
|
||||||
|
configType,
|
||||||
|
)
|
||||||
|
expect(modifiedConfigContent2).toContain('withPayload(someFunc(nextConfig))')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse the config with a multi-lined function', async () => {
|
||||||
|
const { modifiedConfigContent } = await parseAndModifyConfigContent(
|
||||||
|
tsConfigs.nextConfigWithFuncMultiline,
|
||||||
|
configType,
|
||||||
|
)
|
||||||
|
expect(modifiedConfigContent).toContain(importStatement)
|
||||||
|
expect(modifiedConfigContent).toMatch(/withPayload\(someFunc\(\n {2}nextConfig\n\)\)/)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should parse the config with a spread', async () => {
|
||||||
|
const { modifiedConfigContent } = await parseAndModifyConfigContent(
|
||||||
|
tsConfigs.nextConfigWithSpread,
|
||||||
|
configType,
|
||||||
|
)
|
||||||
|
expect(modifiedConfigContent).toContain(importStatement)
|
||||||
|
expect(modifiedConfigContent).toContain('withPayload(nextConfig)')
|
||||||
|
})
|
||||||
|
})
|
||||||
describe('esm', () => {
|
describe('esm', () => {
|
||||||
const configType = 'esm'
|
const configType = 'esm'
|
||||||
const importStatement = withPayloadStatement[configType]
|
const importStatement = withPayloadStatement[configType]
|
||||||
it('should parse the default next config', () => {
|
it('should parse the default next config', async () => {
|
||||||
const { modifiedConfigContent } = parseAndModifyConfigContent(
|
const { modifiedConfigContent } = await parseAndModifyConfigContent(
|
||||||
esmConfigs.defaultNextConfig,
|
esmConfigs.defaultNextConfig,
|
||||||
configType,
|
configType,
|
||||||
)
|
)
|
||||||
expect(modifiedConfigContent).toContain(importStatement)
|
expect(modifiedConfigContent).toContain(importStatement)
|
||||||
expect(modifiedConfigContent).toContain('withPayload(nextConfig)')
|
expect(modifiedConfigContent).toContain('withPayload(nextConfig)')
|
||||||
})
|
})
|
||||||
it('should parse the config with a function', () => {
|
it('should parse the config with a function', async () => {
|
||||||
const { modifiedConfigContent } = parseAndModifyConfigContent(
|
const { modifiedConfigContent } = await parseAndModifyConfigContent(
|
||||||
esmConfigs.nextConfigWithFunc,
|
esmConfigs.nextConfigWithFunc,
|
||||||
configType,
|
configType,
|
||||||
)
|
)
|
||||||
expect(modifiedConfigContent).toContain('withPayload(someFunc(nextConfig))')
|
expect(modifiedConfigContent).toContain('withPayload(someFunc(nextConfig))')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should parse the config with a function on a new line', () => {
|
it('should parse the config with a multi-lined function', async () => {
|
||||||
const { modifiedConfigContent } = parseAndModifyConfigContent(
|
const { modifiedConfigContent } = await parseAndModifyConfigContent(
|
||||||
esmConfigs.nextConfigWithFuncMultiline,
|
esmConfigs.nextConfigWithFuncMultiline,
|
||||||
configType,
|
configType,
|
||||||
)
|
)
|
||||||
@@ -80,8 +148,8 @@ describe('parseAndInsertWithPayload', () => {
|
|||||||
expect(modifiedConfigContent).toMatch(/withPayload\(someFunc\(\n {2}nextConfig\n\)\)/)
|
expect(modifiedConfigContent).toMatch(/withPayload\(someFunc\(\n {2}nextConfig\n\)\)/)
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should parse the config with a spread', () => {
|
it('should parse the config with a spread', async () => {
|
||||||
const { modifiedConfigContent } = parseAndModifyConfigContent(
|
const { modifiedConfigContent } = await parseAndModifyConfigContent(
|
||||||
esmConfigs.nextConfigWithSpread,
|
esmConfigs.nextConfigWithSpread,
|
||||||
configType,
|
configType,
|
||||||
)
|
)
|
||||||
@@ -90,10 +158,10 @@ describe('parseAndInsertWithPayload', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
// Unsupported: export { wrapped as default }
|
// Unsupported: export { wrapped as default }
|
||||||
it('should give warning with a named export as default', () => {
|
it('should give warning with a named export as default', async () => {
|
||||||
const warnLogSpy = jest.spyOn(p.log, 'warn').mockImplementation(() => {})
|
const warnLogSpy = jest.spyOn(p.log, 'warn').mockImplementation(() => {})
|
||||||
|
|
||||||
const { modifiedConfigContent, success } = parseAndModifyConfigContent(
|
const { modifiedConfigContent, success } = await parseAndModifyConfigContent(
|
||||||
esmConfigs.nextConfigExportNamedDefault,
|
esmConfigs.nextConfigExportNamedDefault,
|
||||||
configType,
|
configType,
|
||||||
)
|
)
|
||||||
@@ -109,39 +177,39 @@ describe('parseAndInsertWithPayload', () => {
|
|||||||
describe('cjs', () => {
|
describe('cjs', () => {
|
||||||
const configType = 'cjs'
|
const configType = 'cjs'
|
||||||
const requireStatement = withPayloadStatement[configType]
|
const requireStatement = withPayloadStatement[configType]
|
||||||
it('should parse the default next config', () => {
|
it('should parse the default next config', async () => {
|
||||||
const { modifiedConfigContent } = parseAndModifyConfigContent(
|
const { modifiedConfigContent } = await parseAndModifyConfigContent(
|
||||||
cjsConfigs.defaultNextConfig,
|
cjsConfigs.defaultNextConfig,
|
||||||
configType,
|
configType,
|
||||||
)
|
)
|
||||||
expect(modifiedConfigContent).toContain(requireStatement)
|
expect(modifiedConfigContent).toContain(requireStatement)
|
||||||
expect(modifiedConfigContent).toContain('withPayload(nextConfig)')
|
expect(modifiedConfigContent).toContain('withPayload(nextConfig)')
|
||||||
})
|
})
|
||||||
it('should parse anonymous default config', () => {
|
it('should parse anonymous default config', async () => {
|
||||||
const { modifiedConfigContent } = parseAndModifyConfigContent(
|
const { modifiedConfigContent } = await parseAndModifyConfigContent(
|
||||||
cjsConfigs.anonConfig,
|
cjsConfigs.anonConfig,
|
||||||
configType,
|
configType,
|
||||||
)
|
)
|
||||||
expect(modifiedConfigContent).toContain(requireStatement)
|
expect(modifiedConfigContent).toContain(requireStatement)
|
||||||
expect(modifiedConfigContent).toContain('withPayload({})')
|
expect(modifiedConfigContent).toContain('withPayload({})')
|
||||||
})
|
})
|
||||||
it('should parse the config with a function', () => {
|
it('should parse the config with a function', async () => {
|
||||||
const { modifiedConfigContent } = parseAndModifyConfigContent(
|
const { modifiedConfigContent } = await parseAndModifyConfigContent(
|
||||||
cjsConfigs.nextConfigWithFunc,
|
cjsConfigs.nextConfigWithFunc,
|
||||||
configType,
|
configType,
|
||||||
)
|
)
|
||||||
expect(modifiedConfigContent).toContain('withPayload(someFunc(nextConfig))')
|
expect(modifiedConfigContent).toContain('withPayload(someFunc(nextConfig))')
|
||||||
})
|
})
|
||||||
it('should parse the config with a function on a new line', () => {
|
it('should parse the config with a multi-lined function', async () => {
|
||||||
const { modifiedConfigContent } = parseAndModifyConfigContent(
|
const { modifiedConfigContent } = await parseAndModifyConfigContent(
|
||||||
cjsConfigs.nextConfigWithFuncMultiline,
|
cjsConfigs.nextConfigWithFuncMultiline,
|
||||||
configType,
|
configType,
|
||||||
)
|
)
|
||||||
expect(modifiedConfigContent).toContain(requireStatement)
|
expect(modifiedConfigContent).toContain(requireStatement)
|
||||||
expect(modifiedConfigContent).toMatch(/withPayload\(someFunc\(\n {2}nextConfig\n\)\)/)
|
expect(modifiedConfigContent).toMatch(/withPayload\(someFunc\(\n {2}nextConfig\n\)\)/)
|
||||||
})
|
})
|
||||||
it('should parse the config with a named export as default', () => {
|
it('should parse the config with a named export as default', async () => {
|
||||||
const { modifiedConfigContent } = parseAndModifyConfigContent(
|
const { modifiedConfigContent } = await parseAndModifyConfigContent(
|
||||||
cjsConfigs.nextConfigExportNamedDefault,
|
cjsConfigs.nextConfigExportNamedDefault,
|
||||||
configType,
|
configType,
|
||||||
)
|
)
|
||||||
@@ -149,8 +217,8 @@ describe('parseAndInsertWithPayload', () => {
|
|||||||
expect(modifiedConfigContent).toContain('withPayload(wrapped)')
|
expect(modifiedConfigContent).toContain('withPayload(wrapped)')
|
||||||
})
|
})
|
||||||
|
|
||||||
it('should parse the config with a spread', () => {
|
it('should parse the config with a spread', async () => {
|
||||||
const { modifiedConfigContent } = parseAndModifyConfigContent(
|
const { modifiedConfigContent } = await parseAndModifyConfigContent(
|
||||||
cjsConfigs.nextConfigWithSpread,
|
cjsConfigs.nextConfigWithSpread,
|
||||||
configType,
|
configType,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,25 +1,27 @@
|
|||||||
import type { Program } from 'esprima-next'
|
import type { ExportDefaultExpression, ModuleItem } from '@swc/core'
|
||||||
|
|
||||||
|
import swc from '@swc/core'
|
||||||
import chalk from 'chalk'
|
import chalk from 'chalk'
|
||||||
import { Syntax, parseModule } from 'esprima-next'
|
import { Syntax, parseModule } from 'esprima-next'
|
||||||
import fs from 'fs'
|
import fs from 'fs'
|
||||||
|
|
||||||
|
import type { NextConfigType } from '../types.js'
|
||||||
|
|
||||||
import { log, warning } from '../utils/log.js'
|
import { log, warning } from '../utils/log.js'
|
||||||
|
|
||||||
export const withPayloadStatement = {
|
export const withPayloadStatement = {
|
||||||
cjs: `const { withPayload } = require('@payloadcms/next/withPayload')\n`,
|
cjs: `const { withPayload } = require("@payloadcms/next/withPayload");`,
|
||||||
esm: `import { withPayload } from '@payloadcms/next/withPayload'\n`,
|
esm: `import { withPayload } from "@payloadcms/next/withPayload";`,
|
||||||
|
ts: `import { withPayload } from "@payloadcms/next/withPayload";`,
|
||||||
}
|
}
|
||||||
|
|
||||||
type NextConfigType = 'cjs' | 'esm'
|
export const wrapNextConfig = async (args: {
|
||||||
|
|
||||||
export const wrapNextConfig = (args: {
|
|
||||||
nextConfigPath: string
|
nextConfigPath: string
|
||||||
nextConfigType: NextConfigType
|
nextConfigType: NextConfigType
|
||||||
}) => {
|
}) => {
|
||||||
const { nextConfigPath, nextConfigType: configType } = args
|
const { nextConfigPath, nextConfigType: configType } = args
|
||||||
const configContent = fs.readFileSync(nextConfigPath, 'utf8')
|
const configContent = fs.readFileSync(nextConfigPath, 'utf8')
|
||||||
const { modifiedConfigContent: newConfig, success } = parseAndModifyConfigContent(
|
const { modifiedConfigContent: newConfig, success } = await parseAndModifyConfigContent(
|
||||||
configContent,
|
configContent,
|
||||||
configType,
|
configType,
|
||||||
)
|
)
|
||||||
@@ -34,27 +36,45 @@ export const wrapNextConfig = (args: {
|
|||||||
/**
|
/**
|
||||||
* Parses config content with AST and wraps it with withPayload function
|
* Parses config content with AST and wraps it with withPayload function
|
||||||
*/
|
*/
|
||||||
export function parseAndModifyConfigContent(
|
export async function parseAndModifyConfigContent(
|
||||||
content: string,
|
content: string,
|
||||||
configType: NextConfigType,
|
configType: NextConfigType,
|
||||||
): { modifiedConfigContent: string; success: boolean } {
|
): Promise<{ modifiedConfigContent: string; success: boolean }> {
|
||||||
content = withPayloadStatement[configType] + content
|
content = withPayloadStatement[configType] + '\n' + content
|
||||||
|
|
||||||
let ast: Program | undefined
|
console.log({ configType, content })
|
||||||
|
|
||||||
|
if (configType === 'cjs' || configType === 'esm') {
|
||||||
try {
|
try {
|
||||||
ast = parseModule(content, { loc: true })
|
const ast = parseModule(content, { loc: true })
|
||||||
} catch (error: unknown) {
|
|
||||||
if (error instanceof Error) {
|
if (configType === 'cjs') {
|
||||||
warning(`Unable to parse Next config. Error: ${error.message} `)
|
// Find `module.exports = X`
|
||||||
warnUserWrapNotSuccessful(configType)
|
const moduleExports = ast.body.find(
|
||||||
|
(p) =>
|
||||||
|
p.type === Syntax.ExpressionStatement &&
|
||||||
|
p.expression?.type === Syntax.AssignmentExpression &&
|
||||||
|
p.expression.left?.type === Syntax.MemberExpression &&
|
||||||
|
p.expression.left.object?.type === Syntax.Identifier &&
|
||||||
|
p.expression.left.object.name === 'module' &&
|
||||||
|
p.expression.left.property?.type === Syntax.Identifier &&
|
||||||
|
p.expression.left.property.name === 'exports',
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
) as any
|
||||||
|
|
||||||
|
if (moduleExports && moduleExports.expression.right?.loc) {
|
||||||
|
const modifiedConfigContent = insertBeforeAndAfter(
|
||||||
|
content,
|
||||||
|
moduleExports.expression.right.loc,
|
||||||
|
)
|
||||||
|
return { modifiedConfigContent, success: true }
|
||||||
}
|
}
|
||||||
return {
|
|
||||||
|
return Promise.resolve({
|
||||||
modifiedConfigContent: content,
|
modifiedConfigContent: content,
|
||||||
success: false,
|
success: false,
|
||||||
}
|
})
|
||||||
}
|
} else if (configType === 'esm') {
|
||||||
|
|
||||||
if (configType === 'esm') {
|
|
||||||
const exportDefaultDeclaration = ast.body.find(
|
const exportDefaultDeclaration = ast.body.find(
|
||||||
(p) => p.type === Syntax.ExportDefaultDeclaration,
|
(p) => p.type === Syntax.ExportDefaultDeclaration,
|
||||||
) as Directive | undefined
|
) as Directive | undefined
|
||||||
@@ -96,51 +116,62 @@ export function parseAndModifyConfigContent(
|
|||||||
|
|
||||||
warning('Could not automatically wrap Next config with withPayload.')
|
warning('Could not automatically wrap Next config with withPayload.')
|
||||||
warnUserWrapNotSuccessful(configType)
|
warnUserWrapNotSuccessful(configType)
|
||||||
|
return Promise.resolve({
|
||||||
|
modifiedConfigContent: content,
|
||||||
|
success: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (error: unknown) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
warning(`Unable to parse Next config. Error: ${error.message} `)
|
||||||
|
warnUserWrapNotSuccessful(configType)
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
modifiedConfigContent: content,
|
modifiedConfigContent: content,
|
||||||
success: false,
|
success: false,
|
||||||
}
|
}
|
||||||
} else if (configType === 'cjs') {
|
}
|
||||||
// Find `module.exports = X`
|
} else if (configType === 'ts') {
|
||||||
const moduleExports = ast.body.find(
|
const { moduleItems, parseOffset } = await compileTypeScriptFileToAST(content)
|
||||||
(p) =>
|
|
||||||
p.type === Syntax.ExpressionStatement &&
|
|
||||||
p.expression?.type === Syntax.AssignmentExpression &&
|
|
||||||
p.expression.left?.type === Syntax.MemberExpression &&
|
|
||||||
p.expression.left.object?.type === Syntax.Identifier &&
|
|
||||||
p.expression.left.object.name === 'module' &&
|
|
||||||
p.expression.left.property?.type === Syntax.Identifier &&
|
|
||||||
p.expression.left.property.name === 'exports',
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
||||||
) as any
|
|
||||||
|
|
||||||
if (moduleExports && moduleExports.expression.right?.loc) {
|
const exportDefaultDeclaration = moduleItems.find(
|
||||||
const modifiedConfigContent = insertBeforeAndAfter(
|
(m) =>
|
||||||
|
m.type === 'ExportDefaultExpression' &&
|
||||||
|
(m.expression.type === 'Identifier' || m.expression.type === 'CallExpression'),
|
||||||
|
) as ExportDefaultExpression | undefined
|
||||||
|
|
||||||
|
if (exportDefaultDeclaration) {
|
||||||
|
if (!('span' in exportDefaultDeclaration.expression)) {
|
||||||
|
warning('Could not automatically wrap Next config with withPayload.')
|
||||||
|
warnUserWrapNotSuccessful(configType)
|
||||||
|
return Promise.resolve({
|
||||||
|
modifiedConfigContent: content,
|
||||||
|
success: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const modifiedConfigContent = insertBeforeAndAfterSWC(
|
||||||
content,
|
content,
|
||||||
moduleExports.expression.right.loc,
|
exportDefaultDeclaration.expression.span,
|
||||||
|
parseOffset,
|
||||||
)
|
)
|
||||||
return { modifiedConfigContent, success: true }
|
return { modifiedConfigContent, success: true }
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
|
||||||
modifiedConfigContent: content,
|
|
||||||
success: false,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
warning('Could not automatically wrap Next config with withPayload.')
|
warning('Could not automatically wrap Next config with withPayload.')
|
||||||
warnUserWrapNotSuccessful(configType)
|
warnUserWrapNotSuccessful(configType)
|
||||||
return {
|
return Promise.resolve({
|
||||||
modifiedConfigContent: content,
|
modifiedConfigContent: content,
|
||||||
success: false,
|
success: false,
|
||||||
}
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function warnUserWrapNotSuccessful(configType: NextConfigType) {
|
function warnUserWrapNotSuccessful(configType: NextConfigType) {
|
||||||
// Output directions for user to update next.config.js
|
// Output directions for user to update next.config.js
|
||||||
const withPayloadMessage = `
|
const withPayloadMessage = `
|
||||||
|
|
||||||
${chalk.bold(`Please manually wrap your existing next.config.js with the withPayload function. Here is an example:`)}
|
${chalk.bold(`Please manually wrap your existing Next config with the withPayload function. Here is an example:`)}
|
||||||
|
|
||||||
${withPayloadStatement[configType]}
|
${withPayloadStatement[configType]}
|
||||||
|
|
||||||
@@ -148,7 +179,7 @@ function warnUserWrapNotSuccessful(configType: NextConfigType) {
|
|||||||
// Your Next.js config here
|
// Your Next.js config here
|
||||||
}
|
}
|
||||||
|
|
||||||
${configType === 'esm' ? 'export default withPayload(nextConfig)' : 'module.exports = withPayload(nextConfig)'}
|
${configType === 'cjs' ? 'module.exports = withPayload(nextConfig)' : 'export default withPayload(nextConfig)'}
|
||||||
|
|
||||||
`
|
`
|
||||||
|
|
||||||
@@ -186,7 +217,7 @@ type Loc = {
|
|||||||
start: { column: number; line: number }
|
start: { column: number; line: number }
|
||||||
}
|
}
|
||||||
|
|
||||||
function insertBeforeAndAfter(content: string, loc: Loc) {
|
function insertBeforeAndAfter(content: string, loc: Loc): string {
|
||||||
const { end, start } = loc
|
const { end, start } = loc
|
||||||
const lines = content.split('\n')
|
const lines = content.split('\n')
|
||||||
|
|
||||||
@@ -205,3 +236,57 @@ function insertBeforeAndAfter(content: string, loc: Loc) {
|
|||||||
|
|
||||||
return lines.join('\n')
|
return lines.join('\n')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function insertBeforeAndAfterSWC(
|
||||||
|
content: string,
|
||||||
|
span: ModuleItem['span'],
|
||||||
|
/**
|
||||||
|
* WARNING: This is ONLY for unit tests. Defaults to 0 otherwise.
|
||||||
|
*
|
||||||
|
* @see compileTypeScriptFileToAST
|
||||||
|
*/
|
||||||
|
parseOffset: number,
|
||||||
|
): string {
|
||||||
|
const { end: preOffsetEnd, start: preOffsetStart } = span
|
||||||
|
|
||||||
|
const start = preOffsetStart - parseOffset
|
||||||
|
const end = preOffsetEnd - parseOffset
|
||||||
|
|
||||||
|
const insert = (pos: number, text: string): string => {
|
||||||
|
return content.slice(0, pos) + text + content.slice(pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
// insert ) after end
|
||||||
|
content = insert(end - 1, ')')
|
||||||
|
// insert withPayload before start
|
||||||
|
content = insert(start - 1, 'withPayload(')
|
||||||
|
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compile typescript to AST using the swc compiler
|
||||||
|
*/
|
||||||
|
async function compileTypeScriptFileToAST(
|
||||||
|
fileContent: string,
|
||||||
|
): Promise<{ moduleItems: ModuleItem[]; parseOffset: number }> {
|
||||||
|
let parseOffset = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WARNING: This is ONLY for unit tests.
|
||||||
|
*
|
||||||
|
* Multiple instances of swc DO NOT reset the .span.end value.
|
||||||
|
* During unit tests, the .spawn.end value is read and accounted for.
|
||||||
|
*
|
||||||
|
* https://github.com/swc-project/swc/issues/1366
|
||||||
|
*/
|
||||||
|
if (process.env.NODE_ENV === 'test') {
|
||||||
|
parseOffset = (await swc.parse('')).span.end
|
||||||
|
}
|
||||||
|
|
||||||
|
const module = await swc.parse(fileContent, {
|
||||||
|
syntax: 'typescript',
|
||||||
|
})
|
||||||
|
|
||||||
|
return { moduleItems: module.body, parseOffset }
|
||||||
|
}
|
||||||
|
|||||||
@@ -75,6 +75,6 @@ export type NextAppDetails = {
|
|||||||
nextConfigType?: NextConfigType
|
nextConfigType?: NextConfigType
|
||||||
}
|
}
|
||||||
|
|
||||||
export type NextConfigType = 'cjs' | 'esm'
|
export type NextConfigType = 'cjs' | 'esm' | 'ts'
|
||||||
|
|
||||||
export type StorageAdapterType = 'localDisk' | 'payloadCloud' | 'vercelBlobStorage'
|
export type StorageAdapterType = 'localDisk' | 'payloadCloud' | 'vercelBlobStorage'
|
||||||
|
|||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -206,6 +206,9 @@ importers:
|
|||||||
'@sindresorhus/slugify':
|
'@sindresorhus/slugify':
|
||||||
specifier: ^1.1.0
|
specifier: ^1.1.0
|
||||||
version: 1.1.2
|
version: 1.1.2
|
||||||
|
'@swc/core':
|
||||||
|
specifier: ^1.6.13
|
||||||
|
version: 1.6.13
|
||||||
arg:
|
arg:
|
||||||
specifier: ^5.0.0
|
specifier: ^5.0.0
|
||||||
version: 5.0.2
|
version: 5.0.2
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable jest/no-conditional-in-test */
|
||||||
import type { CompilerOptions } from 'typescript'
|
import type { CompilerOptions } from 'typescript'
|
||||||
|
|
||||||
import * as CommentJson from 'comment-json'
|
import * as CommentJson from 'comment-json'
|
||||||
@@ -13,11 +14,12 @@ import { promisify } from 'util'
|
|||||||
const readFile = promisify(fs.readFile)
|
const readFile = promisify(fs.readFile)
|
||||||
const writeFile = promisify(fs.writeFile)
|
const writeFile = promisify(fs.writeFile)
|
||||||
|
|
||||||
const commonNextCreateParams = '--typescript --eslint --no-tailwind --app --import-alias="@/*"'
|
const commonNextCreateParams =
|
||||||
|
'--typescript --eslint --no-tailwind --app --import-alias="@/*" --turbo --yes'
|
||||||
|
|
||||||
const nextCreateCommands: Partial<Record<'noSrcDir' | 'srcDir', string>> = {
|
const nextCreateCommands: Partial<Record<'noSrcDir' | 'srcDir', string>> = {
|
||||||
noSrcDir: `pnpm create next-app@latest . ${commonNextCreateParams} --no-src-dir`,
|
noSrcDir: `pnpm create next-app@canary . ${commonNextCreateParams} --no-src-dir`,
|
||||||
srcDir: `pnpm create next-app@latest . ${commonNextCreateParams} --src-dir`,
|
srcDir: `pnpm create next-app@canary . ${commonNextCreateParams} --src-dir`,
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('create-payload-app', () => {
|
describe('create-payload-app', () => {
|
||||||
@@ -41,7 +43,11 @@ describe('create-payload-app', () => {
|
|||||||
// Create a new Next.js project with default options
|
// Create a new Next.js project with default options
|
||||||
console.log(`Running: ${nextCreateCommands[nextCmdKey]} in ${projectDir}`)
|
console.log(`Running: ${nextCreateCommands[nextCmdKey]} in ${projectDir}`)
|
||||||
const [cmd, ...args] = nextCreateCommands[nextCmdKey].split(' ')
|
const [cmd, ...args] = nextCreateCommands[nextCmdKey].split(' ')
|
||||||
const { exitCode, stderr } = await execa(cmd, [...args], { cwd: projectDir })
|
console.log(`Running: ${cmd} ${args.join(' ')}`)
|
||||||
|
const { exitCode, stderr } = await execa(cmd, [...args], {
|
||||||
|
cwd: projectDir,
|
||||||
|
stdio: 'inherit',
|
||||||
|
})
|
||||||
if (exitCode !== 0) {
|
if (exitCode !== 0) {
|
||||||
console.error({ exitCode, stderr })
|
console.error({ exitCode, stderr })
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user