Skip to content

Commit bf374a2

Browse files
authored
feat(payload, richtext-lexical): runtime dependency checks (#6838)
1 parent 223d726 commit bf374a2

File tree

7 files changed

+299
-0
lines changed

7 files changed

+299
-0
lines changed

packages/payload/src/exports/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,8 @@ export { getFileByPath } from '../uploads/getFileByPath.js'
354354

355355
export { commitTransaction } from '../utilities/commitTransaction.js'
356356

357+
export { getDependencies } from '../utilities/dependencies/getDependencies.js'
358+
357359
export { initTransaction } from '../utilities/initTransaction.js'
358360

359361
export { killTransaction } from '../utilities/killTransaction.js'

packages/payload/src/index.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import { validateSchema } from './config/validate.js'
5555
import { consoleEmailAdapter } from './email/consoleEmailAdapter.js'
5656
import { fieldAffectsData } from './fields/config/types.js'
5757
import localGlobalOperations from './globals/operations/local/index.js'
58+
import { getDependencies } from './utilities/dependencies/getDependencies.js'
5859
import flattenFields from './utilities/flattenTopLevelFields.js'
5960
import Logger from './utilities/logger.js'
6061
import { serverInit as serverInitTelemetry } from './utilities/telemetry/events/serverInit.js'
@@ -357,6 +358,61 @@ export class BasePayload<TGeneratedTypes extends GeneratedTypes> {
357358
* @param options
358359
*/
359360
async init(options: InitOptions): Promise<Payload> {
361+
if (process.env.NODE_ENV !== 'production') {
362+
// First load. First check if there are mismatching dependency versions of payload packages
363+
const resolvedDependencies = await getDependencies(dirname, [
364+
'@payloadcms/ui/shared',
365+
'payload',
366+
'@payloadcms/next/utilities',
367+
'@payloadcms/richtext-lexical',
368+
'@payloadcms/richtext-slate',
369+
'@payloadcms/graphql',
370+
'@payloadcms/plugin-cloud',
371+
'@payloadcms/db-mongodb',
372+
'@payloadcms/db-postgres',
373+
'@payloadcms/plugin-form-builder',
374+
'@payloadcms/plugin-nested-docs',
375+
'@payloadcms/plugin-seo',
376+
'@payloadcms/plugin-search',
377+
'@payloadcms/plugin-cloud-storage',
378+
'@payloadcms/plugin-stripe',
379+
'@payloadcms/plugin-zapier',
380+
'@payloadcms/plugin-redirects',
381+
'@payloadcms/plugin-sentry',
382+
'@payloadcms/bundler-webpack',
383+
'@payloadcms/bundler-vite',
384+
'@payloadcms/live-preview',
385+
'@payloadcms/live-preview-react',
386+
'@payloadcms/translations',
387+
'@payloadcms/email-nodemailer',
388+
'@payloadcms/email-resend',
389+
'@payloadcms/storage-azure',
390+
'@payloadcms/storage-s3',
391+
'@payloadcms/storage-gcs',
392+
'@payloadcms/storage-vercel-blob',
393+
'@payloadcms/storage-uploadthing',
394+
])
395+
396+
// Go through each resolved dependency. If any dependency has a mismatching version, throw an error
397+
const foundVersions: {
398+
[version: string]: string
399+
} = {}
400+
for (const [_pkg, { version }] of resolvedDependencies.resolved) {
401+
if (!Object.keys(foundVersions).includes(version)) {
402+
foundVersions[version] = _pkg
403+
}
404+
}
405+
if (Object.keys(foundVersions).length !== 1) {
406+
const formattedVersionsWithPackageNameString = Object.entries(foundVersions)
407+
.map(([version, pkg]) => `${pkg}@${version}`)
408+
.join(', ')
409+
410+
throw new Error(
411+
`Mismatching payload dependency versions found: ${formattedVersionsWithPackageNameString}. All payload and @payloadcms/* packages must have the same version.`,
412+
)
413+
}
414+
}
415+
360416
if (!options?.config) {
361417
throw new Error('Error: the payload config is required to initialize payload.')
362418
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
This source code has been taken and modified from https://github.com/vercel/next.js/blob/41a80533f900467e1b788bd2673abe2dca20be6a/packages/next/src/lib/has-necessary-dependencies.ts
3+
4+
License:
5+
6+
The MIT License (MIT)
7+
8+
Copyright (c) 2024 Vercel, Inc.
9+
10+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
15+
*/
16+
17+
import { findUp } from 'find-up'
18+
import { existsSync, promises as fs } from 'fs'
19+
import { dirname } from 'path'
20+
21+
import { resolveFrom } from './resolveFrom.js'
22+
23+
export type NecessaryDependencies = {
24+
missing: string[]
25+
resolved: Map<
26+
string,
27+
{
28+
path: string
29+
version: string
30+
}
31+
>
32+
}
33+
34+
export async function getDependencies(
35+
baseDir: string,
36+
requiredPackages: string[],
37+
): Promise<NecessaryDependencies> {
38+
const resolutions = new Map<
39+
string,
40+
{
41+
path: string
42+
version: string
43+
}
44+
>()
45+
const missingPackages: string[] = []
46+
47+
await Promise.all(
48+
requiredPackages.map(async (pkg) => {
49+
try {
50+
const pkgPath = await fs.realpath(resolveFrom(baseDir, pkg))
51+
52+
const pkgDir = dirname(pkgPath)
53+
54+
let packageJsonFilePath = null
55+
56+
await findUp('package.json', { type: 'file', cwd: pkgDir }).then((path) => {
57+
packageJsonFilePath = path
58+
})
59+
60+
if (packageJsonFilePath && existsSync(packageJsonFilePath)) {
61+
// parse version
62+
const packageJson = JSON.parse(await fs.readFile(packageJsonFilePath, 'utf8'))
63+
const version = packageJson.version
64+
65+
resolutions.set(pkg, {
66+
path: packageJsonFilePath,
67+
version,
68+
})
69+
} else {
70+
return missingPackages.push(pkg)
71+
}
72+
} catch (_) {
73+
return missingPackages.push(pkg)
74+
}
75+
}),
76+
)
77+
78+
return {
79+
missing: missingPackages,
80+
resolved: resolutions,
81+
}
82+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
/*
2+
This source code has been taken and modified from https://github.com/vercel/next.js/blob/be87132327ea28acd4bf7af09a401bac2374cb64/packages/next/src/lib/is-error.ts
3+
4+
License:
5+
6+
The MIT License (MIT)
7+
8+
Copyright (c) 2024 Vercel, Inc.
9+
10+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
15+
*/
16+
17+
export interface ErrorWithCode extends Error {
18+
code?: number | string
19+
}
20+
21+
export function isError(err: unknown): err is ErrorWithCode {
22+
return typeof err === 'object' && err !== null && 'name' in err && 'message' in err
23+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/*
2+
This source code has been taken from https://github.com/vercel/next.js/blob/39498d604c3b25d92a483153fe648a7ee456fbda/packages/next/src/lib/realpath.ts
3+
4+
License:
5+
6+
The MIT License (MIT)
7+
8+
Copyright (c) 2024 Vercel, Inc.
9+
10+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
15+
*/
16+
import fs from 'fs'
17+
18+
const isWindows = process.platform === 'win32'
19+
20+
// Interesting learning from this, that fs.realpathSync is 70x slower than fs.realpathSync.native:
21+
// https://sun0day.github.io/blog/vite/why-vite4_3-is-faster.html#fs-realpathsync-issue
22+
// https://github.com/nodejs/node/issues/2680
23+
// However, we can't use fs.realpathSync.native on Windows due to behavior differences.
24+
export const realpathSync = isWindows ? fs.realpathSync : fs.realpathSync.native
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/*
2+
This source code has been taken and modified from https://github.com/vercel/next.js/blob/39498d604c3b25d92a483153fe648a7ee456fbda/packages/next/src/lib/resolve-from.ts
3+
4+
License:
5+
6+
The MIT License (MIT)
7+
8+
Copyright (c) 2024 Vercel, Inc.
9+
10+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
15+
*/
16+
17+
// source: https://github.com/sindresorhus/resolve-from
18+
import { createRequire } from 'module'
19+
import path from 'path'
20+
21+
import { isError } from './isError.js'
22+
import { realpathSync } from './realPath.js'
23+
24+
export const resolveFrom = (fromDirectory: string, moduleId: string, silent?: boolean) => {
25+
if (typeof fromDirectory !== 'string') {
26+
throw new TypeError(
27+
`Expected \`fromDir\` to be of type \`string\`, got \`${typeof fromDirectory}\``,
28+
)
29+
}
30+
31+
if (typeof moduleId !== 'string') {
32+
throw new TypeError(
33+
`Expected \`moduleId\` to be of type \`string\`, got \`${typeof moduleId}\``,
34+
)
35+
}
36+
37+
try {
38+
fromDirectory = realpathSync(fromDirectory)
39+
} catch (error: unknown) {
40+
if (isError(error) && error.code === 'ENOENT') {
41+
fromDirectory = path.resolve(fromDirectory)
42+
} else if (silent) {
43+
return
44+
} else {
45+
throw error
46+
}
47+
}
48+
49+
const fromFile = path.join(fromDirectory, 'noop.js')
50+
51+
const require = createRequire(import.meta.url)
52+
53+
const Module = require('module')
54+
55+
const resolveFileName = () => {
56+
return Module._resolveFilename(moduleId, {
57+
id: fromFile,
58+
filename: fromFile,
59+
paths: Module._nodeModulePaths(fromDirectory),
60+
})
61+
}
62+
63+
if (silent) {
64+
try {
65+
return resolveFileName()
66+
} catch (error) {
67+
return
68+
}
69+
}
70+
71+
return resolveFileName()
72+
}

packages/richtext-lexical/src/index.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@ import type {
66
} from 'lexical'
77

88
import { withMergedProps } from '@payloadcms/ui/elements/withMergedProps'
9+
import { fileURLToPath } from 'node:url'
10+
import path from 'path'
911
import {
1012
afterChangeTraverseFields,
1113
afterReadTraverseFields,
1214
beforeChangeTraverseFields,
1315
beforeValidateTraverseFields,
16+
getDependencies,
1417
withNullableJSONSchemaType,
1518
} from 'payload'
1619

@@ -44,8 +47,45 @@ import { richTextValidateHOC } from './validate/index.js'
4447

4548
let defaultSanitizedServerEditorConfig: SanitizedServerEditorConfig = null
4649

50+
const filename = fileURLToPath(import.meta.url)
51+
const dirname = path.dirname(filename)
52+
4753
export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapterProvider {
4854
return async ({ config, isRoot }) => {
55+
if (process.env.NODE_ENV !== 'production') {
56+
const resolvedDependencies = await getDependencies(dirname, [
57+
'lexical',
58+
'@lexical/headless',
59+
'@lexical/link',
60+
'@lexical/list',
61+
'@lexical/mark',
62+
'@lexical/markdown',
63+
'@lexical/react',
64+
'@lexical/rich-text',
65+
'@lexical/selection',
66+
'@lexical/utils',
67+
])
68+
69+
// Go through each resolved dependency. If any dependency has a mismatching version, throw an error
70+
const foundVersions: {
71+
[version: string]: string
72+
} = {}
73+
for (const [_pkg, { version }] of resolvedDependencies.resolved) {
74+
if (!Object.keys(foundVersions).includes(version)) {
75+
foundVersions[version] = _pkg
76+
}
77+
}
78+
if (Object.keys(foundVersions).length !== 1) {
79+
const formattedVersionsWithPackageNameString = Object.entries(foundVersions)
80+
.map(([version, pkg]) => `${pkg}@${version}`)
81+
.join(', ')
82+
83+
throw new Error(
84+
`Mismatching lexical dependency versions found: ${formattedVersionsWithPackageNameString}. All lexical and @lexical/* packages must have the same version.`,
85+
)
86+
}
87+
}
88+
4989
let features: FeatureProviderServer<unknown, unknown>[] = []
5090
let resolvedFeatureMap: ResolvedServerFeatureMap
5191

0 commit comments

Comments
 (0)