feat: add nextjs and react version checks to dependency checker (#7868)
This commit is contained in:
@@ -23,7 +23,7 @@ export default withBundleAnalyzer(
|
||||
env: {
|
||||
PAYLOAD_CORE_DEV: 'true',
|
||||
ROOT_DIR: path.resolve(dirname),
|
||||
PAYLOAD_DISABLE_DEPENDENCY_CHECKER: 'true',
|
||||
PAYLOAD_CI_DEPENDENCY_CHECKER: 'true',
|
||||
},
|
||||
async redirects() {
|
||||
return [
|
||||
|
||||
88
packages/payload/src/checkPayloadDependencies.ts
Normal file
88
packages/payload/src/checkPayloadDependencies.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import type { CustomVersionParser } from './utilities/dependencies/dependencyChecker.js'
|
||||
|
||||
import { checkDependencies } from './utilities/dependencies/dependencyChecker.js'
|
||||
|
||||
const customReactVersionParser: CustomVersionParser = (version) => {
|
||||
const [mainVersion, ...preReleases] = version.split('-')
|
||||
|
||||
if (preReleases?.length === 3) {
|
||||
// Needs different handling, as it's in a format like 19.0.0-rc-06d0b89e-20240801 format
|
||||
const date = preReleases[2]
|
||||
|
||||
const parts = mainVersion.split('.').map(Number)
|
||||
return { parts, preReleases: [date] }
|
||||
}
|
||||
|
||||
const parts = mainVersion.split('.').map(Number)
|
||||
return { parts, preReleases }
|
||||
}
|
||||
|
||||
export async function checkPayloadDependencies() {
|
||||
const dependencies = [
|
||||
'@payloadcms/ui/shared',
|
||||
'payload',
|
||||
'@payloadcms/next/utilities',
|
||||
'@payloadcms/richtext-lexical',
|
||||
'@payloadcms/richtext-slate',
|
||||
'@payloadcms/graphql',
|
||||
'@payloadcms/plugin-cloud',
|
||||
'@payloadcms/db-mongodb',
|
||||
'@payloadcms/db-postgres',
|
||||
'@payloadcms/plugin-form-builder',
|
||||
'@payloadcms/plugin-nested-docs',
|
||||
'@payloadcms/plugin-seo',
|
||||
'@payloadcms/plugin-search',
|
||||
'@payloadcms/plugin-cloud-storage',
|
||||
'@payloadcms/plugin-stripe',
|
||||
'@payloadcms/plugin-zapier',
|
||||
'@payloadcms/plugin-redirects',
|
||||
'@payloadcms/bundler-webpack',
|
||||
'@payloadcms/bundler-vite',
|
||||
'@payloadcms/live-preview',
|
||||
'@payloadcms/live-preview-react',
|
||||
'@payloadcms/translations',
|
||||
'@payloadcms/email-nodemailer',
|
||||
'@payloadcms/email-resend',
|
||||
'@payloadcms/storage-azure',
|
||||
'@payloadcms/storage-s3',
|
||||
'@payloadcms/storage-gcs',
|
||||
'@payloadcms/storage-vercel-blob',
|
||||
'@payloadcms/storage-uploadthing',
|
||||
]
|
||||
|
||||
if (process.env.PAYLOAD_CI_DEPENDENCY_CHECKER !== 'true') {
|
||||
dependencies.push('@payloadcms/plugin-sentry')
|
||||
}
|
||||
|
||||
// First load. First check if there are mismatching dependency versions of payload packages
|
||||
await checkDependencies({
|
||||
dependencyGroups: [
|
||||
{
|
||||
name: 'payload',
|
||||
dependencies,
|
||||
targetVersionDependency: 'payload',
|
||||
},
|
||||
{
|
||||
name: 'react',
|
||||
dependencies: ['react', 'react-dom'],
|
||||
targetVersionDependency: 'react',
|
||||
},
|
||||
],
|
||||
dependencyVersions: {
|
||||
next: {
|
||||
required: false,
|
||||
version: '>=15.0.0-canary.104',
|
||||
},
|
||||
react: {
|
||||
customVersionParser: customReactVersionParser,
|
||||
required: false,
|
||||
version: '>=19.0.0-rc-06d0b89e-20240801',
|
||||
},
|
||||
'react-dom': {
|
||||
customVersionParser: customReactVersionParser,
|
||||
required: false,
|
||||
version: '>=19.0.0-rc-06d0b89e-20240801',
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -57,11 +57,12 @@ import type { TypeWithVersion } from './versions/types.js'
|
||||
import { decrypt, encrypt } from './auth/crypto.js'
|
||||
import { APIKeyAuthentication } from './auth/strategies/apiKey.js'
|
||||
import { JWTAuthentication } from './auth/strategies/jwt.js'
|
||||
import { checkPayloadDependencies } from './checkPayloadDependencies.js'
|
||||
import localOperations from './collections/operations/local/index.js'
|
||||
import { consoleEmailAdapter } from './email/consoleEmailAdapter.js'
|
||||
import { fieldAffectsData } from './fields/config/types.js'
|
||||
import localGlobalOperations from './globals/operations/local/index.js'
|
||||
import { getDependencies } from './utilities/dependencies/getDependencies.js'
|
||||
import { checkDependencies } from './utilities/dependencies/dependencyChecker.js'
|
||||
import flattenFields from './utilities/flattenTopLevelFields.js'
|
||||
import { getLogger } from './utilities/logger.js'
|
||||
import { serverInit as serverInitTelemetry } from './utilities/telemetry/events/serverInit.js'
|
||||
@@ -430,58 +431,7 @@ export class BasePayload {
|
||||
process.env.NODE_ENV !== 'production' &&
|
||||
process.env.PAYLOAD_DISABLE_DEPENDENCY_CHECKER !== 'true'
|
||||
) {
|
||||
// First load. First check if there are mismatching dependency versions of payload packages
|
||||
const resolvedDependencies = await getDependencies(dirname, [
|
||||
'@payloadcms/ui/shared',
|
||||
'payload',
|
||||
'@payloadcms/next/utilities',
|
||||
'@payloadcms/richtext-lexical',
|
||||
'@payloadcms/richtext-slate',
|
||||
'@payloadcms/graphql',
|
||||
'@payloadcms/plugin-cloud',
|
||||
'@payloadcms/db-mongodb',
|
||||
'@payloadcms/db-postgres',
|
||||
'@payloadcms/plugin-form-builder',
|
||||
'@payloadcms/plugin-nested-docs',
|
||||
'@payloadcms/plugin-seo',
|
||||
'@payloadcms/plugin-search',
|
||||
'@payloadcms/plugin-cloud-storage',
|
||||
'@payloadcms/plugin-stripe',
|
||||
'@payloadcms/plugin-zapier',
|
||||
'@payloadcms/plugin-redirects',
|
||||
'@payloadcms/plugin-sentry',
|
||||
'@payloadcms/bundler-webpack',
|
||||
'@payloadcms/bundler-vite',
|
||||
'@payloadcms/live-preview',
|
||||
'@payloadcms/live-preview-react',
|
||||
'@payloadcms/translations',
|
||||
'@payloadcms/email-nodemailer',
|
||||
'@payloadcms/email-resend',
|
||||
'@payloadcms/storage-azure',
|
||||
'@payloadcms/storage-s3',
|
||||
'@payloadcms/storage-gcs',
|
||||
'@payloadcms/storage-vercel-blob',
|
||||
'@payloadcms/storage-uploadthing',
|
||||
])
|
||||
|
||||
// Go through each resolved dependency. If any dependency has a mismatching version, throw an error
|
||||
const foundVersions: {
|
||||
[version: string]: string
|
||||
} = {}
|
||||
for (const [_pkg, { version }] of resolvedDependencies.resolved) {
|
||||
if (!Object.keys(foundVersions).includes(version)) {
|
||||
foundVersions[version] = _pkg
|
||||
}
|
||||
}
|
||||
if (Object.keys(foundVersions).length > 1) {
|
||||
const formattedVersionsWithPackageNameString = Object.entries(foundVersions)
|
||||
.map(([version, pkg]) => `${pkg}@${version}`)
|
||||
.join(', ')
|
||||
|
||||
throw new Error(
|
||||
`Mismatching payload dependency versions found: ${formattedVersionsWithPackageNameString}. All payload and @payloadcms/* packages must have the same version. This is an error with your set-up, caused by you, not a bug in payload. Please go to your package.json and ensure all payload and @payloadcms/* packages have the same version.`,
|
||||
)
|
||||
}
|
||||
await checkPayloadDependencies()
|
||||
}
|
||||
|
||||
this.importMap = options.importMap
|
||||
@@ -1047,6 +997,7 @@ export {
|
||||
deepMergeWithReactComponents,
|
||||
deepMergeWithSourceArrays,
|
||||
} from './utilities/deepMerge.js'
|
||||
export { getDependencies } from './utilities/dependencies/getDependencies.js'
|
||||
export { default as flattenTopLevelFields } from './utilities/flattenTopLevelFields.js'
|
||||
export { formatLabels, formatNames, toWords } from './utilities/formatLabels.js'
|
||||
export { getCollectionIDFieldTypes } from './utilities/getCollectionIDFieldTypes.js'
|
||||
@@ -1061,7 +1012,7 @@ export { mapAsync } from './utilities/mapAsync.js'
|
||||
export { mergeListSearchAndWhere } from './utilities/mergeListSearchAndWhere.js'
|
||||
export { buildVersionCollectionFields } from './versions/buildCollectionFields.js'
|
||||
export { buildVersionGlobalFields } from './versions/buildGlobalFields.js'
|
||||
export { getDependencies }
|
||||
export { checkDependencies }
|
||||
export { versionDefaults } from './versions/defaults.js'
|
||||
export { deleteCollectionVersions } from './versions/deleteCollectionVersions.js'
|
||||
export { enforceMaxVersions } from './versions/enforceMaxVersions.js'
|
||||
|
||||
113
packages/payload/src/utilities/dependencies/dependencyChecker.ts
Normal file
113
packages/payload/src/utilities/dependencies/dependencyChecker.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
import { getDependencies } from '../../index.js'
|
||||
import { compareVersions } from './versionUtils.js'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
export type CustomVersionParser = (version: string) => { parts: number[]; preReleases: string[] }
|
||||
|
||||
export type DependencyCheckerArgs = {
|
||||
/**
|
||||
* Define dependency groups to ensure that all dependencies within that group are on the same version, and that no dependencies in that group with different versions are found
|
||||
*/
|
||||
dependencyGroups?: {
|
||||
dependencies: string[]
|
||||
/**
|
||||
* Name of the dependency group to be displayed in the error message
|
||||
*/
|
||||
name: string
|
||||
targetVersionDependency: string
|
||||
}[]
|
||||
/**
|
||||
* Dependency package names keyed to their required versions. Supports >= (greater or equal than version) as a prefix, or no prefix for the exact version
|
||||
*/
|
||||
dependencyVersions?: {
|
||||
[dependency: string]: {
|
||||
customVersionParser?: CustomVersionParser
|
||||
required?: boolean
|
||||
version?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function checkDependencies({
|
||||
dependencyGroups,
|
||||
dependencyVersions,
|
||||
}: DependencyCheckerArgs): Promise<void> {
|
||||
if (dependencyGroups?.length) {
|
||||
for (const dependencyGroup of dependencyGroups) {
|
||||
const resolvedDependencies = await getDependencies(dirname, dependencyGroup.dependencies)
|
||||
|
||||
// Go through each resolved dependency. If any dependency has a mismatching version, throw an error
|
||||
const foundVersions: {
|
||||
[version: string]: string
|
||||
} = {}
|
||||
for (const [_pkg, { version }] of resolvedDependencies.resolved) {
|
||||
if (!Object.keys(foundVersions).includes(version)) {
|
||||
foundVersions[version] = _pkg
|
||||
}
|
||||
}
|
||||
if (Object.keys(foundVersions).length > 1) {
|
||||
const targetVersion = resolvedDependencies.resolved.get(
|
||||
dependencyGroup.targetVersionDependency,
|
||||
)?.version
|
||||
if (targetVersion) {
|
||||
const formattedVersionsWithPackageNameString = Object.entries(foundVersions)
|
||||
.filter(([version]) => version !== targetVersion)
|
||||
.map(([version, pkg]) => `${pkg}@${version} (Please change this to ${targetVersion})`)
|
||||
.join(', ')
|
||||
throw new Error(
|
||||
`Mismatching "${dependencyGroup.name}" dependency versions found: ${formattedVersionsWithPackageNameString}. All "${dependencyGroup.name}" packages must have the same version. This is an error with your set-up, not a bug in Payload. Please go to your package.json and ensure all "${dependencyGroup.name}" packages have the same version.`,
|
||||
)
|
||||
} else {
|
||||
const formattedVersionsWithPackageNameString = Object.entries(foundVersions)
|
||||
.map(([version, pkg]) => `${pkg}@${version}`)
|
||||
.join(', ')
|
||||
throw new Error(
|
||||
`Mismatching "${dependencyGroup.name}" dependency versions found: ${formattedVersionsWithPackageNameString}. All "${dependencyGroup.name}" packages must have the same version. This is an error with your set-up, not a bug in Payload. Please go to your package.json and ensure all "${dependencyGroup.name}" packages have the same version.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dependencyVersions && Object.keys(dependencyVersions).length) {
|
||||
const resolvedDependencies = await getDependencies(dirname, Object.keys(dependencyVersions))
|
||||
for (const [dependency, settings] of Object.entries(dependencyVersions)) {
|
||||
const resolvedDependency = resolvedDependencies.resolved.get(dependency)
|
||||
if (!resolvedDependency) {
|
||||
if (!settings.required) {
|
||||
continue
|
||||
}
|
||||
throw new Error(`Dependency ${dependency} not found. Please ensure it is installed.`)
|
||||
}
|
||||
|
||||
if (settings.version) {
|
||||
const settingsVersionToCheck = settings.version.startsWith('>=')
|
||||
? settings.version.slice(2)
|
||||
: settings.version
|
||||
|
||||
const versionCompareResult = compareVersions(
|
||||
resolvedDependency.version,
|
||||
settingsVersionToCheck,
|
||||
settings.customVersionParser,
|
||||
)
|
||||
|
||||
if (settings.version.startsWith('>=')) {
|
||||
if (versionCompareResult === 'lower') {
|
||||
throw new Error(
|
||||
`Dependency ${dependency} is on version ${resolvedDependency.version}, but ${settings.version} or greater is required. Please update this dependency.`,
|
||||
)
|
||||
}
|
||||
} else if (versionCompareResult === 'lower' || versionCompareResult === 'greater') {
|
||||
throw new Error(
|
||||
`Dependency ${dependency} is on version ${resolvedDependency.version}, but ${settings.version} is required. Please update this dependency.`,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
75
packages/payload/src/utilities/dependencies/versionUtils.ts
Normal file
75
packages/payload/src/utilities/dependencies/versionUtils.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
import type { CustomVersionParser } from './dependencyChecker.js'
|
||||
|
||||
export function parseVersion(version: string): { parts: number[]; preReleases: string[] } {
|
||||
const [mainVersion, ...preReleases] = version.split('-')
|
||||
const parts = mainVersion.split('.').map(Number)
|
||||
return { parts, preReleases }
|
||||
}
|
||||
|
||||
function extractNumbers(str: string): number[] {
|
||||
const matches = str.match(/\d+/g) || []
|
||||
return matches.map(Number)
|
||||
}
|
||||
|
||||
function comparePreRelease(v1: string, v2: string): number {
|
||||
const num1 = extractNumbers(v1)
|
||||
const num2 = extractNumbers(v2)
|
||||
|
||||
for (let i = 0; i < Math.max(num1.length, num2.length); i++) {
|
||||
if ((num1[i] || 0) < (num2[i] || 0)) return -1
|
||||
if ((num1[i] || 0) > (num2[i] || 0)) return 1
|
||||
}
|
||||
|
||||
// If numeric parts are equal, compare the whole string
|
||||
if (v1 < v2) return -1
|
||||
if (v1 > v2) return 1
|
||||
return 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares two semantic version strings, including handling pre-release identifiers.
|
||||
*
|
||||
* This function first compares the major, minor, and patch components as integers.
|
||||
* If these components are equal, it then moves on to compare pre-release versions.
|
||||
* Pre-release versions are compared first by extracting and comparing any numerical values.
|
||||
* If numerical values are equal, it compares the whole pre-release string lexicographically.
|
||||
*
|
||||
* @param {string} compare - The first version string to compare.
|
||||
* @param {string} to - The second version string to compare.
|
||||
* @param {function} [customVersionParser] - An optional function to parse version strings into parts and pre-releases.
|
||||
* @returns {string} - Returns greater if compare is greater than to, lower if compare is less than to, and equal if they are equal.
|
||||
*/
|
||||
export function compareVersions(
|
||||
compare: string,
|
||||
to: string,
|
||||
customVersionParser?: CustomVersionParser,
|
||||
): 'equal' | 'greater' | 'lower' {
|
||||
const { parts: parts1, preReleases: preReleases1 } = customVersionParser
|
||||
? customVersionParser(compare)
|
||||
: parseVersion(compare)
|
||||
const { parts: parts2, preReleases: preReleases2 } = customVersionParser
|
||||
? customVersionParser(to)
|
||||
: parseVersion(to)
|
||||
|
||||
// Compare main version parts
|
||||
for (let i = 0; i < Math.max(parts1.length, parts2.length); i++) {
|
||||
if ((parts1[i] || 0) > (parts2[i] || 0)) return 'greater'
|
||||
if ((parts1[i] || 0) < (parts2[i] || 0)) return 'lower'
|
||||
}
|
||||
|
||||
// Compare pre-release parts if main versions are equal
|
||||
if (preReleases1?.length || preReleases2?.length) {
|
||||
for (let i = 0; i < Math.max(preReleases1.length, preReleases2.length); i++) {
|
||||
if (!preReleases1[i]) return 'greater'
|
||||
if (!preReleases2[i]) return 'lower'
|
||||
|
||||
const result = comparePreRelease(preReleases1[i], preReleases2[i])
|
||||
if (result !== 0) {
|
||||
return result === 1 ? 'greater' : 'lower'
|
||||
}
|
||||
// Equal => continue for loop to check for next pre-release part
|
||||
}
|
||||
}
|
||||
|
||||
return 'equal'
|
||||
}
|
||||
@@ -5,16 +5,14 @@ import type {
|
||||
SerializedLexicalNode,
|
||||
} from 'lexical'
|
||||
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import path from 'path'
|
||||
import {
|
||||
afterChangeTraverseFields,
|
||||
afterReadTraverseFields,
|
||||
beforeChangeTraverseFields,
|
||||
beforeValidateTraverseFields,
|
||||
checkDependencies,
|
||||
deepCopyObject,
|
||||
deepCopyObjectSimple,
|
||||
getDependencies,
|
||||
withNullableJSONSchemaType,
|
||||
} from 'payload'
|
||||
|
||||
@@ -42,46 +40,32 @@ import { richTextValidateHOC } from './validate/index.js'
|
||||
|
||||
let defaultSanitizedServerEditorConfig: SanitizedServerEditorConfig = null
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapterProvider {
|
||||
return async ({ config, isRoot }) => {
|
||||
if (
|
||||
process.env.NODE_ENV !== 'production' &&
|
||||
process.env.PAYLOAD_DISABLE_DEPENDENCY_CHECKER !== 'true'
|
||||
) {
|
||||
const resolvedDependencies = await getDependencies(dirname, [
|
||||
'lexical',
|
||||
'@lexical/headless',
|
||||
'@lexical/link',
|
||||
'@lexical/list',
|
||||
'@lexical/mark',
|
||||
'@lexical/markdown',
|
||||
'@lexical/react',
|
||||
'@lexical/rich-text',
|
||||
'@lexical/selection',
|
||||
'@lexical/utils',
|
||||
])
|
||||
|
||||
// Go through each resolved dependency. If any dependency has a mismatching version, throw an error
|
||||
const foundVersions: {
|
||||
[version: string]: string
|
||||
} = {}
|
||||
for (const [_pkg, { version }] of resolvedDependencies.resolved) {
|
||||
if (!Object.keys(foundVersions).includes(version)) {
|
||||
foundVersions[version] = _pkg
|
||||
}
|
||||
}
|
||||
if (Object.keys(foundVersions).length > 1) {
|
||||
const formattedVersionsWithPackageNameString = Object.entries(foundVersions)
|
||||
.map(([version, pkg]) => `${pkg}@${version}`)
|
||||
.join(', ')
|
||||
|
||||
throw new Error(
|
||||
`Mismatching lexical dependency versions found: ${formattedVersionsWithPackageNameString}. All lexical and @lexical/* packages must have the same version. This is an error with your set-up, caused by you, not a bug in payload. Please go to your package.json and ensure all lexical and @lexical/* packages have the same version.`,
|
||||
)
|
||||
}
|
||||
await checkDependencies({
|
||||
dependencyGroups: [
|
||||
{
|
||||
name: 'lexical',
|
||||
dependencies: [
|
||||
'lexical',
|
||||
'@lexical/headless',
|
||||
'@lexical/link',
|
||||
'@lexical/list',
|
||||
'@lexical/mark',
|
||||
'@lexical/markdown',
|
||||
'@lexical/react',
|
||||
'@lexical/rich-text',
|
||||
'@lexical/selection',
|
||||
'@lexical/utils',
|
||||
],
|
||||
targetVersionDependency: 'lexical',
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
let features: FeatureProviderServer<any, any, any>[] = []
|
||||
|
||||
@@ -32,7 +32,7 @@ export async function initPayloadE2E({ dirname }: Args): Promise<Result> {
|
||||
|
||||
const port = 3000
|
||||
process.env.PORT = String(port)
|
||||
process.env.PAYLOAD_DISABLE_DEPENDENCY_CHECKER = 'true'
|
||||
process.env.PAYLOAD_CI_DEPENDENCY_CHECKER = 'true'
|
||||
|
||||
const serverURL = `http://localhost:${port}`
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ export async function initPayloadE2ENoConfig<T extends GeneratedTypes<T>>({
|
||||
|
||||
const port = 3000
|
||||
process.env.PORT = String(port)
|
||||
process.env.PAYLOAD_DISABLE_DEPENDENCY_CHECKER = 'true'
|
||||
process.env.PAYLOAD_CI_DEPENDENCY_CHECKER = 'true'
|
||||
|
||||
const serverURL = `http://localhost:${port}`
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ process.env.PAYLOAD_DROP_DATABASE = 'true'
|
||||
process.env.PAYLOAD_PUBLIC_CLOUD_STORAGE_ADAPTER = 's3'
|
||||
|
||||
process.env.NODE_OPTIONS = '--no-deprecation'
|
||||
process.env.PAYLOAD_DISABLE_DEPENDENCY_CHECKER = 'true'
|
||||
process.env.PAYLOAD_CI_DEPENDENCY_CHECKER = 'true'
|
||||
|
||||
const filename = fileURLToPath(import.meta.url)
|
||||
const dirname = path.dirname(filename)
|
||||
|
||||
Reference in New Issue
Block a user