Compare commits

...

29 Commits

Author SHA1 Message Date
Alessio Gravili
3545514736 fix config loading 2024-11-25 09:00:22 -07:00
Alessio Gravili
c372c04bcd Merge remote-tracking branch 'origin/perf/clientconfig' into perf/cache-payload-config 2024-11-25 08:31:21 -07:00
Alessio Gravili
9da0158e1f Merge remote-tracking branch 'origin/main' into perf/clientconfig 2024-11-25 08:30:48 -07:00
Alessio Gravili
b534fb4818 fix initPayloadInt config loading 2024-11-24 23:34:45 -07:00
Alessio Gravili
9d0ef67273 perf: cache payload config during dev, ensure sanitization only runs once 2024-11-24 23:20:43 -07:00
Alessio Gravili
f0fecc4d3b chore: add missing void to checkPayloadDependencies 2024-11-24 22:22:07 -07:00
Alessio Gravili
61874272ab small optimization 2024-11-24 22:13:00 -07:00
Alessio Gravili
d4a6735d91 remove console.log 2024-11-24 22:09:21 -07:00
Alessio Gravili
5f8f9f7a61 fix: hidden or disabled fields were included in clientConfig 2024-11-24 21:52:47 -07:00
Alessio Gravili
e264631114 add new e2e test suite that tests new user registration 2024-11-24 21:39:46 -07:00
Alessio Gravili
8409de7196 remove more deep copying in lexical 2024-11-24 20:37:28 -07:00
Alessio Gravili
616de3c1b6 perf(richtext-lexical): remove deep copying in adapter, ensure default editor config is properly cached and only sanitized once for all lexical fields, instead of once per lexical field, as previously this was not cached until the sanitization function finished 2024-11-24 19:11:51 -07:00
Alessio Gravili
a75e426433 Merge remote-tracking branch 'origin/main' into perf/clientconfig 2024-11-23 17:15:26 -07:00
Sasha
815c5b55cb fix: fallback version if not selected 2024-11-23 08:29:44 +02:00
Alessio Gravili
dac7f0bd89 add comment 2024-11-22 22:41:17 -07:00
Alessio Gravili
8cba6c5d45 simplify getVersions 2024-11-22 22:20:12 -07:00
Alessio Gravili
81763b66af perf(next): completely skip query to find out if published document exists, if already-queried document is published 2024-11-22 22:13:46 -07:00
Alessio Gravili
7f881618e8 do the same for globals 2024-11-22 22:09:41 -07:00
Alessio Gravili
c588c7e38a perf(next): speed up version fetching by disabling pagination and limiting returned fields using select 2024-11-22 22:07:59 -07:00
Alessio Gravili
4c6e9a190e fix auth property never added 2024-11-22 21:17:33 -07:00
Alessio Gravili
952fd26849 fix incorrect client tab handling 2024-11-22 21:00:53 -07:00
Alessio Gravili
44e1bd4ba7 turn down shameful performance warning 2024-11-22 20:55:37 -07:00
Alessio Gravili
7aa34535b5 perf: lexical: handle clientField and clientSchemaMap generation in payload and cache it. This now only happens once 2024-11-22 20:51:29 -07:00
Alessio Gravili
8147299a2f perf: remove createClientField call in renderField, cache schemaMap and clientSchemaMap, introduce clientSchemaMap 2024-11-22 18:43:31 -07:00
Alessio Gravili
9c79c51904 fix: skip blocks and tabs from just being copied over 2024-11-22 16:21:38 -07:00
Alessio Gravili
c40ff0173f remove unnecessary awaits 2024-11-22 15:41:14 -07:00
Alessio Gravili
c768fb054e perf: do not await dependency checker. Doesn't matter when the result comes back, we should run it in the background 2024-11-22 15:27:50 -07:00
Alessio Gravili
c5789d6026 perf: properly cache createClientConfig in both prod & dev. It now only runs once, and does not re-run when refreshing the page or navigating. HMR still works despite caching in dev 2024-11-22 15:24:27 -07:00
Alessio Gravili
79e221d306 perf: speed up createClientConfig (4.1s => 50ms with 400 fields) by removing deep copying and building client config from the ground up instead 2024-11-22 13:34:24 -07:00
124 changed files with 1851 additions and 975 deletions

View File

@@ -296,6 +296,7 @@ jobs:
- admin__e2e__3
- admin-root
- auth
- auth-basic
- field-error-states
- fields-relationship
- fields

View File

@@ -17,7 +17,7 @@ const customReactVersionParser: CustomVersionParser = (version) => {
let checkedDependencies = false
export const checkDependencies = async () => {
export const checkDependencies = () => {
if (
process.env.NODE_ENV !== 'production' &&
process.env.PAYLOAD_DISABLE_DEPENDENCY_CHECKER !== 'true' &&
@@ -26,7 +26,7 @@ export const checkDependencies = async () => {
checkedDependencies = true
// First check if there are mismatching dependency versions of next / react packages
await payloadCheckDependencies({
void payloadCheckDependencies({
dependencyGroups: [
{
name: 'react',

View File

@@ -1,14 +1,14 @@
import type { AcceptedLanguages } from '@payloadcms/translations'
import type { ImportMap, SanitizedConfig, ServerFunctionClient } from 'payload'
import type { ConfigImport, ImportMap, SanitizedConfig, ServerFunctionClient } from 'payload'
import { rtlLanguages } from '@payloadcms/translations'
import { RootProvider } from '@payloadcms/ui'
import { getClientConfig } from '@payloadcms/ui/utilities/getClientConfig'
import { headers as getHeaders, cookies as nextCookies } from 'next/headers.js'
import { getPayload, parseCookies } from 'payload'
import { getConfig, getPayload, parseCookies } from 'payload'
import React from 'react'
import { getNavPrefs } from '../../elements/Nav/getNavPrefs.js'
import { getClientConfig } from '../../utilities/getClientConfig.js'
import { getRequestLanguage } from '../../utilities/getRequestLanguage.js'
import { getRequestTheme } from '../../utilities/getRequestTheme.js'
import { initReq } from '../../utilities/initReq.js'
@@ -24,18 +24,18 @@ export const metadata = {
export const RootLayout = async ({
children,
config: configPromise,
config: configImport,
importMap,
serverFunction,
}: {
readonly children: React.ReactNode
readonly config: Promise<SanitizedConfig>
readonly config: ConfigImport
readonly importMap: ImportMap
readonly serverFunction: ServerFunctionClient
}) => {
await checkDependencies()
void checkDependencies()
const config = await configPromise
const config = await getConfig(configImport)
const headers = await getHeaders()
const cookies = parseCookies(headers)
@@ -54,7 +54,7 @@ export const RootLayout = async ({
const payload = await getPayload({ config, importMap })
const { i18n, permissions, req, user } = await initReq(config)
const { i18n, permissions, user } = await initReq(config)
const dir = (rtlLanguages as unknown as AcceptedLanguages[]).includes(languageCode)
? 'RTL'
@@ -86,7 +86,7 @@ export const RootLayout = async ({
const navPrefs = await getNavPrefs({ payload, user })
const clientConfig = await getClientConfig({
const clientConfig = getClientConfig({
config,
i18n,
importMap,

View File

@@ -1,23 +0,0 @@
import type { I18nClient } from '@payloadcms/translations'
import type { ClientConfig, ImportMap, SanitizedConfig } from 'payload'
import { createClientConfig } from 'payload'
import { cache } from 'react'
export const getClientConfig = cache(
async (args: {
config: SanitizedConfig
i18n: I18nClient
importMap: ImportMap
}): Promise<ClientConfig> => {
const { config, i18n, importMap } = args
const clientConfig = createClientConfig({
config,
i18n,
importMap,
})
return Promise.resolve(clientConfig)
},
)

View File

@@ -1,9 +1,9 @@
import type { I18n, I18nClient } from '@payloadcms/translations'
import type { PayloadRequest, SanitizedConfig, SanitizedPermissions, User } from 'payload'
import type { ConfigImport, PayloadRequest, SanitizedPermissions, User } from 'payload'
import { initI18n } from '@payloadcms/translations'
import { headers as getHeaders } from 'next/headers.js'
import { createLocalReq, getPayload, parseCookies } from 'payload'
import { createLocalReq, getConfig, getPayload, parseCookies } from 'payload'
import { cache } from 'react'
import { getRequestLanguage } from './getRequestLanguage.js'
@@ -15,10 +15,9 @@ type Result = {
user: User
}
export const initReq = cache(async function (
configPromise: Promise<SanitizedConfig> | SanitizedConfig,
): Promise<Result> {
const config = await configPromise
export const initReq = cache(async function (configImport: ConfigImport): Promise<Result> {
const config = await getConfig(configImport)
const payload = await getPayload({ config })
const headers = await getHeaders()

View File

@@ -102,6 +102,7 @@ export const Account: React.FC<AdminViewProps> = async ({
await getVersions({
id: user.id,
collectionConfig,
doc: data,
docPermissions,
locale: locale?.code,
payload,

View File

@@ -10,6 +10,13 @@ import { sanitizeID } from '@payloadcms/ui/shared'
type Args = {
collectionConfig?: SanitizedCollectionConfig
/**
* Optional - performance optimization.
* If a document has been fetched before fetching versions, pass it here.
* If this document is set to published, we can skip the query to find out if a published document exists,
* as the passed in document is proof of its existence.
*/
doc?: Record<string, any>
docPermissions: SanitizedDocumentPermissions
globalConfig?: SanitizedGlobalConfig
id?: number | string
@@ -27,9 +34,11 @@ type Result = Promise<{
// TODO: in the future, we can parallelize some of these queries
// this will speed up the API by ~30-100ms or so
// Note from the future: I have attempted parallelizing these queries, but it made this function almost 2x slower.
export const getVersions = async ({
id: idArg,
collectionConfig,
doc,
docPermissions,
globalConfig,
locale,
@@ -37,7 +46,7 @@ export const getVersions = async ({
user,
}: Args): Result => {
const id = sanitizeID(idArg)
let publishedQuery
let publishedDoc
let hasPublishedDoc = false
let mostRecentVersionIsAutosaved = false
let unpublishedVersionCount = 0
@@ -70,37 +79,49 @@ export const getVersions = async ({
}
if (versionsConfig?.drafts) {
publishedQuery = await payload.find({
collection: collectionConfig.slug,
depth: 0,
locale: locale || undefined,
user,
where: {
and: [
{
or: [
// Find out if a published document exists
if (doc?._status === 'published') {
publishedDoc = doc
} else {
publishedDoc = (
await payload.find({
collection: collectionConfig.slug,
depth: 0,
limit: 1,
locale: locale || undefined,
pagination: false,
select: {
updatedAt: true,
},
user,
where: {
and: [
{
_status: {
equals: 'published',
},
or: [
{
_status: {
equals: 'published',
},
},
{
_status: {
exists: false,
},
},
],
},
{
_status: {
exists: false,
id: {
equals: id,
},
},
],
},
{
id: {
equals: id,
},
},
],
},
})
})
)?.docs?.[0]
}
if (publishedQuery.docs?.[0]) {
if (publishedDoc) {
hasPublishedDoc = true
}
@@ -109,6 +130,9 @@ export const getVersions = async ({
collection: collectionConfig.slug,
depth: 0,
limit: 1,
select: {
autosave: true,
},
user,
where: {
and: [
@@ -130,7 +154,7 @@ export const getVersions = async ({
}
}
if (publishedQuery.docs?.[0]?.updatedAt) {
if (publishedDoc?.updatedAt) {
;({ totalDocs: unpublishedVersionCount } = await payload.countVersions({
collection: collectionConfig.slug,
user,
@@ -148,7 +172,7 @@ export const getVersions = async ({
},
{
updatedAt: {
greater_than: publishedQuery.docs[0].updatedAt,
greater_than: publishedDoc.updatedAt,
},
},
],
@@ -159,6 +183,7 @@ export const getVersions = async ({
;({ totalDocs: versionCount } = await payload.countVersions({
collection: collectionConfig.slug,
depth: 0,
user,
where: {
and: [
@@ -173,15 +198,23 @@ export const getVersions = async ({
}
if (globalConfig) {
// Find out if a published document exists
if (versionsConfig?.drafts) {
publishedQuery = await payload.findGlobal({
slug: globalConfig.slug,
depth: 0,
locale,
user,
})
if (doc?._status === 'published') {
publishedDoc = doc
} else {
publishedDoc = await payload.findGlobal({
slug: globalConfig.slug,
depth: 0,
locale,
select: {
updatedAt: true,
},
user,
})
}
if (publishedQuery?._status === 'published') {
if (publishedDoc?._status === 'published') {
hasPublishedDoc = true
}
@@ -204,7 +237,7 @@ export const getVersions = async ({
}
}
if (publishedQuery?.updatedAt) {
if (publishedDoc?.updatedAt) {
;({ totalDocs: unpublishedVersionCount } = await payload.countGlobalVersions({
depth: 0,
global: globalConfig.slug,
@@ -218,7 +251,7 @@ export const getVersions = async ({
},
{
updatedAt: {
greater_than: publishedQuery.updatedAt,
greater_than: publishedDoc.updatedAt,
},
},
],

View File

@@ -1,46 +1,11 @@
import type { I18nClient } from '@payloadcms/translations'
import type {
ClientConfig,
Data,
DocumentPreferences,
FormState,
ImportMap,
PayloadRequest,
SanitizedConfig,
VisibleEntities,
} from 'payload'
import type { Data, DocumentPreferences, FormState, PayloadRequest, VisibleEntities } from 'payload'
import { getClientConfig } from '@payloadcms/ui/utilities/getClientConfig'
import { headers as getHeaders } from 'next/headers.js'
import { createClientConfig, getAccessResults, isEntityHidden, parseCookies } from 'payload'
import { getAccessResults, isEntityHidden, parseCookies } from 'payload'
import { renderDocument } from './index.js'
let cachedClientConfig = global._payload_clientConfig
if (!cachedClientConfig) {
cachedClientConfig = global._payload_clientConfig = null
}
export const getClientConfig = (args: {
config: SanitizedConfig
i18n: I18nClient
importMap: ImportMap
}): ClientConfig => {
const { config, i18n, importMap } = args
if (cachedClientConfig && process.env.NODE_ENV !== 'development') {
return cachedClientConfig
}
cachedClientConfig = createClientConfig({
config,
i18n,
importMap,
})
return cachedClientConfig
}
type RenderDocumentResult = {
data: any
Document: React.ReactNode

View File

@@ -1,10 +1,4 @@
import type {
AdminViewProps,
Data,
PayloadComponent,
ServerProps,
ServerSideEditViewProps,
} from 'payload'
import type { AdminViewProps, Data, PayloadComponent, ServerSideEditViewProps } from 'payload'
import { DocumentInfoProvider, EditDepthProvider, HydrateAuthProvider } from '@payloadcms/ui'
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
@@ -137,6 +131,7 @@ export const renderDocument = async ({
getVersions({
id: idFromArgs,
collectionConfig,
doc,
docPermissions,
globalConfig,
locale: locale?.code,

View File

@@ -1,45 +1,12 @@
import type { I18nClient } from '@payloadcms/translations'
import type { ListPreferences } from '@payloadcms/ui'
import type {
ClientConfig,
ImportMap,
ListQuery,
PayloadRequest,
SanitizedConfig,
VisibleEntities,
} from 'payload'
import type { ListQuery, PayloadRequest, VisibleEntities } from 'payload'
import { getClientConfig } from '@payloadcms/ui/utilities/getClientConfig'
import { headers as getHeaders } from 'next/headers.js'
import { createClientConfig, getAccessResults, isEntityHidden, parseCookies } from 'payload'
import { getAccessResults, isEntityHidden, parseCookies } from 'payload'
import { renderListView } from './index.js'
let cachedClientConfig = global._payload_clientConfig
if (!cachedClientConfig) {
cachedClientConfig = global._payload_clientConfig = null
}
export const getClientConfig = (args: {
config: SanitizedConfig
i18n: I18nClient
importMap: ImportMap
}): ClientConfig => {
const { config, i18n, importMap } = args
if (cachedClientConfig && process.env.NODE_ENV !== 'development') {
return cachedClientConfig
}
cachedClientConfig = createClientConfig({
config,
i18n,
importMap,
})
return cachedClientConfig
}
type RenderListResult = {
List: React.ReactNode
preferences: ListPreferences

View File

@@ -1,13 +1,15 @@
import type { I18n } from '@payloadcms/translations'
import type { Metadata } from 'next'
import type {
AdminViewComponent,
ImportMap,
PayloadServerReactComponent,
SanitizedConfig,
} from 'payload'
import { formatAdminURL } from '@payloadcms/ui/shared'
import {
type AdminViewComponent,
type ConfigImport,
getConfig,
type ImportMap,
type PayloadServerReactComponent,
type SanitizedConfig,
} from 'payload'
import React from 'react'
import { DefaultTemplate } from '../../templates/Default/index.js'
@@ -16,12 +18,12 @@ import { initPage } from '../../utilities/initPage/index.js'
import { NotFoundClient } from './index.client.js'
export const generatePageMetadata = async ({
config: configPromise,
config: configImport,
}: {
config: Promise<SanitizedConfig> | SanitizedConfig
config: ConfigImport
params?: { [key: string]: string | string[] }
}): Promise<Metadata> => {
const config = await configPromise
const config = await getConfig(configImport)
const i18n = await getNextRequestI18n({
config,
@@ -39,12 +41,12 @@ export type GenerateViewMetadata = (args: {
}) => Promise<Metadata>
export const NotFoundPage = async ({
config: configPromise,
config: configImport,
importMap,
params: paramsPromise,
searchParams: searchParamsPromise,
}: {
config: Promise<SanitizedConfig>
config: ConfigImport
importMap: ImportMap
params: Promise<{
segments: string[]
@@ -53,7 +55,8 @@ export const NotFoundPage = async ({
[key: string]: string | string[]
}>
}) => {
const config = await configPromise
const config = await getConfig(configImport)
const { routes: { admin: adminRoute } = {} } = config
const searchParams = await searchParamsPromise

View File

@@ -1,15 +1,15 @@
import type { I18nClient } from '@payloadcms/translations'
import type { Metadata } from 'next'
import type { ImportMap, SanitizedConfig } from 'payload'
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
import { formatAdminURL } from '@payloadcms/ui/shared'
import { getClientConfig } from '@payloadcms/ui/utilities/getClientConfig'
import { notFound, redirect } from 'next/navigation.js'
import { type ConfigImport, getConfig, type ImportMap, type SanitizedConfig } from 'payload'
import React, { Fragment } from 'react'
import { DefaultTemplate } from '../../templates/Default/index.js'
import { MinimalTemplate } from '../../templates/Minimal/index.js'
import { getClientConfig } from '../../utilities/getClientConfig.js'
import { initPage } from '../../utilities/initPage/index.js'
import { getViewFromConfig } from './getViewFromConfig.js'
@@ -23,12 +23,12 @@ export type GenerateViewMetadata = (args: {
}) => Promise<Metadata>
export const RootPage = async ({
config: configPromise,
config: configImport,
importMap,
params: paramsPromise,
searchParams: searchParamsPromise,
}: {
readonly config: Promise<SanitizedConfig>
readonly config: ConfigImport
readonly importMap: ImportMap
readonly params: Promise<{
segments: string[]
@@ -37,7 +37,7 @@ export const RootPage = async ({
[key: string]: string | string[]
}>
}) => {
const config = await configPromise
const config = await getConfig(configImport)
const {
admin: {
@@ -115,7 +115,7 @@ export const RootPage = async ({
redirect(adminRoute)
}
const clientConfig = await getClientConfig({
const clientConfig = getClientConfig({
config,
i18n: initPageResult?.req.i18n,
importMap,

View File

@@ -1,5 +1,6 @@
import type { Metadata } from 'next'
import type { SanitizedConfig } from 'payload'
import { type ConfigImport, getConfig } from 'payload'
import { getNextRequestI18n } from '../../utilities/getNextRequestI18n.js'
import { generateAccountMetadata } from '../Account/index.js'
@@ -26,7 +27,7 @@ const oneSegmentMeta = {
}
type Args = {
config: Promise<SanitizedConfig>
config: ConfigImport
params: Promise<{
[key: string]: string | string[]
}>
@@ -36,10 +37,10 @@ type Args = {
}
export const generatePageMetadata = async ({
config: configPromise,
config: configImport,
params: paramsPromise,
}: Args) => {
const config = await configPromise
const config = await getConfig(configImport)
const params = await paramsPromise
const segments = Array.isArray(params.segments) ? params.segments : []

View File

@@ -6,6 +6,7 @@ import type { ClientBlock, ClientField, Field } from '../../fields/config/types.
import type { DocumentPreferences } from '../../preferences/types.js'
import type { Operation, Payload, PayloadRequest } from '../../types/index.js'
import type {
ClientFieldSchemaMap,
ClientTab,
Data,
FieldSchemaMap,
@@ -67,6 +68,7 @@ export type FieldPaths = {
export type ServerComponentProps = {
clientField: ClientFieldWithOptionalType
clientFieldSchemaMap: ClientFieldSchemaMap
collectionSlug: string
data: Data
field: Field

View File

@@ -3,8 +3,16 @@ import type React from 'react'
import type { ImportMap } from '../bin/generateImportMap/index.js'
import type { SanitizedConfig } from '../config/types.js'
import type { Block, Field, FieldTypes, Tab } from '../fields/config/types.js'
import type {
Block,
ClientBlock,
ClientField,
Field,
FieldTypes,
Tab,
} from '../fields/config/types.js'
import type { JsonObject } from '../types/index.js'
import type { ClientTab } from './fields/Tabs.js'
import type {
BuildFormStateArgs,
Data,
@@ -489,3 +497,13 @@ export type FieldSchemaMap = Map<
| Field
| Tab
>
export type ClientFieldSchemaMap = Map<
SchemaPath,
| {
fields: ClientField[]
}
| ClientBlock
| ClientField
| ClientTab
>

View File

@@ -6,7 +6,7 @@ import path from 'path'
import type { BinScript } from '../config/types.js'
import { findConfig } from '../config/find.js'
import { getPayload } from '../index.js'
import { getConfig, getPayload } from '../index.js'
import { generateImportMap } from './generateImportMap/index.js'
import { generateTypes } from './generateTypes.js'
import { info } from './info.js'
@@ -51,12 +51,13 @@ export const bin = async () => {
}
const configPath = findConfig()
const configPromise = await import(pathToFileURL(configPath).toString())
let config = await configPromise
if (config.default) {
config = await config.default
let configImport = await import(pathToFileURL(configPath).toString())
if (configImport.default) {
configImport = configImport.default
}
const config = await getConfig(configImport)
const userBinScript = Array.isArray(config.bin)
? config.bin.find(({ key }) => key === script)
: false

View File

@@ -1,7 +1,7 @@
import { checkDependencies } from './utilities/dependencies/dependencyChecker.js'
import { PAYLOAD_PACKAGE_LIST } from './versions/payloadPackageList.js'
export async function checkPayloadDependencies() {
export function checkPayloadDependencies() {
const dependencies = [...PAYLOAD_PACKAGE_LIST]
if (process.env.PAYLOAD_CI_DEPENDENCY_CHECKER !== 'true') {
@@ -9,7 +9,7 @@ export async function checkPayloadDependencies() {
}
// First load. First check if there are mismatching dependency versions of payload packages
await checkDependencies({
void checkDependencies({
dependencyGroups: [
{
name: 'payload',

View File

@@ -9,10 +9,10 @@ import type {
} from '../../config/types.js'
import type { ClientField } from '../../fields/config/client.js'
import type { Payload } from '../../types/index.js'
import type { SanitizedUploadConfig } from '../../uploads/types.js'
import type { SanitizedCollectionConfig } from './types.js'
import { createClientFields } from '../../fields/config/client.js'
import { deepCopyObjectSimple } from '../../utilities/deepCopyObject.js'
export type ServerOnlyCollectionProperties = keyof Pick<
SanitizedCollectionConfig,
@@ -47,12 +47,19 @@ export type ClientCollectionConfig = {
| 'preview'
| ServerOnlyCollectionAdminProperties
>
auth?: { verify?: true } & Omit<
SanitizedCollectionConfig['auth'],
'forgotPassword' | 'strategies' | 'verify'
>
fields: ClientField[]
labels: {
plural: StaticLabel
singular: StaticLabel
}
} & Omit<SanitizedCollectionConfig, 'admin' | 'fields' | 'labels' | ServerOnlyCollectionProperties>
} & Omit<
SanitizedCollectionConfig,
'admin' | 'auth' | 'fields' | 'labels' | ServerOnlyCollectionProperties
>
const serverOnlyCollectionProperties: Partial<ServerOnlyCollectionProperties>[] = [
'hooks',
@@ -93,97 +100,147 @@ export const createClientCollectionConfig = ({
i18n: I18nClient
importMap: ImportMap
}): ClientCollectionConfig => {
const clientCollection = deepCopyObjectSimple(
collection,
true,
) as unknown as ClientCollectionConfig
const clientCollection = {} as Partial<ClientCollectionConfig>
clientCollection.fields = createClientFields({
clientFields: clientCollection?.fields || [],
defaultIDType,
fields: collection.fields,
i18n,
importMap,
})
serverOnlyCollectionProperties.forEach((key) => {
if (key in clientCollection) {
delete clientCollection[key]
for (const key in collection) {
if (serverOnlyCollectionProperties.includes(key as any)) {
continue
}
})
if ('upload' in clientCollection && typeof clientCollection.upload === 'object') {
serverOnlyUploadProperties.forEach((key) => {
if (key in clientCollection.upload) {
delete clientCollection.upload[key]
}
})
if ('imageSizes' in clientCollection.upload && clientCollection.upload.imageSizes.length) {
clientCollection.upload.imageSizes = clientCollection.upload.imageSizes.map((size) => {
const sanitizedSize = { ...size }
if ('generateImageName' in sanitizedSize) {
delete sanitizedSize.generateImageName
switch (key) {
case 'admin':
if (!collection.admin) {
break
}
return sanitizedSize
})
clientCollection.admin = {} as ClientCollectionConfig['admin']
for (const adminKey in collection.admin) {
if (serverOnlyCollectionAdminProperties.includes(adminKey as any)) {
continue
}
switch (adminKey) {
case 'description':
if (
typeof collection.admin.description === 'string' ||
typeof collection.admin.description === 'object'
) {
if (collection.admin.description) {
clientCollection.admin.description = collection.admin.description
}
} else if (typeof collection.admin.description === 'function') {
const description = collection.admin.description({ t: i18n.t })
if (description) {
clientCollection.admin.description = description
}
}
break
case 'livePreview':
clientCollection.admin.livePreview =
{} as ClientCollectionConfig['admin']['livePreview']
if (collection.admin.livePreview.breakpoints) {
clientCollection.admin.livePreview.breakpoints =
collection.admin.livePreview.breakpoints
}
break
case 'preview':
if (collection.admin.preview) {
clientCollection.admin.preview = true
}
break
default:
clientCollection.admin[adminKey] = collection.admin[adminKey]
}
}
break
case 'auth':
if (!collection.auth) {
break
}
clientCollection.auth = {} as { verify?: true } & SanitizedCollectionConfig['auth']
if (collection.auth.cookies) {
clientCollection.auth.cookies = collection.auth.cookies
}
if (collection.auth.depth !== undefined) {
// Check for undefined as it can be a number (0)
clientCollection.auth.depth = collection.auth.depth
}
if (collection.auth.disableLocalStrategy) {
clientCollection.auth.disableLocalStrategy = collection.auth.disableLocalStrategy
}
if (collection.auth.lockTime !== undefined) {
// Check for undefined as it can be a number (0)
clientCollection.auth.lockTime = collection.auth.lockTime
}
if (collection.auth.loginWithUsername) {
clientCollection.auth.loginWithUsername = collection.auth.loginWithUsername
}
if (collection.auth.maxLoginAttempts !== undefined) {
// Check for undefined as it can be a number (0)
clientCollection.auth.maxLoginAttempts = collection.auth.maxLoginAttempts
}
if (collection.auth.removeTokenFromResponses) {
clientCollection.auth.removeTokenFromResponses = collection.auth.removeTokenFromResponses
}
if (collection.auth.useAPIKey) {
clientCollection.auth.useAPIKey = collection.auth.useAPIKey
}
if (collection.auth.tokenExpiration) {
clientCollection.auth.tokenExpiration = collection.auth.tokenExpiration
}
if (collection.auth.verify) {
clientCollection.auth.verify = true
}
break
case 'fields':
clientCollection.fields = createClientFields({
defaultIDType,
fields: collection.fields,
i18n,
importMap,
})
break
case 'labels':
clientCollection.labels = {
plural:
typeof collection.labels.plural === 'function'
? collection.labels.plural({ t: i18n.t })
: collection.labels.plural,
singular:
typeof collection.labels.singular === 'function'
? collection.labels.singular({ t: i18n.t })
: collection.labels.singular,
}
break
case 'upload':
if (!collection.upload) {
break
}
clientCollection.upload = {} as SanitizedUploadConfig
for (const uploadKey in collection.upload) {
if (serverOnlyUploadProperties.includes(uploadKey as any)) {
continue
}
if (uploadKey === 'imageSizes') {
clientCollection.upload.imageSizes = collection.upload.imageSizes.map((size) => {
const sanitizedSize = { ...size }
if ('generateImageName' in sanitizedSize) {
delete sanitizedSize.generateImageName
}
return sanitizedSize
})
} else {
clientCollection.upload[uploadKey] = collection.upload[uploadKey]
}
}
break
break
default:
clientCollection[key] = collection[key]
}
}
if ('auth' in clientCollection && typeof clientCollection.auth === 'object') {
delete clientCollection.auth.strategies
delete clientCollection.auth.forgotPassword
delete clientCollection.auth.verify
}
if (collection.labels) {
Object.entries(collection.labels).forEach(([labelType, collectionLabel]) => {
if (typeof collectionLabel === 'function') {
clientCollection.labels[labelType] = collectionLabel({ t: i18n.t })
}
})
}
if (!clientCollection.admin) {
clientCollection.admin = {} as ClientCollectionConfig['admin']
}
serverOnlyCollectionAdminProperties.forEach((key) => {
if (key in clientCollection.admin) {
delete clientCollection.admin[key]
}
})
if (collection.admin.preview) {
clientCollection.admin.preview = true
}
let description = undefined
if (collection.admin?.description) {
if (
typeof collection.admin?.description === 'string' ||
typeof collection.admin?.description === 'object'
) {
description = collection.admin.description
} else if (typeof collection.admin?.description === 'function') {
description = collection.admin?.description({ t: i18n.t })
}
}
if (description) {
clientCollection.admin.description = description
}
if (
'livePreview' in clientCollection.admin &&
clientCollection.admin.livePreview &&
'url' in clientCollection.admin.livePreview
) {
delete clientCollection.admin.livePreview.url
}
return clientCollection
return clientCollection as ClientCollectionConfig
}
export const createClientCollectionConfigs = ({

View File

@@ -91,6 +91,11 @@ export const findVersionByIDOperation = async <TData extends TypeWithID = any>(
return null
}
if (!result.version) {
// Fallback if not selected
;(result as any).version = {}
}
// /////////////////////////////////////
// beforeRead - Collection
// /////////////////////////////////////

View File

@@ -93,6 +93,10 @@ export const findVersionsOperation = async <TData extends TypeWithVersion<TData>
docs: await Promise.all(
paginatedDocs.docs.map(async (doc) => {
const docRef = doc
// Fallback if not selected
if (!docRef.version) {
;(docRef as any).version = {}
}
await collectionConfig.hooks.beforeRead.reduce(async (priorHook, hook) => {
await priorHook

View File

@@ -7,7 +7,31 @@ import { sanitizeConfig } from './sanitize.js'
* @param config Payload Config
* @returns Built and sanitized Payload Config
*/
export async function buildConfig(config: Config): Promise<SanitizedConfig> {
export async function buildConfig(
_config: (() => Config) | Config,
): Promise<(() => Promise<SanitizedConfig>) | SanitizedConfig> {
if (typeof _config !== 'function') {
console.warn(
'For optimal performance, buildConfig should be called with a function that returns a config object. Otherwise, you will notice increased memory usage and decreased performance during development.',
)
// We could still return a function that returns the sanitized config here,
// so that it's cached properly and not loaded multiple times after every page transition.
// However, in order for this to be backwards compatible, we return the sanitized config directly, the old way.
// Otherwise, the imported config would suddenly be a function when imported, which may break standalone scripts
return await loadAndSanitizeConfig(() => _config)
} else {
if (process.env.NODE_ENV === 'production') {
return await loadAndSanitizeConfig(_config)
} else {
return async () => {
return await loadAndSanitizeConfig(_config)
}
}
}
}
const loadAndSanitizeConfig = async (configFn: () => Config): Promise<SanitizedConfig> => {
const config = configFn()
if (Array.isArray(config.plugins)) {
const configAfterPlugins = await config.plugins.reduce(async (acc, plugin) => {
const configAfterPlugin = await acc

View File

@@ -1,4 +1,5 @@
import type { I18nClient } from '@payloadcms/translations'
import type { DeepPartial } from 'ts-essentials'
import type { ImportMap } from '../bin/generateImportMap/index.js'
import type {
@@ -12,7 +13,6 @@ import {
createClientCollectionConfigs,
} from '../collections/config/client.js'
import { type ClientGlobalConfig, createClientGlobalConfigs } from '../globals/config/client.js'
import { deepCopyObjectSimple } from '../utilities/deepCopyObject.js'
export type ServerOnlyRootProperties = keyof Pick<
SanitizedConfig,
@@ -39,7 +39,6 @@ export type ServerOnlyRootAdminProperties = keyof Pick<SanitizedConfig['admin'],
export type ClientConfig = {
admin: {
components: null
dependencies?: Record<string, React.ReactNode>
livePreview?: Omit<LivePreviewConfig, ServerOnlyLivePreviewProperties>
} & Omit<SanitizedConfig['admin'], 'components' | 'dependencies' | 'livePreview'>
@@ -81,56 +80,95 @@ export const createClientConfig = ({
i18n: I18nClient
importMap: ImportMap
}): ClientConfig => {
// We can use deepCopySimple here, as the clientConfig should be JSON serializable anyways, since it will be sent from server => client
const clientConfig = deepCopyObjectSimple(config, true) as unknown as ClientConfig
const clientConfig = {} as DeepPartial<ClientConfig>
for (const key of serverOnlyConfigProperties) {
if (key in clientConfig) {
delete clientConfig[key]
for (const key in config) {
if (serverOnlyConfigProperties.includes(key as any)) {
continue
}
switch (key) {
case 'admin':
clientConfig.admin = {
autoLogin: config.admin.autoLogin,
avatar: config.admin.avatar,
custom: config.admin.custom,
dateFormat: config.admin.dateFormat,
dependencies: config.admin.dependencies,
disable: config.admin.disable,
importMap: config.admin.importMap,
meta: config.admin.meta,
routes: config.admin.routes,
theme: config.admin.theme,
user: config.admin.user,
}
if (config.admin.livePreview) {
clientConfig.admin.livePreview = {}
if (config.admin.livePreview.breakpoints) {
clientConfig.admin.livePreview.breakpoints = config.admin.livePreview.breakpoints
}
}
break
case 'collections':
;(clientConfig.collections as ClientCollectionConfig[]) = createClientCollectionConfigs({
collections: config.collections,
defaultIDType: config.db.defaultIDType,
i18n,
importMap,
})
break
case 'globals':
;(clientConfig.globals as ClientGlobalConfig[]) = createClientGlobalConfigs({
defaultIDType: config.db.defaultIDType,
globals: config.globals,
i18n,
importMap,
})
break
case 'i18n':
clientConfig.i18n = {
fallbackLanguage: config.i18n.fallbackLanguage,
translations: config.i18n.translations,
}
break
case 'localization':
if (typeof config.localization === 'object' && config.localization) {
clientConfig.localization = {}
if (config.localization.defaultLocale) {
clientConfig.localization.defaultLocale = config.localization.defaultLocale
}
if (config.localization.fallback) {
clientConfig.localization.fallback = config.localization.fallback
}
if (config.localization.localeCodes) {
clientConfig.localization.localeCodes = config.localization.localeCodes
}
if (config.localization.locales) {
clientConfig.localization.locales = []
for (const locale of config.localization.locales) {
if (locale) {
const clientLocale: Partial<(typeof config.localization.locales)[0]> = {}
if (locale.code) {
clientLocale.code = locale.code
}
if (locale.fallbackLocale) {
clientLocale.fallbackLocale = locale.fallbackLocale
}
if (locale.label) {
clientLocale.label = locale.label
}
if (locale.rtl) {
clientLocale.rtl = locale.rtl
}
clientConfig.localization.locales.push(clientLocale)
}
}
}
}
break
default:
clientConfig[key] = config[key]
}
}
if ('localization' in clientConfig && clientConfig.localization) {
for (const locale of clientConfig.localization.locales) {
delete locale.toString
}
}
if (
'i18n' in clientConfig &&
'supportedLanguages' in clientConfig.i18n &&
clientConfig.i18n.supportedLanguages
) {
delete clientConfig.i18n.supportedLanguages
}
if (!clientConfig.admin) {
clientConfig.admin = {} as ClientConfig['admin']
}
clientConfig.admin.components = null
if (
'livePreview' in clientConfig.admin &&
clientConfig.admin.livePreview &&
'url' in clientConfig.admin.livePreview
) {
delete clientConfig.admin.livePreview.url
}
clientConfig.collections = createClientCollectionConfigs({
collections: config.collections,
defaultIDType: config.db.defaultIDType,
i18n,
importMap,
})
clientConfig.globals = createClientGlobalConfigs({
defaultIDType: config.db.defaultIDType,
globals: config.globals,
i18n,
importMap,
})
return clientConfig
return clientConfig as ClientConfig
}

View File

@@ -58,6 +58,7 @@ const sanitizeAdminConfig = (configToSanitize: Config): Partial<SanitizedConfig>
}
export const sanitizeConfig = async (incomingConfig: Config): Promise<SanitizedConfig> => {
console.log('Sanitizing config')
const configWithDefaults = {
...defaults,
...incomingConfig,

View File

@@ -1090,6 +1090,10 @@ export type Config = {
upload?: FetchAPIFileUploadOptions
}
export type GetSanitizedConfig = () => Promise<SanitizedConfig>
export type ConfigImport = GetSanitizedConfig | Promise<SanitizedConfig> | SanitizedConfig
export type SanitizedConfig = {
collections: SanitizedCollectionConfig[]
/** Default richtext editor to use for richText fields */

View File

@@ -39,61 +39,97 @@ export type ServerOnlyFieldProperties =
export type ServerOnlyFieldAdminProperties = keyof Pick<FieldBase['admin'], 'condition'>
const serverOnlyFieldProperties: Partial<ServerOnlyFieldProperties>[] = [
'hooks',
'access',
'validate',
'defaultValue',
'filterOptions', // This is a `relationship` and `upload` only property
'editor', // This is a `richText` only property
'custom',
'typescriptSchema',
'dbName', // can be a function
'enumName', // can be a function
// the following props are handled separately (see below):
// `label`
// `fields`
// `blocks`
// `tabs`
// `admin`
]
const serverOnlyFieldAdminProperties: Partial<ServerOnlyFieldAdminProperties>[] = ['condition']
type FieldWithDescription = {
admin: AdminClient
} & ClientField
export const createClientField = ({
clientField = {} as ClientField,
defaultIDType,
field: incomingField,
i18n,
importMap,
}: {
clientField?: ClientField
defaultIDType: Payload['config']['db']['defaultIDType']
field: Field
i18n: I18nClient
importMap: ImportMap
}): ClientField => {
const serverOnlyFieldProperties: Partial<ServerOnlyFieldProperties>[] = [
'hooks',
'access',
'validate',
'defaultValue',
'filterOptions', // This is a `relationship` and `upload` only property
'editor', // This is a `richText` only property
'custom',
'typescriptSchema',
'dbName', // can be a function
'enumName', // can be a function
// the following props are handled separately (see below):
// `label`
// `fields`
// `blocks`
// `tabs`
// `admin`
]
clientField.admin = clientField.admin || {}
// clientField.admin.readOnly = true
serverOnlyFieldProperties.forEach((key) => {
if (key in clientField) {
delete clientField[key]
}
})
const clientField: ClientField = {} as ClientField
const isHidden = 'hidden' in incomingField && incomingField?.hidden
const disabledFromAdmin =
incomingField?.admin && 'disabled' in incomingField.admin && incomingField.admin.disabled
if (fieldAffectsData(clientField) && (isHidden || disabledFromAdmin)) {
if (fieldAffectsData(incomingField) && (isHidden || disabledFromAdmin)) {
return null
}
if (
'label' in clientField &&
'label' in incomingField &&
typeof incomingField.label === 'function'
) {
clientField.label = incomingField.label({ t: i18n.t })
for (const key in incomingField) {
if (serverOnlyFieldProperties.includes(key as any)) {
continue
}
switch (key) {
case 'admin':
if (!incomingField.admin) {
break
}
clientField.admin = {} as AdminClient
for (const adminKey in incomingField.admin) {
if (serverOnlyFieldAdminProperties.includes(adminKey as any)) {
continue
}
switch (adminKey) {
case 'description':
if ('description' in incomingField.admin) {
if (typeof incomingField.admin?.description !== 'function') {
;(clientField as FieldWithDescription).admin.description =
incomingField.admin.description
}
}
break
default:
clientField.admin[adminKey] = incomingField.admin[adminKey]
}
}
break
case 'blocks':
case 'fields':
case 'tabs':
// Skip - we handle sub-fields in the switch below
break
case 'label':
//@ts-expect-error - would need to type narrow
if (typeof incomingField.label === 'function') {
//@ts-expect-error - would need to type narrow
clientField.label = incomingField.label({ t: i18n.t })
} else {
//@ts-expect-error - would need to type narrow
clientField.label = incomingField.label
}
break
default:
clientField[key] = incomingField[key]
}
}
switch (incomingField.type) {
@@ -108,7 +144,6 @@ export const createClientField = ({
}
field.fields = createClientFields({
clientFields: field.fields,
defaultIDType,
disableAddingID: incomingField.type !== 'array',
fields: incomingField.fields,
@@ -167,7 +202,6 @@ export const createClientField = ({
}
clientBlock.fields = createClientFields({
clientFields: clientBlock.fields,
defaultIDType,
fields: block.fields,
i18n,
@@ -225,24 +259,29 @@ export const createClientField = ({
const field = clientField as unknown as TabsFieldClient
if (incomingField.tabs?.length) {
field.tabs = []
for (let i = 0; i < incomingField.tabs.length; i++) {
const tab = incomingField.tabs[i]
const clientTab = field.tabs[i]
const clientTab = {} as unknown as TabsFieldClient['tabs'][0]
serverOnlyFieldProperties.forEach((key) => {
if (key in clientTab) {
delete clientTab[key]
for (const key in tab) {
if (serverOnlyFieldProperties.includes(key as any)) {
continue
}
})
clientTab.fields = createClientFields({
clientFields: clientTab.fields,
defaultIDType,
disableAddingID: true,
fields: tab.fields,
i18n,
importMap,
})
if (key === 'fields') {
clientTab.fields = createClientFields({
defaultIDType,
disableAddingID: true,
fields: tab.fields,
i18n,
importMap,
})
} else {
clientTab[key] = tab[key]
}
}
field.tabs[i] = clientTab
}
}
@@ -253,70 +292,43 @@ export const createClientField = ({
break
}
const serverOnlyFieldAdminProperties: Partial<ServerOnlyFieldAdminProperties>[] = ['condition']
if (!clientField.admin) {
clientField.admin = {} as AdminClient
}
serverOnlyFieldAdminProperties.forEach((key) => {
if (key in clientField.admin) {
delete clientField.admin[key]
}
})
type FieldWithDescription = {
admin: AdminClient
} & ClientField
if (incomingField.admin && 'description' in incomingField.admin) {
if (typeof incomingField.admin?.description === 'function') {
delete (clientField as FieldWithDescription).admin.description
} else {
;(clientField as FieldWithDescription).admin.description = incomingField.admin.description
}
}
return clientField
}
export const createClientFields = ({
clientFields,
defaultIDType,
disableAddingID,
fields,
i18n,
importMap,
}: {
clientFields: ClientField[]
defaultIDType: Payload['config']['db']['defaultIDType']
disableAddingID?: boolean
fields: Field[]
i18n: I18nClient
importMap: ImportMap
}): ClientField[] => {
const newClientFields: ClientField[] = []
const clientFields: ClientField[] = []
for (let i = 0; i < fields.length; i++) {
const field = fields[i]
const newField = createClientField({
clientField: clientFields[i],
const clientField = createClientField({
defaultIDType,
field,
i18n,
importMap,
})
if (newField) {
newClientFields.push(newField)
if (clientField) {
clientFields.push(clientField)
}
}
const hasID = flattenTopLevelFields(fields).some((f) => fieldAffectsData(f) && f.name === 'id')
if (!disableAddingID && !hasID) {
newClientFields.push({
clientFields.push({
name: 'id',
type: defaultIDType,
admin: {
@@ -330,5 +342,5 @@ export const createClientFields = ({
})
}
return newClientFields
return clientFields
}

View File

@@ -1,9 +1,7 @@
import type { ClientField, Field, TabAsField } from './config/types.js'
import { fieldAffectsData } from './config/types.js'
import type { ClientField, Field, TabAsField, TabAsFieldClient } from './config/types.js'
type Args = {
field: ClientField | Field | TabAsField
field: ClientField | Field | TabAsField | TabAsFieldClient
index: number
parentIndexPath: string
parentPath: string

View File

@@ -10,14 +10,16 @@ import type { Payload } from '../../types/index.js'
import type { SanitizedGlobalConfig } from './types.js'
import { type ClientField, createClientFields } from '../../fields/config/client.js'
import { deepCopyObjectSimple } from '../../utilities/deepCopyObject.js'
export type ServerOnlyGlobalProperties = keyof Pick<
SanitizedGlobalConfig,
'access' | 'admin' | 'custom' | 'endpoints' | 'fields' | 'flattenedFields' | 'hooks'
>
export type ServerOnlyGlobalAdminProperties = keyof Pick<SanitizedGlobalConfig['admin'], 'hidden'>
export type ServerOnlyGlobalAdminProperties = keyof Pick<
SanitizedGlobalConfig['admin'],
'components' | 'hidden'
>
export type ClientGlobalConfig = {
admin: {
@@ -40,7 +42,10 @@ const serverOnlyProperties: Partial<ServerOnlyGlobalProperties>[] = [
// `admin` is handled separately
]
const serverOnlyGlobalAdminProperties: Partial<ServerOnlyGlobalAdminProperties>[] = ['hidden']
const serverOnlyGlobalAdminProperties: Partial<ServerOnlyGlobalAdminProperties>[] = [
'hidden',
'components',
]
export const createClientGlobalConfig = ({
defaultIDType,
@@ -53,44 +58,55 @@ export const createClientGlobalConfig = ({
i18n: I18nClient
importMap: ImportMap
}): ClientGlobalConfig => {
const clientGlobal = deepCopyObjectSimple(global, true) as unknown as ClientGlobalConfig
const clientGlobal = {} as ClientGlobalConfig
clientGlobal.fields = createClientFields({
clientFields: clientGlobal?.fields || [],
defaultIDType,
fields: global.fields,
i18n,
importMap,
})
serverOnlyProperties.forEach((key) => {
if (key in clientGlobal) {
delete clientGlobal[key]
for (const key in global) {
if (serverOnlyProperties.includes(key as any)) {
continue
}
})
if (!clientGlobal.admin) {
clientGlobal.admin = {} as ClientGlobalConfig['admin']
}
serverOnlyGlobalAdminProperties.forEach((key) => {
if (key in clientGlobal.admin) {
delete clientGlobal.admin[key]
switch (key) {
case 'admin':
if (!global.admin) {
break
}
clientGlobal.admin = {} as ClientGlobalConfig['admin']
for (const adminKey in global.admin) {
if (serverOnlyGlobalAdminProperties.includes(adminKey as any)) {
continue
}
switch (adminKey) {
case 'livePreview':
if (!global.admin.livePreview) {
break
}
clientGlobal.admin.livePreview = {}
if (global.admin.livePreview.breakpoints) {
clientGlobal.admin.livePreview.breakpoints = global.admin.livePreview.breakpoints
}
break
case 'preview':
if (global.admin.preview) {
clientGlobal.admin.preview = true
}
break
default:
clientGlobal.admin[adminKey] = global.admin[adminKey]
}
}
break
case 'fields':
clientGlobal.fields = createClientFields({
defaultIDType,
fields: global.fields,
i18n,
importMap,
})
break
default: {
clientGlobal[key] = global[key]
break
}
}
})
if (global.admin.preview) {
clientGlobal.admin.preview = true
}
clientGlobal.admin.components = null
if (
'livePreview' in clientGlobal.admin &&
clientGlobal.admin.livePreview &&
'url' in clientGlobal.admin.livePreview
) {
delete clientGlobal.admin.livePreview.url
}
return clientGlobal

View File

@@ -90,6 +90,10 @@ export const findVersionByIDOperation = async <T extends TypeWithVersion<T> = an
// Clone the result - it may have come back memoized
let result: any = deepCopyObjectSimple(results[0])
if (!result.version) {
result.version = {}
}
// Patch globalType onto version doc
result.version.globalType = globalConfig.slug

View File

@@ -90,6 +90,10 @@ export const findVersionsOperation = async <T extends TypeWithVersion<T>>(
...paginatedDocs,
docs: await Promise.all(
paginatedDocs.docs.map(async (data) => {
if (!data.version) {
// Fallback if not selected
;(data as any).version = {}
}
return {
...data,
version: await afterRead<T>({

View File

@@ -44,7 +44,7 @@ import type {
ManyOptions as UpdateManyOptions,
Options as UpdateOptions,
} from './collections/operations/local/update.js'
import type { InitOptions, SanitizedConfig } from './config/types.js'
import type { ConfigImport, InitOptions, SanitizedConfig } from './config/types.js'
import type { BaseDatabaseAdapter, PaginatedDocs } from './database/types.js'
import type { InitializedEmailAdapter } from './email/types.js'
import type { DataFromGlobalSlug, Globals, SelectFromGlobalSlug } from './globals/config/types.js'
@@ -555,7 +555,7 @@ export class BasePayload {
!checkedDependencies
) {
checkedDependencies = true
await checkPayloadDependencies()
void checkPayloadDependencies()
}
this.importMap = options.importMap
@@ -782,8 +782,37 @@ export const reload = async (
if (payload.db.connect) {
await payload.db.connect({ hotReload: true })
}
global._payload_clientConfig = null
global._payload_schemaMap = null
global._payload_clientSchemaMap = null
global._payload_doNotCacheClientConfig = true // This will help refreshing the client config cache more reliably. If you remove this, please test HMR + client config refreshing (do new fields appear in the document?)
global._payload_doNotCacheSchemaMap = true
global._payload_doNotCacheClientSchemaMap = true
}
export const getConfig = async (configImport: ConfigImport): Promise<SanitizedConfig> => {
if (global._payload_config) {
let config = await global._payload_config
if (typeof config === 'function') {
config = await config()
}
return await config
}
if (typeof configImport === 'function') {
global._payload_config = configImport()
await global._payload_config
return global._payload_config
} else {
global._payload_config = configImport
global._payload_config = await global._payload_config
if (typeof global._payload_config === 'function') {
global._payload_config = global._payload_config()
global._payload_config = await global._payload_config
}
return global._payload_config
}
}
export const getPayload = async (
options: Pick<InitOptions, 'config' | 'importMap'>,
): Promise<Payload> => {

View File

@@ -212,10 +212,8 @@ export const BlockComponent: React.FC<Props> = (props) => {
editor.update(() => {
const node = $getNodeByKey(nodeKey)
if (node && $isBlockNode(node)) {
const newData = {
...newFormStateData,
blockType: formData.blockType,
}
const newData = newFormStateData
newData.blockType = formData.blockType
node.setFields(newData)
}
})

View File

@@ -14,7 +14,6 @@ import type { JSX } from 'react'
import { DecoratorBlockNode } from '@lexical/react/LexicalDecoratorBlockNode.js'
import ObjectID from 'bson-objectid'
import { deepCopyObjectSimple } from 'payload/shared'
type BaseBlockFields<TBlockFields extends JsonObject = JsonObject> = {
/** Block form data */
@@ -121,10 +120,8 @@ export class ServerBlockNode extends DecoratorBlockNode {
}
setFields(fields: BlockFields): void {
const fieldsCopy = deepCopyObjectSimple(fields)
const writable = this.getWritable()
writable.__fields = fieldsCopy
writable.__fields = fields
}
}

View File

@@ -13,7 +13,6 @@ import type { JSX } from 'react'
import ObjectID from 'bson-objectid'
import { DecoratorNode } from 'lexical'
import { deepCopyObjectSimple } from 'payload/shared'
export type InlineBlockFields = {
/** Block form data */
@@ -108,10 +107,8 @@ export class ServerInlineBlockNode extends DecoratorNode<null | React.ReactEleme
}
setFields(fields: InlineBlockFields): void {
const fieldsCopy = deepCopyObjectSimple(fields)
const writable = this.getWritable()
writable.__fields = fieldsCopy
writable.__fields = fields
}
updateDOM(): boolean {

View File

@@ -9,7 +9,6 @@ import type {
import escapeHTML from 'escape-html'
import { sanitizeFields } from 'payload'
import { deepCopyObject } from 'payload/shared'
import type { ClientProps } from '../client/index.js'
@@ -78,7 +77,7 @@ export const LinkFeature = createServerFeature<
const validRelationships = _config.collections.map((c) => c.slug) || []
const _transformedFields = transformExtraFields(
props.fields ? deepCopyObject(props.fields) : null,
props.fields ? props.fields : null,
_config,
props.enabledCollections,
props.disabledCollections,
@@ -97,7 +96,7 @@ export const LinkFeature = createServerFeature<
// the text field is not included in the node data.
// Thus, for tasks like validation, we do not want to pass it a text field in the schema which will never have data.
// Otherwise, it will cause a validation error (field is required).
const sanitizedFieldsWithoutText = deepCopyObject(sanitizedFields).filter(
const sanitizedFieldsWithoutText = sanitizedFields.filter(
(field) => !('name' in field) || field.name !== 'text',
)

View File

@@ -29,7 +29,9 @@ export const RscEntryLexicalField: React.FC<
const field: RichTextFieldType = args.field as RichTextFieldType
const path = args.path ?? (args.clientField as RichTextFieldClient).name
const schemaPath = args.schemaPath ?? path
const { clientFeatures, featureClientSchemaMap } = initLexicalFeatures({
clientFieldSchemaMap: args.clientFieldSchemaMap,
fieldSchemaMap: args.fieldSchemaMap,
i18n: args.i18n,
path,
@@ -43,6 +45,7 @@ export const RscEntryLexicalField: React.FC<
initialLexicalFormState = await buildInitialState({
context: {
id: args.id,
clientFieldSchemaMap: args.clientFieldSchemaMap,
collectionSlug: args.collectionSlug,
field,
fieldSchemaMap: args.fieldSchemaMap,
@@ -60,7 +63,7 @@ export const RscEntryLexicalField: React.FC<
const props: LexicalRichTextFieldProps = {
admin: args.admin,
clientFeatures,
featureClientSchemaMap,
featureClientSchemaMap, // TODO: Does client need this? Why cant this just live in the server
field: args.clientField as RichTextFieldClient,
forceRender: args.forceRender,
initialLexicalFormState,

View File

@@ -0,0 +1,46 @@
import type { SanitizedConfig } from 'payload'
import { cache } from 'react'
import { type SanitizedServerEditorConfig } from './index.js'
import { defaultEditorConfig } from './lexical/config/server/default.js'
import { sanitizeServerEditorConfig } from './lexical/config/server/sanitize.js'
let cachedDefaultSanitizedServerEditorConfig:
| null
| Promise<SanitizedServerEditorConfig>
| SanitizedServerEditorConfig = (global as any)
._payload_lexical_defaultSanitizedServerEditorConfig
if (!cachedDefaultSanitizedServerEditorConfig) {
cachedDefaultSanitizedServerEditorConfig = (
global as any
)._payload_lexical_defaultSanitizedServerEditorConfig = null
}
export const getDefaultSanitizedEditorConfig = cache(
async (args: {
config: SanitizedConfig
parentIsLocalized: boolean
}): Promise<SanitizedServerEditorConfig> => {
const { config, parentIsLocalized } = args
if (cachedDefaultSanitizedServerEditorConfig) {
return await cachedDefaultSanitizedServerEditorConfig
}
cachedDefaultSanitizedServerEditorConfig = sanitizeServerEditorConfig(
defaultEditorConfig,
config,
parentIsLocalized,
)
;(global as any).payload_lexical_defaultSanitizedServerEditorConfig =
cachedDefaultSanitizedServerEditorConfig
cachedDefaultSanitizedServerEditorConfig = await cachedDefaultSanitizedServerEditorConfig
;(global as any).payload_lexical_defaultSanitizedServerEditorConfig =
cachedDefaultSanitizedServerEditorConfig
return cachedDefaultSanitizedServerEditorConfig
},
)

View File

@@ -7,8 +7,6 @@ import {
beforeChangeTraverseFields,
beforeValidateTraverseFields,
checkDependencies,
deepCopyObject,
deepCopyObjectSimple,
withNullableJSONSchemaType,
} from 'payload'
@@ -21,89 +19,80 @@ import type {
LexicalRichTextAdapterProvider,
} from './types.js'
import { getDefaultSanitizedEditorConfig } from './getDefaultSanitizedEditorConfig.js'
import { i18n } from './i18n.js'
import { defaultEditorConfig, defaultEditorFeatures } from './lexical/config/server/default.js'
import { loadFeatures } from './lexical/config/server/loader.js'
import {
sanitizeServerEditorConfig,
sanitizeServerFeatures,
} from './lexical/config/server/sanitize.js'
import { sanitizeServerFeatures } from './lexical/config/server/sanitize.js'
import { populateLexicalPopulationPromises } from './populateGraphQL/populateLexicalPopulationPromises.js'
import { getGenerateImportMap } from './utilities/generateImportMap.js'
import { getGenerateSchemaMap } from './utilities/generateSchemaMap.js'
import { recurseNodeTree } from './utilities/recurseNodeTree.js'
import { richTextValidateHOC } from './validate/index.js'
let defaultSanitizedServerEditorConfig: null | SanitizedServerEditorConfig = null
let checkedDependencies = false
export function lexicalEditor(props?: LexicalEditorProps): LexicalRichTextAdapterProvider {
if (
process.env.NODE_ENV !== 'production' &&
process.env.PAYLOAD_DISABLE_DEPENDENCY_CHECKER !== 'true' &&
!checkedDependencies
) {
checkedDependencies = true
void checkDependencies({
dependencyGroups: [
{
name: 'lexical',
dependencies: [
'lexical',
'@lexical/headless',
'@lexical/link',
'@lexical/list',
'@lexical/mark',
'@lexical/react',
'@lexical/rich-text',
'@lexical/selection',
'@lexical/utils',
],
targetVersion: '0.20.0',
},
],
})
}
return async ({ config, isRoot, parentIsLocalized }) => {
if (
process.env.NODE_ENV !== 'production' &&
process.env.PAYLOAD_DISABLE_DEPENDENCY_CHECKER !== 'true' &&
!checkedDependencies
) {
checkedDependencies = true
await checkDependencies({
dependencyGroups: [
{
name: 'lexical',
dependencies: [
'lexical',
'@lexical/headless',
'@lexical/link',
'@lexical/list',
'@lexical/mark',
'@lexical/react',
'@lexical/rich-text',
'@lexical/selection',
'@lexical/utils',
],
targetVersion: '0.20.0',
},
],
})
}
let features: FeatureProviderServer<unknown, unknown, unknown>[] = []
let resolvedFeatureMap: ResolvedServerFeatureMap
let finalSanitizedEditorConfig: SanitizedServerEditorConfig // For server only
if (!props || (!props.features && !props.lexical)) {
if (!defaultSanitizedServerEditorConfig) {
defaultSanitizedServerEditorConfig = await sanitizeServerEditorConfig(
defaultEditorConfig,
config,
parentIsLocalized,
)
features = deepCopyObject(defaultEditorFeatures)
}
finalSanitizedEditorConfig = await getDefaultSanitizedEditorConfig({
config,
parentIsLocalized,
})
finalSanitizedEditorConfig = deepCopyObject(defaultSanitizedServerEditorConfig)
delete finalSanitizedEditorConfig.lexical // We don't want to send the default lexical editor config to the client
features = defaultEditorFeatures
resolvedFeatureMap = finalSanitizedEditorConfig.resolvedFeatureMap
} else {
const rootEditor = config.editor
let rootEditorFeatures: FeatureProviderServer<unknown, unknown, unknown>[] = []
if (typeof rootEditor === 'object' && 'features' in rootEditor) {
rootEditorFeatures = (rootEditor as LexicalRichTextAdapter).features
if (props.features && typeof props.features === 'function') {
const rootEditor = config.editor
let rootEditorFeatures: FeatureProviderServer<unknown, unknown, unknown>[] = []
if (typeof rootEditor === 'object' && 'features' in rootEditor) {
rootEditorFeatures = (rootEditor as LexicalRichTextAdapter).features
}
features = props.features({
defaultFeatures: defaultEditorFeatures,
rootFeatures: rootEditorFeatures,
})
} else {
features = props.features as FeatureProviderServer<unknown, unknown, unknown>[]
}
features =
props.features && typeof props.features === 'function'
? props.features({
defaultFeatures: deepCopyObject(defaultEditorFeatures),
rootFeatures: rootEditorFeatures,
})
: (props.features as FeatureProviderServer<unknown, unknown, unknown>[])
if (!features) {
features = deepCopyObject(defaultEditorFeatures)
features = defaultEditorFeatures
}
const lexical = props.lexical ?? deepCopyObjectSimple(defaultEditorConfig.lexical)!
const lexical = props.lexical ?? defaultEditorConfig.lexical
resolvedFeatureMap = await loadFeatures({
config,

View File

@@ -2,6 +2,7 @@ import type { SanitizedConfig } from 'payload'
import type {
FeatureProviderServer,
ResolvedServerFeature,
ResolvedServerFeatureMap,
ServerFeatureProviderMap,
} from '../../../features/typesServer.js'
@@ -186,14 +187,21 @@ export async function loadFeatures({
unSanitizedEditorConfig,
})
: featureProvider.feature
resolvedFeatures.set(featureProvider.key, {
...feature,
dependencies: featureProvider.dependencies!,
dependenciesPriority: featureProvider.dependenciesPriority!,
dependenciesSoft: featureProvider.dependenciesSoft!,
key: featureProvider.key,
order: loaded,
})
const resolvedFeature: ResolvedServerFeature<any, any> = feature as ResolvedServerFeature<
any,
any
>
// All these new properties would be added to the feature, as it's mutated. However, this does not cause any damage and allows
// us to prevent an unnecessary spread operation.
resolvedFeature.key = featureProvider.key
resolvedFeature.order = loaded
resolvedFeature.dependencies = featureProvider.dependencies!
resolvedFeature.dependenciesPriority = featureProvider.dependenciesPriority!
resolvedFeature.dependenciesSoft = featureProvider.dependenciesSoft!
resolvedFeatures.set(featureProvider.key, resolvedFeature)
loaded++
}

View File

@@ -80,10 +80,11 @@ export type LexicalRichTextAdapterProvider =
parentIsLocalized: boolean
}) => Promise<LexicalRichTextAdapter>
export type SingleFeatureClientSchemaMap = {
[key: string]: ClientField[]
}
export type FeatureClientSchemaMap = {
[featureKey: string]: {
[key: string]: ClientField[]
}
[featureKey: string]: SingleFeatureClientSchemaMap
}
export type LexicalRichTextFieldProps = {

View File

@@ -1,5 +1,6 @@
import type { SerializedLexicalNode } from 'lexical'
import type {
ClientFieldSchemaMap,
DocumentPreferences,
FieldSchemaMap,
FormState,
@@ -22,6 +23,7 @@ export type InitialLexicalFormState = {
type Props = {
context: {
clientFieldSchemaMap: ClientFieldSchemaMap
collectionSlug: string
field: RichTextField
fieldSchemaMap: FieldSchemaMap
@@ -68,6 +70,7 @@ export async function buildInitialState({
const formStateResult = await fieldSchemasToFormState({
id: context.id,
clientFieldSchemaMap: context.clientFieldSchemaMap,
collectionSlug: context.collectionSlug,
data: blockNode.fields,
fields: (context.fieldSchemaMap.get(schemaFieldsPath) as any)?.fields,

View File

@@ -1,18 +1,13 @@
import type { I18nClient } from '@payloadcms/translations'
import {
type ClientField,
createClientFields,
deepCopyObjectSimple,
type FieldSchemaMap,
type Payload,
} from 'payload'
import { type ClientFieldSchemaMap, type FieldSchemaMap, type Payload } from 'payload'
import { getFromImportMap } from 'payload/shared'
import type { FeatureProviderProviderClient } from '../features/typesClient.js'
import type { SanitizedServerEditorConfig } from '../lexical/config/types.js'
import type { FeatureClientSchemaMap, LexicalRichTextFieldProps } from '../types.js'
type Args = {
clientFieldSchemaMap: ClientFieldSchemaMap
fieldSchemaMap: FieldSchemaMap
i18n: I18nClient
path: string
@@ -27,9 +22,6 @@ export function initLexicalFeatures(args: Args): {
} {
const clientFeatures: LexicalRichTextFieldProps['clientFeatures'] = {}
const fieldSchemaMap = Object.fromEntries(new Map(args.fieldSchemaMap))
//&const value = deepCopyObjectSimple(args.fieldState.value)
// turn args.resolvedFeatureMap into an array of [key, value] pairs, ordered by value.order, lowest order first:
const resolvedFeatureMapArray = Array.from(
args.sanitizedEditorConfig.resolvedFeatureMap.entries(),
@@ -84,64 +76,14 @@ export function initLexicalFeatures(args: Args): {
featureKey,
].join('.')
const featurePath = [...args.path.split('.'), 'lexical_internal_feature', featureKey].join(
'.',
)
// Like args.fieldSchemaMap, we only want to include the sub-fields of the current feature
const featureSchemaMap: typeof fieldSchemaMap = {}
for (const key in fieldSchemaMap) {
const state = fieldSchemaMap[key]
if (key.startsWith(featureSchemaPath)) {
featureSchemaMap[key] = state
}
}
featureClientSchemaMap[featureKey] = {}
for (const key in featureSchemaMap) {
const state = featureSchemaMap[key]
const clientFields = createClientFields({
clientFields: ('fields' in state
? deepCopyObjectSimple(state.fields)
: [deepCopyObjectSimple(state)]) as ClientField[],
defaultIDType: args.payload.config.db.defaultIDType,
disableAddingID: true,
fields: 'fields' in state ? state.fields : [state],
i18n: args.i18n,
importMap: args.payload.importMap,
})
featureClientSchemaMap[featureKey][key] = clientFields
}
/*
This is for providing an initial form state. Right now we only want to provide the clientfields though
const schemaMap: {
[key: string]: FieldState
} = {}
const lexicalDeepIterate = (editorState) => {
console.log('STATE', editorState)
if (
editorState &&
typeof editorState === 'object' &&
'children' in editorState &&
Array.isArray(editorState.children)
) {
for (const childKey in editorState.children) {
const childState = editorState.children[childKey]
if (childState && typeof childState === 'object') {
lexicalDeepIterate(childState)
}
}
// Like args.fieldSchemaMap, we only want to include the sub-fields of the current feature
for (const [key, entry] of args.clientFieldSchemaMap.entries()) {
if (key.startsWith(featureSchemaPath)) {
featureClientSchemaMap[featureKey][key] = 'fields' in entry ? entry.fields : [entry]
}
}
lexicalDeepIterate(value.root)*/
}
}
return {

View File

@@ -8,7 +8,7 @@ import type {
} from 'payload'
import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent'
import { createClientFields, deepCopyObjectSimple } from 'payload'
import { createClientFields } from 'payload'
import React from 'react'
import type { AdapterArguments, RichTextCustomElement, RichTextCustomLeaf } from '../types.js'
@@ -134,11 +134,7 @@ export const RscEntrySlateField: React.FC<
switch (element.name) {
case 'link': {
let clientFields = deepCopyObjectSimple(
args.admin?.link?.fields,
) as unknown as ClientField[]
clientFields = createClientFields({
clientFields,
const clientFields = createClientFields({
defaultIDType: payload.config.db.defaultIDType,
fields: args.admin?.link?.fields as Field[],
i18n,
@@ -166,11 +162,7 @@ export const RscEntrySlateField: React.FC<
uploadEnabledCollections.forEach((collection) => {
if (args?.admin?.upload?.collections[collection.slug]?.fields) {
let clientFields = deepCopyObjectSimple(
args?.admin?.upload?.collections[collection.slug]?.fields,
) as unknown as ClientField[]
clientFields = createClientFields({
clientFields,
const clientFields = createClientFields({
defaultIDType: payload.config.db.defaultIDType,
fields: args?.admin?.upload?.collections[collection.slug]?.fields,
i18n,

View File

@@ -53,6 +53,11 @@
"types": "./src/utilities/buildTableState.ts",
"default": "./src/utilities/buildTableState.ts"
},
"./utilities/getClientConfig": {
"import": "./src/utilities/getClientConfig.ts",
"types": "./src/utilities/getClientConfig.ts",
"default": "./src/utilities/getClientConfig.ts"
},
"./utilities/buildFieldSchemaMap/traverseFields": {
"import": "./src/utilities/buildFieldSchemaMap/traverseFields.ts",
"types": "./src/utilities/buildFieldSchemaMap/traverseFields.ts",

View File

@@ -1,4 +1,5 @@
import type {
ClientFieldSchemaMap,
Data,
DocumentPreferences,
Field,
@@ -35,6 +36,7 @@ export type AddFieldStatePromiseArgs = {
* if all parents are localized, then the field is localized
*/
anyParentLocalized?: boolean
clientFieldSchemaMap?: ClientFieldSchemaMap
collectionSlug?: string
data: Data
field: Field
@@ -93,6 +95,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
id,
addErrorPathToParent: addErrorPathToParentArg,
anyParentLocalized = false,
clientFieldSchemaMap,
collectionSlug,
data,
field,
@@ -119,6 +122,12 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
state,
} = args
if (!args.clientFieldSchemaMap && args.renderFieldFn) {
console.warn(
'clientFieldSchemaMap is not passed to addFieldStatePromise - this will reduce performance',
)
}
const { indexPath, path, schemaPath } = getFieldPaths({
field,
index: fieldIndex,
@@ -237,6 +246,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
id,
addErrorPathToParent,
anyParentLocalized: field.localized || anyParentLocalized,
clientFieldSchemaMap,
collectionSlug,
data: row,
fields: field.fields,
@@ -370,6 +380,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
id,
addErrorPathToParent,
anyParentLocalized: field.localized || anyParentLocalized,
clientFieldSchemaMap,
collectionSlug,
data: row,
fields: block.fields,
@@ -460,6 +471,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
id,
addErrorPathToParent,
anyParentLocalized: field.localized || anyParentLocalized,
clientFieldSchemaMap,
collectionSlug,
data: data?.[field.name] || {},
fields: field.fields,
@@ -605,6 +617,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
// passthrough parent functionality
addErrorPathToParent: addErrorPathToParentArg,
anyParentLocalized: field.localized || anyParentLocalized,
clientFieldSchemaMap,
collectionSlug,
data,
fields: field.fields,
@@ -668,6 +681,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
id,
addErrorPathToParent: addErrorPathToParentArg,
anyParentLocalized: tab.localized || anyParentLocalized,
clientFieldSchemaMap,
collectionSlug,
data: isNamedTab ? data?.[tab.name] || {} : data,
fields: tab.fields,
@@ -733,6 +747,7 @@ export const addFieldStatePromise = async (args: AddFieldStatePromiseArgs): Prom
renderFieldFn({
id,
clientFieldSchemaMap,
collectionSlug,
data: fullData,
fieldConfig: fieldConfig as Field,

View File

@@ -1,4 +1,5 @@
import type {
ClientFieldSchemaMap,
Data,
DocumentPreferences,
Field,
@@ -15,6 +16,12 @@ import { calculateDefaultValues } from './calculateDefaultValues/index.js'
import { iterateFields } from './iterateFields.js'
type Args = {
/**
* The client field schema map is required for field rendering.
* If fields should not be rendered (=> `renderFieldFn` is not provided),
* then the client field schema map is not required.
*/
clientFieldSchemaMap?: ClientFieldSchemaMap
collectionSlug?: string
data?: Data
fields: Field[] | undefined
@@ -40,12 +47,19 @@ type Args = {
renderAllFields: boolean
renderFieldFn?: RenderFieldMethod
req: PayloadRequest
schemaPath: string
}
export const fieldSchemasToFormState = async (args: Args): Promise<FormState> => {
if (!args.clientFieldSchemaMap && args.renderFieldFn) {
console.warn(
'clientFieldSchemaMap is not passed to fieldSchemasToFormState - this will reduce performance',
)
}
const {
id,
clientFieldSchemaMap,
collectionSlug,
data = {},
fields,
@@ -77,6 +91,7 @@ export const fieldSchemasToFormState = async (args: Args): Promise<FormState> =>
await iterateFields({
id,
addErrorPathToParent: null,
clientFieldSchemaMap,
collectionSlug,
data: dataWithDefaultValues,
fields,

View File

@@ -1,4 +1,5 @@
import type {
ClientFieldSchemaMap,
Data,
DocumentPreferences,
Field as FieldSchema,
@@ -20,6 +21,7 @@ type Args = {
* if any parents is localized, then the field is localized. @default false
*/
anyParentLocalized?: boolean
clientFieldSchemaMap?: ClientFieldSchemaMap
collectionSlug?: string
data: Data
fields: FieldSchema[]
@@ -71,6 +73,7 @@ export const iterateFields = async ({
id,
addErrorPathToParent: addErrorPathToParentArg,
anyParentLocalized = false,
clientFieldSchemaMap,
collectionSlug,
data,
fields,
@@ -112,6 +115,7 @@ export const iterateFields = async ({
id,
addErrorPathToParent: addErrorPathToParentArg,
anyParentLocalized,
clientFieldSchemaMap,
collectionSlug,
data,
field,

View File

@@ -1,7 +1,7 @@
import type { ClientComponentProps, ClientField, FieldPaths, ServerComponentProps } from 'payload'
import { getTranslation } from '@payloadcms/translations'
import { createClientField, deepCopyObjectSimple, MissingEditorProp } from 'payload'
import { createClientField, MissingEditorProp } from 'payload'
import type { RenderFieldMethod } from './types.js'
@@ -18,6 +18,7 @@ const defaultUIFieldComponentKeys: Array<'Cell' | 'Description' | 'Field' | 'Fil
export const renderField: RenderFieldMethod = ({
id,
clientFieldSchemaMap,
collectionSlug,
data,
fieldConfig,
@@ -35,13 +36,14 @@ export const renderField: RenderFieldMethod = ({
schemaPath,
siblingData,
}) => {
const clientField = createClientField({
clientField: deepCopyObjectSimple(fieldConfig) as ClientField,
defaultIDType: req.payload.config.db.defaultIDType,
field: fieldConfig,
i18n: req.i18n,
importMap: req.payload.importMap,
})
const clientField = clientFieldSchemaMap
? (clientFieldSchemaMap.get(schemaPath) as ClientField)
: createClientField({
defaultIDType: req.payload.config.db.defaultIDType,
field: fieldConfig,
i18n: req.i18n,
importMap: req.payload.importMap,
})
const clientProps: ClientComponentProps & Partial<FieldPaths> = {
customComponents: fieldState?.customComponents || {},
@@ -61,6 +63,7 @@ export const renderField: RenderFieldMethod = ({
const serverProps: ServerComponentProps = {
id,
clientField,
clientFieldSchemaMap,
data,
field: fieldConfig,
fieldSchemaMap,

View File

@@ -1,4 +1,5 @@
import type {
ClientFieldSchemaMap,
Data,
DocumentPreferences,
Field,
@@ -11,6 +12,7 @@ import type {
} from 'payload'
export type RenderFieldArgs = {
clientFieldSchemaMap?: ClientFieldSchemaMap
collectionSlug: string
data: Data
fieldConfig: Field

View File

@@ -14,7 +14,7 @@ const LocaleContext = createContext({} as Locale)
export const LocaleProvider: React.FC<{ children?: React.ReactNode }> = ({ children }) => {
const {
config: { localization },
config: { localization = false },
} = useConfig()
const { user } = useAuth()

View File

@@ -0,0 +1,94 @@
import type { I18n } from '@payloadcms/translations'
import type {
ClientConfig,
ClientField,
ClientFieldSchemaMap,
FieldSchemaMap,
Payload,
TextFieldClient,
} from 'payload'
import { traverseFields } from './traverseFields.js'
const baseAuthFields: ClientField[] = [
{
name: 'password',
type: 'text',
required: true,
},
{
name: 'confirm-password',
type: 'text',
required: true,
},
]
/**
* Flattens the config fields into a map of field schemas
*/
export const buildClientFieldSchemaMap = (args: {
collectionSlug?: string
config: ClientConfig
globalSlug?: string
i18n: I18n
payload: Payload
schemaMap: FieldSchemaMap
}): { clientFieldSchemaMap: ClientFieldSchemaMap } => {
const { collectionSlug, config, globalSlug, i18n, payload, schemaMap } = args
const clientSchemaMap: ClientFieldSchemaMap = new Map()
if (collectionSlug) {
const matchedCollection = config.collections.find(
(collection) => collection.slug === collectionSlug,
)
if (matchedCollection) {
if (matchedCollection.auth && !matchedCollection.auth.disableLocalStrategy) {
// register schema with auth schemaPath
;(baseAuthFields[0] as TextFieldClient).label = i18n.t('general:password')
;(baseAuthFields[1] as TextFieldClient).label = i18n.t('authentication:confirmPassword')
clientSchemaMap.set(`_${matchedCollection.slug}.auth`, {
fields: [...baseAuthFields, ...matchedCollection.fields],
})
}
clientSchemaMap.set(collectionSlug, {
fields: matchedCollection.fields,
})
traverseFields({
clientSchemaMap,
config,
fields: matchedCollection.fields,
i18n,
parentIndexPath: '',
parentSchemaPath: collectionSlug,
payload,
schemaMap,
})
}
} else if (globalSlug) {
const matchedGlobal = config.globals.find((global) => global.slug === globalSlug)
if (matchedGlobal) {
clientSchemaMap.set(globalSlug, {
fields: matchedGlobal.fields,
})
traverseFields({
clientSchemaMap,
config,
fields: matchedGlobal.fields,
i18n,
parentIndexPath: '',
parentSchemaPath: globalSlug,
payload,
schemaMap,
})
}
}
return { clientFieldSchemaMap: clientSchemaMap }
}

View File

@@ -0,0 +1,154 @@
import type { I18n } from '@payloadcms/translations'
import {
type ClientConfig,
type ClientField,
type ClientFieldSchemaMap,
createClientFields,
type FieldSchemaMap,
type Payload,
} from 'payload'
import { getFieldPaths, tabHasName } from 'payload/shared'
type Args = {
clientSchemaMap: ClientFieldSchemaMap
config: ClientConfig
fields: ClientField[]
i18n: I18n<any, any>
parentIndexPath: string
parentSchemaPath: string
payload: Payload
schemaMap: FieldSchemaMap
}
export const traverseFields = ({
clientSchemaMap,
config,
fields,
i18n,
parentIndexPath,
parentSchemaPath,
payload,
schemaMap,
}: Args) => {
for (const [index, field] of fields.entries()) {
const { indexPath, schemaPath } = getFieldPaths({
field,
index,
parentIndexPath: 'name' in field ? '' : parentIndexPath,
parentPath: '',
parentSchemaPath,
})
clientSchemaMap.set(schemaPath, field)
switch (field.type) {
case 'array':
case 'group':
traverseFields({
clientSchemaMap,
config,
fields: field.fields,
i18n,
parentIndexPath: '',
parentSchemaPath: schemaPath,
payload,
schemaMap,
})
break
case 'blocks':
field.blocks.map((block) => {
const blockSchemaPath = `${schemaPath}.${block.slug}`
clientSchemaMap.set(blockSchemaPath, block)
traverseFields({
clientSchemaMap,
config,
fields: block.fields,
i18n,
parentIndexPath: '',
parentSchemaPath: blockSchemaPath,
payload,
schemaMap,
})
})
break
case 'collapsible':
case 'row':
traverseFields({
clientSchemaMap,
config,
fields: field.fields,
i18n,
parentIndexPath: indexPath,
parentSchemaPath,
payload,
schemaMap,
})
break
case 'richText': {
// richText sub-fields are not part of the ClientConfig or the Config.
// They only exist in the field schema map.
// Thus, we need to
// 1. get them from the field schema map
// 2. convert them to client fields
// 3. add them to the client schema map
// So these would basically be all fields that are not part of the client config already
const richTextFieldSchemaMap: FieldSchemaMap = new Map()
for (const [path, subField] of schemaMap.entries()) {
if (path.startsWith(`${schemaPath}.`)) {
richTextFieldSchemaMap.set(path, subField)
}
}
// Now loop through them, convert each entry to a client field and add it to the client schema map
for (const [path, subField] of richTextFieldSchemaMap.entries()) {
const clientFields = createClientFields({
defaultIDType: payload.config.db.defaultIDType,
disableAddingID: true,
fields: 'fields' in subField ? subField.fields : [subField],
i18n,
importMap: payload.importMap,
})
clientSchemaMap.set(path, {
fields: clientFields,
})
}
break
}
case 'tabs':
field.tabs.map((tab, tabIndex) => {
const { indexPath: tabIndexPath, schemaPath: tabSchemaPath } = getFieldPaths({
field: {
...tab,
type: 'tab',
},
index: tabIndex,
parentIndexPath: indexPath,
parentPath: '',
parentSchemaPath,
})
clientSchemaMap.set(tabSchemaPath, tab)
traverseFields({
clientSchemaMap,
config,
fields: tab.fields,
i18n,
parentIndexPath: tabHasName(tab) ? '' : tabIndexPath,
parentSchemaPath: tabHasName(tab) ? tabSchemaPath : parentSchemaPath,
payload,
schemaMap,
})
})
break
}
}
}

View File

@@ -1,9 +1,25 @@
import type { I18n } from '@payloadcms/translations'
import type { Field, FieldSchemaMap, SanitizedConfig } from 'payload'
import type { Field, FieldSchemaMap, SanitizedConfig, TextField } from 'payload'
import { confirmPassword, password } from 'payload/shared'
import { traverseFields } from './traverseFields.js'
const baseAuthFields: Field[] = [
{
name: 'password',
type: 'text',
required: true,
validate: password,
},
{
name: 'confirm-password',
type: 'text',
required: true,
validate: confirmPassword,
},
]
/**
* Flattens the config fields into a map of field schemas
*/
@@ -25,22 +41,8 @@ export const buildFieldSchemaMap = (args: {
if (matchedCollection) {
if (matchedCollection.auth && !matchedCollection.auth.disableLocalStrategy) {
// register schema with auth schemaPath
const baseAuthFields: Field[] = [
{
name: 'password',
type: 'text',
label: i18n.t('general:password'),
required: true,
validate: password,
},
{
name: 'confirm-password',
type: 'text',
label: i18n.t('authentication:confirmPassword'),
required: true,
validate: confirmPassword,
},
]
;(baseAuthFields[0] as TextField).label = i18n.t('general:password')
;(baseAuthFields[1] as TextField).label = i18n.t('authentication:confirmPassword')
schemaMap.set(`_${matchedCollection.slug}.auth`, {
fields: [...baseAuthFields, ...matchedCollection.fields],

View File

@@ -63,7 +63,6 @@ export const traverseFields = ({
break
case 'collapsible':
case 'row':
traverseFields({
config,

View File

@@ -1,65 +1,15 @@
import type { I18n, I18nClient } from '@payloadcms/translations'
import type {
BuildFormStateArgs,
ClientConfig,
ClientUser,
ErrorResult,
FieldSchemaMap,
FormState,
SanitizedConfig,
} from 'payload'
import type { BuildFormStateArgs, ClientConfig, ClientUser, ErrorResult, FormState } from 'payload'
import { formatErrors } from 'payload'
import { reduceFieldsToValues } from 'payload/shared'
import { fieldSchemasToFormState } from '../forms/fieldSchemasToFormState/index.js'
import { renderField } from '../forms/fieldSchemasToFormState/renderField.js'
import { buildFieldSchemaMap } from './buildFieldSchemaMap/index.js'
import { getClientConfig } from './getClientConfig.js'
import { getClientSchemaMap } from './getClientSchemaMap.js'
import { getSchemaMap } from './getSchemaMap.js'
import { handleFormStateLocking } from './handleFormStateLocking.js'
let cachedFieldMap = global._payload_fieldMap
let cachedClientConfig = global._payload_clientConfig
if (!cachedFieldMap) {
cachedFieldMap = global._payload_fieldMap = null
}
if (!cachedClientConfig) {
cachedClientConfig = global._payload_clientConfig = null
}
export const getFieldSchemaMap = (args: {
collectionSlug?: string
config: SanitizedConfig
globalSlug?: string
i18n: I18nClient
}): FieldSchemaMap => {
const { collectionSlug, config, globalSlug, i18n } = args
if (process.env.NODE_ENV !== 'development') {
if (!cachedFieldMap) {
cachedFieldMap = new Map()
}
const cachedEntityFieldMap = cachedFieldMap.get(collectionSlug || globalSlug)
if (cachedEntityFieldMap) {
return cachedEntityFieldMap
}
}
const { fieldSchemaMap: entityFieldMap } = buildFieldSchemaMap({
collectionSlug,
config,
globalSlug,
i18n: i18n as I18n,
})
if (process.env.NODE_ENV !== 'development') {
cachedFieldMap.set(collectionSlug || globalSlug, entityFieldMap)
}
return entityFieldMap
}
type BuildFormStateSuccessResult = {
clientConfig?: ClientConfig
errors?: never
@@ -167,15 +117,24 @@ export const buildFormState = async (
throw new Error('Either collectionSlug or globalSlug must be provided')
}
const fieldSchemaMap = getFieldSchemaMap({
const schemaMap = getSchemaMap({
collectionSlug,
config,
globalSlug,
i18n,
})
const clientSchemaMap = getClientSchemaMap({
collectionSlug,
config: getClientConfig({ config, i18n, importMap: req.payload.importMap }),
globalSlug,
i18n,
payload,
schemaMap,
})
const id = collectionSlug ? idFromArgs : undefined
const fieldOrEntityConfig = fieldSchemaMap.get(schemaPath)
const fieldOrEntityConfig = schemaMap.get(schemaPath)
if (!fieldOrEntityConfig) {
throw new Error(`Could not find "${schemaPath}" in the fieldSchemaMap`)
@@ -216,10 +175,11 @@ export const buildFormState = async (
const formStateResult = await fieldSchemasToFormState({
id,
clientFieldSchemaMap: clientSchemaMap,
collectionSlug,
data,
fields,
fieldSchemaMap,
fieldSchemaMap: schemaMap,
operation,
permissions: docPermissions?.fields || {},
preferences: docPreferences || { fields: {} },

View File

@@ -1,49 +1,21 @@
import type { I18nClient } from '@payloadcms/translations'
import type {
BuildTableStateArgs,
ClientCollectionConfig,
ClientConfig,
ErrorResult,
ImportMap,
PaginatedDocs,
SanitizedCollectionConfig,
SanitizedConfig,
} from 'payload'
import { dequal } from 'dequal' // TODO: Can we change this to dequal/lite ? If not, please add comment explaining why
import { createClientConfig, formatErrors } from 'payload'
import { formatErrors } from 'payload'
import type { Column } from '../elements/Table/index.js'
import type { ListPreferences } from '../elements/TableColumns/index.js'
import { getClientConfig } from './getClientConfig.js'
import { renderFilters, renderTable } from './renderTable.js'
let cachedClientConfig = global._payload_clientConfig
if (!cachedClientConfig) {
cachedClientConfig = global._payload_clientConfig = null
}
export const getClientConfig = (args: {
config: SanitizedConfig
i18n: I18nClient
importMap: ImportMap
}): ClientConfig => {
const { config, i18n, importMap } = args
if (cachedClientConfig && process.env.NODE_ENV !== 'development') {
return cachedClientConfig
}
cachedClientConfig = createClientConfig({
config,
i18n,
importMap,
})
return cachedClientConfig
}
type BuildTableStateSuccessResult = {
clientConfig?: ClientConfig
data: PaginatedDocs

View File

@@ -0,0 +1,31 @@
import type { I18nClient } from '@payloadcms/translations'
import type { ClientConfig, ImportMap, SanitizedConfig } from 'payload'
import { createClientConfig } from 'payload'
import { cache } from 'react'
let cachedClientConfig = global._payload_clientConfig
if (!cachedClientConfig) {
cachedClientConfig = global._payload_clientConfig = null
}
export const getClientConfig = cache(
(args: { config: SanitizedConfig; i18n: I18nClient; importMap: ImportMap }): ClientConfig => {
if (cachedClientConfig && !global._payload_doNotCacheClientConfig) {
return cachedClientConfig
}
const { config, i18n, importMap } = args
cachedClientConfig = createClientConfig({
config,
i18n,
importMap,
})
global._payload_clientConfig = cachedClientConfig
global._payload_doNotCacheClientConfig = false
return cachedClientConfig
},
)

View File

@@ -0,0 +1,54 @@
import type { I18n, I18nClient } from '@payloadcms/translations'
import type { ClientConfig, ClientFieldSchemaMap, FieldSchemaMap, Payload } from 'payload'
import { cache } from 'react'
import { buildClientFieldSchemaMap } from './buildClientFieldSchemaMap/index.js'
let cachedClientSchemaMap = global._payload_clientSchemaMap
if (!cachedClientSchemaMap) {
cachedClientSchemaMap = global._payload_clientSchemaMap = null
}
export const getClientSchemaMap = cache(
(args: {
collectionSlug?: string
config: ClientConfig
globalSlug?: string
i18n: I18nClient
payload: Payload
schemaMap: FieldSchemaMap
}): ClientFieldSchemaMap => {
const { collectionSlug, config, globalSlug, i18n, payload, schemaMap } = args
if (!cachedClientSchemaMap || global._payload_doNotCacheClientSchemaMap) {
cachedClientSchemaMap = new Map()
}
let cachedEntityClientFieldMap = cachedClientSchemaMap.get(collectionSlug || globalSlug)
if (cachedEntityClientFieldMap) {
return cachedEntityClientFieldMap
}
cachedEntityClientFieldMap = new Map()
const { clientFieldSchemaMap: entityClientFieldMap } = buildClientFieldSchemaMap({
collectionSlug,
config,
globalSlug,
i18n: i18n as I18n,
payload,
schemaMap,
})
cachedClientSchemaMap.set(collectionSlug || globalSlug, entityClientFieldMap)
global._payload_clientSchemaMap = cachedClientSchemaMap
global._payload_doNotCacheClientSchemaMap = false
return entityClientFieldMap
},
)

View File

@@ -0,0 +1,50 @@
import type { I18n, I18nClient } from '@payloadcms/translations'
import type { FieldSchemaMap, SanitizedConfig } from 'payload'
import { cache } from 'react'
import { buildFieldSchemaMap } from './buildFieldSchemaMap/index.js'
let cachedSchemaMap = global._payload_schemaMap
if (!cachedSchemaMap) {
cachedSchemaMap = global._payload_schemaMap = null
}
export const getSchemaMap = cache(
(args: {
collectionSlug?: string
config: SanitizedConfig
globalSlug?: string
i18n: I18nClient
}): FieldSchemaMap => {
const { collectionSlug, config, globalSlug, i18n } = args
if (!cachedSchemaMap || global._payload_doNotCacheSchemaMap) {
cachedSchemaMap = new Map()
}
let cachedEntityFieldMap = cachedSchemaMap.get(collectionSlug || globalSlug)
if (cachedEntityFieldMap) {
return cachedEntityFieldMap
}
cachedEntityFieldMap = new Map()
const { fieldSchemaMap: entityFieldMap } = buildFieldSchemaMap({
collectionSlug,
config,
globalSlug,
i18n: i18n as I18n,
})
cachedSchemaMap.set(collectionSlug || globalSlug, entityFieldMap)
global._payload_schemaMap = cachedSchemaMap
global._payload_doNotCacheSchemaMap = false
return entityFieldMap
},
)

View File

@@ -1,4 +1,4 @@
import type { SanitizedCollectionConfig, VerifyConfig } from 'payload'
import type { SanitizedCollectionConfig } from 'payload'
export type Props = {
className?: string
@@ -13,5 +13,5 @@ export type Props = {
setValidateBeforeSubmit: (validate: boolean) => void
useAPIKey?: boolean
username: string
verify?: boolean | VerifyConfig
verify?: boolean
}

View File

@@ -11,7 +11,7 @@ import { MenuGlobal } from './globals/Menu/index.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export default buildConfigWithDefaults({
export default buildConfigWithDefaults(() => ({
// ...extend config here
collections: [PostsCollection, MediaCollection],
admin: {
@@ -43,4 +43,4 @@ export default buildConfigWithDefaults({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})
}))

View File

@@ -40,9 +40,9 @@ export interface Config {
user: User & {
collection: 'users';
};
jobs?: {
jobs: {
tasks: unknown;
workflows?: unknown;
workflows: unknown;
};
}
export interface UserAuthOperations {

View File

@@ -66,7 +66,7 @@ function isUser(user: Config['user']): user is {
return user?.collection === 'users'
}
export default buildConfigWithDefaults({
export default buildConfigWithDefaults(() => ({
admin: {
autoLogin: false,
user: 'users',
@@ -706,4 +706,4 @@ export default buildConfigWithDefaults({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})
}))

View File

@@ -10,7 +10,7 @@ import { adminRoute } from './shared.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export default buildConfigWithDefaults({
export default buildConfigWithDefaults(() => ({
collections: [PostsCollection],
admin: {
autoLogin: {
@@ -47,4 +47,4 @@ export default buildConfigWithDefaults({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})
}))

View File

@@ -39,7 +39,7 @@ import {
publicCustomViewPath,
} from './shared.js'
export default buildConfigWithDefaults({
export default buildConfigWithDefaults(() => ({
admin: {
importMap: {
baseDir: path.resolve(dirname),
@@ -203,4 +203,4 @@ export default buildConfigWithDefaults({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})
}))

View File

@@ -6,7 +6,7 @@ import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
import { devUser } from '../credentials.js'
import { arraySlug } from './shared.js'
export default buildConfigWithDefaults({
export default buildConfigWithDefaults(() => ({
admin: {
importMap: {
baseDir: path.resolve(dirname),
@@ -64,4 +64,4 @@ export default buildConfigWithDefaults({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})
}))

19
test/auth-basic/config.ts Normal file
View File

@@ -0,0 +1,19 @@
import { fileURLToPath } from 'node:url'
import path from 'path'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
// eslint-disable-next-line no-restricted-exports
export default buildConfigWithDefaults(() => ({
admin: {
autoLogin: false,
importMap: {
baseDir: path.resolve(dirname),
},
},
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
}))

152
test/auth-basic/e2e.spec.ts Normal file
View File

@@ -0,0 +1,152 @@
import type { Page } from '@playwright/test'
import type { SanitizedConfig } from 'payload'
import { expect, test } from '@playwright/test'
import { devUser } from 'credentials.js'
import path from 'path'
import { fileURLToPath } from 'url'
import type { PayloadTestSDK } from '../helpers/sdk/index.js'
import type { Config } from './payload-types.js'
import { ensureCompilationIsDone, getRoutes, initPageConsoleErrorCatch } from '../helpers.js'
import { AdminUrlUtil } from '../helpers/adminUrlUtil.js'
import { initPayloadE2ENoConfig } from '../helpers/initPayloadE2ENoConfig.js'
import { reInitializeDB } from '../helpers/reInitializeDB.js'
import { POLL_TOPASS_TIMEOUT, TEST_TIMEOUT_LONG } from '../playwright.config.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
let payload: PayloadTestSDK<Config>
const { beforeAll, beforeEach, describe } = test
const createFirstUser = async ({
page,
serverURL,
}: {
customAdminRoutes?: SanitizedConfig['admin']['routes']
customRoutes?: SanitizedConfig['routes']
page: Page
serverURL: string
}) => {
const {
admin: {
routes: { createFirstUser: createFirstUserRoute },
},
routes: { admin: adminRoute },
} = getRoutes({})
// wait for create first user route
await page.goto(serverURL + `${adminRoute}${createFirstUserRoute}`)
// forget to fill out confirm password
await page.locator('#field-email').fill(devUser.email)
await page.locator('#field-password').fill(devUser.password)
await page.locator('.form-submit > button').click()
await expect(page.locator('.field-type.confirm-password .field-error')).toHaveText(
'This field is required.',
)
// make them match, but does not pass password validation
await page.locator('#field-email').fill(devUser.email)
await page.locator('#field-password').fill('12')
await page.locator('#field-confirm-password').fill('12')
await page.locator('.form-submit > button').click()
await expect(page.locator('.field-type.password .field-error')).toHaveText(
'This value must be longer than the minimum length of 3 characters.',
)
await page.locator('#field-email').fill(devUser.email)
await page.locator('#field-password').fill(devUser.password)
await page.locator('#field-confirm-password').fill(devUser.password)
await page.locator('.form-submit > button').click()
await expect
.poll(() => page.url(), { timeout: POLL_TOPASS_TIMEOUT })
.not.toContain('create-first-user')
}
describe('auth-basic', () => {
let page: Page
let url: AdminUrlUtil
let serverURL: string
let apiURL: string
beforeAll(async ({ browser }, testInfo) => {
testInfo.setTimeout(TEST_TIMEOUT_LONG)
;({ payload, serverURL } = await initPayloadE2ENoConfig<Config>({ dirname }))
apiURL = `${serverURL}/api`
url = new AdminUrlUtil(serverURL, 'users')
const context = await browser.newContext()
page = await context.newPage()
initPageConsoleErrorCatch(page)
await ensureCompilationIsDone({
page,
serverURL,
readyURL: `${serverURL}/admin/**`,
noAutoLogin: true,
})
// Undo onInit seeding, as we need to test this without having a user created, or testing create-first-user
await reInitializeDB({
serverURL,
snapshotKey: 'auth-basic',
deleteOnly: true,
})
await payload.delete({
collection: 'users',
where: {
id: {
exists: true,
},
},
})
await ensureCompilationIsDone({
page,
serverURL,
readyURL: `${serverURL}/admin/create-first-user`,
})
})
beforeEach(async () => {
await payload.delete({
collection: 'users',
where: {
id: {
exists: true,
},
},
})
})
describe('unauthenticated users', () => {
test('ensure create first user page only has 3 fields', async () => {
await page.goto(url.admin + '/create-first-user')
// Ensure there are only 2 elements with class field-type
await expect(page.locator('.field-type')).toHaveCount(3) // Email, Password, Confirm Password
})
test('ensure first user can be created', async () => {
await createFirstUser({ page, serverURL })
// use the api key in a fetch to assert that it is disabled
await expect(async () => {
const users = await payload.find({
collection: 'users',
})
expect(users.totalDocs).toBe(1)
expect(users.docs[0].email).toBe(devUser.email)
}).toPass({
timeout: POLL_TOPASS_TIMEOUT,
})
})
})
})

View File

@@ -0,0 +1,186 @@
/* tslint:disable */
/* eslint-disable */
/**
* This file was automatically generated by Payload.
* DO NOT MODIFY IT BY HAND. Instead, modify your source Payload config,
* and re-run `payload generate:types` to regenerate this file.
*/
export interface Config {
auth: {
users: UserAuthOperations;
};
collections: {
users: User;
'payload-locked-documents': PayloadLockedDocument;
'payload-preferences': PayloadPreference;
'payload-migrations': PayloadMigration;
};
collectionsJoins: {};
collectionsSelect: {
users: UsersSelect<false> | UsersSelect<true>;
'payload-locked-documents': PayloadLockedDocumentsSelect<false> | PayloadLockedDocumentsSelect<true>;
'payload-preferences': PayloadPreferencesSelect<false> | PayloadPreferencesSelect<true>;
'payload-migrations': PayloadMigrationsSelect<false> | PayloadMigrationsSelect<true>;
};
db: {
defaultIDType: string;
};
globals: {};
globalsSelect: {};
locale: null;
user: User & {
collection: 'users';
};
jobs: {
tasks: unknown;
workflows: unknown;
};
}
export interface UserAuthOperations {
forgotPassword: {
email: string;
password: string;
};
login: {
email: string;
password: string;
};
registerFirstUser: {
email: string;
password: string;
};
unlock: {
email: string;
password: string;
};
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users".
*/
export interface User {
id: string;
updatedAt: string;
createdAt: string;
email: string;
resetPasswordToken?: string | null;
resetPasswordExpiration?: string | null;
salt?: string | null;
hash?: string | null;
loginAttempts?: number | null;
lockUntil?: string | null;
password?: string | null;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents".
*/
export interface PayloadLockedDocument {
id: string;
document?: {
relationTo: 'users';
value: string | User;
} | null;
globalSlug?: string | null;
user: {
relationTo: 'users';
value: string | User;
};
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences".
*/
export interface PayloadPreference {
id: string;
user: {
relationTo: 'users';
value: string | User;
};
key?: string | null;
value?:
| {
[k: string]: unknown;
}
| unknown[]
| string
| number
| boolean
| null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations".
*/
export interface PayloadMigration {
id: string;
name?: string | null;
batch?: number | null;
updatedAt: string;
createdAt: string;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "users_select".
*/
export interface UsersSelect<T extends boolean = true> {
updatedAt?: T;
createdAt?: T;
email?: T;
resetPasswordToken?: T;
resetPasswordExpiration?: T;
salt?: T;
hash?: T;
loginAttempts?: T;
lockUntil?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-locked-documents_select".
*/
export interface PayloadLockedDocumentsSelect<T extends boolean = true> {
document?: T;
globalSlug?: T;
user?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-preferences_select".
*/
export interface PayloadPreferencesSelect<T extends boolean = true> {
user?: T;
key?: T;
value?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "payload-migrations_select".
*/
export interface PayloadMigrationsSelect<T extends boolean = true> {
name?: T;
batch?: T;
updatedAt?: T;
createdAt?: T;
}
/**
* This interface was referenced by `Config`'s JSON-Schema
* via the `definition` "auth".
*/
export interface Auth {
[k: string]: unknown;
}
declare module 'payload' {
// @ts-ignore
export interface GeneratedTypes extends Config {}
}

View File

@@ -0,0 +1,13 @@
{
// extend your base config to share compilerOptions, etc
//"extends": "./tsconfig.json",
"compilerOptions": {
// ensure that nobody can accidentally use this config for a build
"noEmit": true
},
"include": [
// whatever paths you intend to lint
"./**/*.ts",
"./**/*.tsx"
]
}

View File

@@ -0,0 +1,3 @@
{
"extends": "../tsconfig.json"
}

View File

@@ -8,7 +8,7 @@ import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
import { devUser } from '../credentials.js'
import { apiKeysSlug, namedSaveToJWTValue, saveToJWTKey, slug } from './shared.js'
export default buildConfigWithDefaults({
export default buildConfigWithDefaults(() => ({
admin: {
autoLogin: {
email: devUser.email,
@@ -253,4 +253,4 @@ export default buildConfigWithDefaults({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})
}))

View File

@@ -41,136 +41,139 @@ import { testEmailAdapter } from './testEmailAdapter.js'
// process.env.PAYLOAD_DATABASE = 'sqlite'
export async function buildConfigWithDefaults(
testConfig?: Partial<Config>,
testConfigFn?: () => Partial<Config>,
options?: {
disableAutoLogin?: boolean
},
): Promise<SanitizedConfig> {
const config: Config = {
db: databaseAdapter,
editor: lexicalEditor({
features: [
ParagraphFeature(),
RelationshipFeature(),
LinkFeature({
fields: ({ defaultFields }) => [
...defaultFields,
{
name: 'description',
type: 'text',
},
],
}),
ChecklistFeature(),
UnorderedListFeature(),
OrderedListFeature(),
AlignFeature(),
BlockquoteFeature(),
BoldFeature(),
ItalicFeature(),
UploadFeature({
collections: {
media: {
fields: [
{
name: 'alt',
type: 'text',
},
],
},
},
}),
UnderlineFeature(),
StrikethroughFeature(),
SubscriptFeature(),
SuperscriptFeature(),
InlineCodeFeature(),
InlineToolbarFeature(),
TreeViewFeature(),
HeadingFeature(),
IndentFeature(),
BlocksFeature({
blocks: [
{
slug: 'myBlock',
fields: [
{
name: 'someText',
type: 'text',
},
{
name: 'someTextRequired',
type: 'text',
required: true,
},
{
name: 'radios',
type: 'radio',
options: [
{
label: 'Option 1',
value: 'option1',
},
{
label: 'Option 2',
value: 'option2',
},
{
label: 'Option 3',
value: 'option3',
},
],
validate: (value) => {
return value !== 'option2' ? true : 'Cannot be option2'
): Promise<(() => Promise<SanitizedConfig>) | SanitizedConfig> {
return await buildConfig(() => {
const testConfig = testConfigFn ? testConfigFn() : undefined
const config: Config = {
db: databaseAdapter,
editor: lexicalEditor({
features: [
ParagraphFeature(),
RelationshipFeature(),
LinkFeature({
fields: ({ defaultFields }) => [
...defaultFields,
{
name: 'description',
type: 'text',
},
],
}),
ChecklistFeature(),
UnorderedListFeature(),
OrderedListFeature(),
AlignFeature(),
BlockquoteFeature(),
BoldFeature(),
ItalicFeature(),
UploadFeature({
collections: {
media: {
fields: [
{
name: 'alt',
type: 'text',
},
},
],
],
},
},
],
}),
],
}),
email: testEmailAdapter,
endpoints: [localAPIEndpoint, reInitEndpoint],
secret: 'TEST_SECRET',
sharp,
telemetry: false,
...testConfig,
i18n: {
supportedLanguages: {
de,
en,
es,
}),
UnderlineFeature(),
StrikethroughFeature(),
SubscriptFeature(),
SuperscriptFeature(),
InlineCodeFeature(),
InlineToolbarFeature(),
TreeViewFeature(),
HeadingFeature(),
IndentFeature(),
BlocksFeature({
blocks: [
{
slug: 'myBlock',
fields: [
{
name: 'someText',
type: 'text',
},
{
name: 'someTextRequired',
type: 'text',
required: true,
},
{
name: 'radios',
type: 'radio',
options: [
{
label: 'Option 1',
value: 'option1',
},
{
label: 'Option 2',
value: 'option2',
},
{
label: 'Option 3',
value: 'option3',
},
],
validate: (value) => {
return value !== 'option2' ? true : 'Cannot be option2'
},
},
],
},
],
}),
],
}),
email: testEmailAdapter,
endpoints: [localAPIEndpoint, reInitEndpoint],
secret: 'TEST_SECRET',
sharp,
telemetry: false,
...testConfig,
i18n: {
supportedLanguages: {
de,
en,
es,
},
...(testConfig?.i18n || {}),
},
...(testConfig?.i18n || {}),
},
typescript: {
declare: {
ignoreTSError: true,
typescript: {
declare: {
ignoreTSError: true,
},
...testConfig?.typescript,
},
...testConfig?.typescript,
},
}
}
if (!config.admin) {
config.admin = {}
}
if (config.admin.autoLogin === undefined) {
config.admin.autoLogin =
process.env.PAYLOAD_PUBLIC_DISABLE_AUTO_LOGIN === 'true' || options?.disableAutoLogin
? false
: {
email: 'dev@payloadcms.com',
}
}
if (process.env.PAYLOAD_DISABLE_ADMIN === 'true') {
if (typeof config.admin !== 'object') {
if (!config.admin) {
config.admin = {}
}
config.admin.disable = true
}
return await buildConfig(config)
if (config.admin.autoLogin === undefined) {
config.admin.autoLogin =
process.env.PAYLOAD_PUBLIC_DISABLE_AUTO_LOGIN === 'true' || options?.disableAutoLogin
? false
: {
email: 'dev@payloadcms.com',
}
}
if (process.env.PAYLOAD_DISABLE_ADMIN === 'true') {
if (typeof config.admin !== 'object') {
config.admin = {}
}
config.admin.disable = true
}
return config
})
}

View File

@@ -39,7 +39,7 @@ export const pointSlug = 'point'
export const errorOnHookSlug = 'error-on-hooks'
export default buildConfigWithDefaults({
export default buildConfigWithDefaults(() => ({
admin: {
importMap: {
baseDir: path.resolve(dirname),
@@ -550,4 +550,4 @@ export default buildConfigWithDefaults({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})
}))

View File

@@ -43,7 +43,7 @@ export const errorOnHookSlug = 'error-on-hooks'
export const endpointsSlug = 'endpoints'
export default buildConfigWithDefaults({
export default buildConfigWithDefaults(() => ({
admin: {
importMap: {
baseDir: path.resolve(dirname),
@@ -409,4 +409,4 @@ export default buildConfigWithDefaults({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})
}))

View File

@@ -5,7 +5,7 @@ const dirname = path.dirname(filename)
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
import { devUser } from '../credentials.js'
export default buildConfigWithDefaults({
export default buildConfigWithDefaults(() => ({
admin: {
importMap: {
baseDir: path.resolve(dirname),
@@ -122,4 +122,4 @@ export default buildConfigWithDefaults({
origins: '*',
headers: ['x-custom-header'],
},
})
}))

View File

@@ -22,7 +22,7 @@ const resolveTransactionId = async (_obj, _args, context) => {
}
}
export default buildConfigWithDefaults({
export default buildConfigWithDefaults(() => ({
admin: {
importMap: {
baseDir: path.resolve(dirname),
@@ -73,4 +73,4 @@ export default buildConfigWithDefaults({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})
}))

View File

@@ -15,7 +15,7 @@ const defaultValueField: TextField = {
defaultValue: 'default value from database',
}
export default buildConfigWithDefaults({
export default buildConfigWithDefaults(() => ({
admin: {
importMap: {
baseDir: path.resolve(dirname),
@@ -469,7 +469,7 @@ export default buildConfigWithDefaults({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})
}))
export const postDoc = {
title: 'test post',

View File

@@ -9,7 +9,7 @@ import type { Post } from './payload-types.js'
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
import { devUser } from '../credentials.js'
export default buildConfigWithDefaults({
export default buildConfigWithDefaults(() => ({
admin: {
importMap: {
baseDir: path.resolve(dirname),
@@ -146,7 +146,7 @@ export default buildConfigWithDefaults({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})
}))
export const postDoc: Pick<Post, 'title'> = {
title: 'test post',

View File

@@ -7,7 +7,7 @@ import { nodemailerAdapter } from '@payloadcms/email-nodemailer'
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
import { devUser } from '../credentials.js'
export default buildConfigWithDefaults({
export default buildConfigWithDefaults(() => ({
admin: {
importMap: {
baseDir: path.resolve(dirname),
@@ -34,4 +34,4 @@ export default buildConfigWithDefaults({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})
}))

View File

@@ -7,7 +7,7 @@ import { resendAdapter } from '@payloadcms/email-resend'
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
import { devUser } from '../credentials.js'
export default buildConfigWithDefaults({
export default buildConfigWithDefaults(() => ({
admin: {
importMap: {
baseDir: path.resolve(dirname),
@@ -42,4 +42,4 @@ export default buildConfigWithDefaults({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})
}))

View File

@@ -12,7 +12,7 @@ import { MenuGlobal } from './globals/Menu/index.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export default buildConfigWithDefaults({
export default buildConfigWithDefaults(() => ({
admin: {
importMap: {
baseDir: path.resolve(dirname),
@@ -55,4 +55,4 @@ export default buildConfigWithDefaults({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})
}))

View File

@@ -14,7 +14,7 @@ import {
noEndpointsGlobalSlug,
} from './shared.js'
export default buildConfigWithDefaults({
export default buildConfigWithDefaults(() => ({
admin: {
importMap: {
baseDir: path.resolve(dirname),
@@ -80,4 +80,4 @@ export default buildConfigWithDefaults({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})
}))

View File

@@ -13,7 +13,7 @@ import { ValidateDraftsOn } from './collections/ValidateDraftsOn/index.js'
import { ValidateDraftsOnAndAutosave } from './collections/ValidateDraftsOnAutosave/index.js'
import { GlobalValidateDraftsOn } from './globals/ValidateDraftsOn/index.js'
export default buildConfigWithDefaults({
export default buildConfigWithDefaults(() => ({
admin: {
importMap: {
baseDir: path.resolve(dirname),
@@ -41,4 +41,4 @@ export default buildConfigWithDefaults({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})
}))

View File

@@ -5,7 +5,7 @@ const dirname = path.dirname(filename)
import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
import { devUser } from '../credentials.js'
export default buildConfigWithDefaults({
export default buildConfigWithDefaults(() => ({
admin: {
importMap: {
baseDir: path.resolve(dirname),
@@ -105,4 +105,4 @@ export default buildConfigWithDefaults({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})
}))

View File

@@ -51,7 +51,7 @@ const baseRelationshipFields: CollectionConfig['fields'] = [
},
]
export default buildConfigWithDefaults({
export default buildConfigWithDefaults(() => ({
admin: {
importMap: {
baseDir: path.resolve(dirname),
@@ -543,4 +543,4 @@ export default buildConfigWithDefaults({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})
}))

View File

@@ -99,7 +99,7 @@ export const collectionSlugs: CollectionConfig[] = [
UIFields,
]
export default buildConfigWithDefaults({
export default buildConfigWithDefaults(() => ({
collections: collectionSlugs,
globals: [TabsWithRichText],
custom: {
@@ -136,4 +136,4 @@ export default buildConfigWithDefaults({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})
}))

View File

@@ -2,10 +2,8 @@ import path from 'path'
const [testConfigDir] = process.argv.slice(2)
import type { SanitizedConfig } from 'payload'
import fs from 'fs'
import { generateImportMap } from 'payload'
import { type ConfigImport, generateImportMap, getConfig } from 'payload'
import { fileURLToPath } from 'url'
const filename = fileURLToPath(import.meta.url)
@@ -20,7 +18,8 @@ async function run() {
const pathWithConfig = path.resolve(testDir, 'config.ts')
console.log('Generating ad-hoc import map for config:', pathWithConfig)
const config: SanitizedConfig = await (await import(pathWithConfig)).default
const configImport: ConfigImport = (await import(pathWithConfig)).default
const config = await getConfig(configImport)
let rootDir = ''
if (testConfigDir === 'live-preview' || testConfigDir === 'admin-root') {

View File

@@ -22,7 +22,7 @@ const access = {
update: () => true,
}
export default buildConfigWithDefaults({
export default buildConfigWithDefaults(() => ({
admin: {
importMap: {
baseDir: path.resolve(dirname),
@@ -138,4 +138,4 @@ export default buildConfigWithDefaults({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})
}))

View File

@@ -5,7 +5,7 @@ import { buildConfigWithDefaults } from '../buildConfigWithDefaults.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export default buildConfigWithDefaults({
export default buildConfigWithDefaults(() => ({
admin: {
importMap: {
baseDir: path.resolve(dirname),
@@ -159,4 +159,4 @@ export default buildConfigWithDefaults({
],
},
],
})
}))

View File

@@ -64,11 +64,13 @@ export async function ensureCompilationIsDone({
page,
serverURL,
noAutoLogin,
readyURL,
}: {
customAdminRoutes?: Config['admin']['routes']
customRoutes?: Config['routes']
noAutoLogin?: boolean
page: Page
readyURL?: string
serverURL: string
}): Promise<void> {
const {
@@ -82,11 +84,16 @@ export async function ensureCompilationIsDone({
while (attempt <= maxAttempts) {
try {
console.log(`Checking if compilation is done (attempt ${attempt}/${maxAttempts})...`)
console.log(
`Checking if compilation is done (attempt ${attempt}/${maxAttempts})...`,
readyURL ??
(noAutoLogin ? `${adminURL + (adminURL.endsWith('/') ? '' : '/')}login` : adminURL),
)
await page.goto(adminURL)
await page.waitForURL(
noAutoLogin ? `${adminURL + (adminURL.endsWith('/') ? '' : '/')}login` : adminURL,
readyURL ??
(noAutoLogin ? `${adminURL + (adminURL.endsWith('/') ? '' : '/')}login` : adminURL),
)
console.log('Successfully compiled')

View File

@@ -1,7 +1,7 @@
import type { Payload, SanitizedConfig } from 'payload'
import path from 'path'
import { getPayload } from 'payload'
import { getConfig, getPayload } from 'payload'
import { runInit } from '../runInit.js'
import { NextRESTClient } from './NextRESTClient.js'
@@ -17,15 +17,15 @@ export async function initPayloadInt(
const testSuiteName = testSuiteNameOverride ?? path.basename(dirname)
await runInit(testSuiteName, false, true)
console.log('importing config', path.resolve(dirname, 'config.ts'))
const { default: config } = await import(path.resolve(dirname, 'config.ts'))
const { default: configImport } = await import(path.resolve(dirname, 'config.ts'))
if (!initializePayload) {
return { config: await config }
return { config: await getConfig(configImport) }
}
console.log('starting payload')
const payload = await getPayload({ config })
const payload = await getPayload({ config: await getConfig(configImport) })
console.log('initializing rest client')
const restClient = new NextRESTClient(payload.config)
console.log('initPayloadInt done')

View File

@@ -17,7 +17,7 @@ import Relations from './collections/Relations/index.js'
import TransformHooks from './collections/Transform/index.js'
import Users, { seedHooksUsers } from './collections/Users/index.js'
import { DataHooksGlobal } from './globals/Data/index.js'
export const HooksConfig: Promise<SanitizedConfig> = buildConfigWithDefaults({
export const HooksConfig: Promise<SanitizedConfig> = buildConfigWithDefaults(() => ({
admin: {
importMap: {
baseDir: path.resolve(dirname),
@@ -68,6 +68,6 @@ export const HooksConfig: Promise<SanitizedConfig> = buildConfigWithDefaults({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})
}))
export default HooksConfig

View File

@@ -30,7 +30,7 @@ const customTranslationsObject = {
export type CustomTranslationsObject = typeof customTranslationsObject.en
export type CustomTranslationsKeys = NestedKeysStripped<CustomTranslationsObject>
export default buildConfigWithDefaults({
export default buildConfigWithDefaults(() => ({
admin: {
importMap: {
baseDir: path.resolve(dirname),
@@ -94,4 +94,4 @@ export default buildConfigWithDefaults({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})
}))

View File

@@ -1,7 +1,7 @@
import fs from 'fs'
import path from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { generateImportMap, type SanitizedConfig } from 'payload'
import { type ConfigImport, generateImportMap, getConfig } from 'payload'
import type { allDatabaseAdapters } from './generateDatabaseAdapter.js'
@@ -45,13 +45,13 @@ export async function initDevAndTest(
console.log('Generating import map for config:', testDir)
const configUrl = pathToFileURL(path.resolve(testDir, 'config.ts')).href
const config: SanitizedConfig = await (await import(configUrl)).default
const configImport: ConfigImport = (await import(configUrl)).default
const config = await getConfig(configImport)
process.env.ROOT_DIR = getNextRootDir(testSuiteArg).rootDir
await generateImportMap(config, { log: true, force: true })
console.log('Done')
}
if (runImmediately === 'true') {

View File

@@ -20,7 +20,7 @@ import {
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export default buildConfigWithDefaults({
export default buildConfigWithDefaults(() => ({
collections: [
Posts,
Categories,
@@ -126,4 +126,4 @@ export default buildConfigWithDefaults({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})
}))

View File

@@ -11,7 +11,7 @@ import { docsBasePath } from './collections/Posts/shared.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export default buildConfigWithDefaults({
export default buildConfigWithDefaults(() => ({
// ...extend config here
collections: [
PostsCollection,
@@ -78,4 +78,4 @@ export default buildConfigWithDefaults({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})
}))

View File

@@ -24,7 +24,7 @@ import {
} from './shared.js'
import { formatLivePreviewURL } from './utilities/formatLivePreviewURL.js'
export default buildConfigWithDefaults({
export default buildConfigWithDefaults(() => ({
admin: {
importMap: {
baseDir: path.resolve(dirname),
@@ -46,4 +46,4 @@ export default buildConfigWithDefaults({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})
}))

View File

@@ -9,7 +9,7 @@ import { Posts } from './collections/posts.js'
import { Users } from './collections/users.js'
import deepMerge from './deepMerge.js'
export default buildConfigWithDefaults({
export default buildConfigWithDefaults(() => ({
admin: {
importMap: {
baseDir: path.resolve(dirname),
@@ -50,4 +50,4 @@ export default buildConfigWithDefaults({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})
}))

View File

@@ -47,7 +47,7 @@ const openAccess = {
update: () => true,
}
export default buildConfigWithDefaults({
export default buildConfigWithDefaults(() => ({
admin: {
importMap: {
baseDir: path.resolve(dirname),
@@ -561,4 +561,4 @@ export default buildConfigWithDefaults({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})
}))

View File

@@ -13,7 +13,7 @@ import { MenuGlobal } from './globals/Menu/index.js'
const filename = fileURLToPath(import.meta.url)
const dirname = path.dirname(filename)
export default buildConfigWithDefaults({
export default buildConfigWithDefaults(() => ({
admin: {
importMap: {
baseDir: path.resolve(dirname),
@@ -61,4 +61,4 @@ export default buildConfigWithDefaults({
typescript: {
outputFile: path.resolve(dirname, 'payload-types.ts'),
},
})
}))

Some files were not shown because too many files have changed in this diff Show More