feat: add nextjs and react version checks to dependency checker (#7868)

This commit is contained in:
Alessio Gravili
2024-08-26 17:19:14 -04:00
committed by GitHub
parent ad7a387e19
commit dfb4c8eb4c
9 changed files with 306 additions and 95 deletions

View File

@@ -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 [

View 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',
},
},
})
}

View File

@@ -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'

View 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.`,
)
}
}
}
}
}

View 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'
}

View File

@@ -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>[] = []

View File

@@ -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}`

View File

@@ -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}`

View File

@@ -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)