Files
payloadcms/docs/migration-guide/overview.mdx
2024-11-15 11:30:40 -05:00

789 lines
24 KiB
Plaintext
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 🚧 **DRAFT:** 3.0 Migration Guide / Breaking Changes
> [!IMPORTANT]
> This document will continue to be updated and cleaned up until the 3.0 release.
## What has changed?
The core logic and principles of Payload remain the same from 2.0 to 3.0, with the majority of changes affecting specifically the HTTP layer and the Admin Panel, which is now built upon Next.js. With this change, your entire application can be served within a single repo. Payload endpoints are now opened directly within your own Next.js application, right alongside your frontend. All Payload APIs remain exactly the same (with a few new features), and the Payload Config is generally the same, with the breaking changes detailed below.
Payload is still headless, you will still be able to leverage it completely headlessly just as you do now with Sveltekit, etc. In fact, Payload itself is now _truly_ portable because it is fully Node.js compatible and completely separate from the HTTP layer. The entire Payload suite of packages has been modularized to make this possible.
## To migrate from Payload 2.0 to 3.0:
1. Delete the `admin.bundler` property from your Payload Config. Payload no longer bundles the Admin Panel. Instead, we rely directly on Next.js for bundling.
```diff
// payload.config.ts
- import { webpackBundler } from '@payloadcms/bundler-webpack'
buildConfig({
// ...
admin: {
// ...
- bundler: webpackBundler(),
}
})
```
This also means that the `@payloadcms/bundler-webpack` and `@payloadcms/bundler-vite` packages have been deprecated. You can completely uninstall those from your project by removing them from your `package.json` file and re-running your package managers installation process, i.e. `pnpm i`.
1. Add the `secret` property to your Payload Config. This used to be set in the `payload.init()` function of your `server.ts` file. Instead, move it to `payload.config.ts`:
```diff
// payload.config.ts
buildConfig({
// ...
+ secret: process.env.PAYLOAD_SECRET
})
```
1. The `admin.css` and `admin.scss` properties in the Payload Config have been removed.
```diff
// payload.config.ts
buildConfig({
// ...
admin: {
// ...
- css: '',
- scss: ''
}
})
```
To migrate, choose one of the following options:
1. For most use cases, you can simply customize the file located at `(payload)/custom.scss`. You can import or add your own styles here, such as for Tailwind.
1. For plugins author, you can use a Custom Provider at `admin.components.providers` to import your stylesheet:
```tsx
// payload.config.js
//...
admin: {
components: {
providers: [
MyProvider: './providers/MyProvider.tsx'
]
}
},
//...
// providers/MyProvider.tsx
'use client'
import React from 'react'
import './globals.css'
export const MyProvider: React.FC<{children?: any}= ({ children }) ={
return (
<React.fragment>
{children}
</React.fragment>
)
}
```
1. The `admin.indexHTML` property has been removed. Delete this from your Payload Config.
```diff
// payload.config.ts
buildConfig({
// ...
admin: {
// ...
- indexHTML: ''
}
})
```
1. The `collection.admin.hooks` property has been removed. Instead, use the new `beforeDuplicate` field-level hook which take the usual field hook arguments.
```diff
// collections/Posts.ts
import type { CollectionConfig } from 'payload'
export const PostsCollection: CollectionConfig = {
slug: 'posts',
admin: {
hooks: {
- beforeDuplicate: ({ data }) => {
- return {
- ...data,
- title: `${data.title}-duplicate`
- }
- }
}
},
fields: [
{
name: 'title',
type: 'text',
hooks: {
+ beforeDuplicate: [
+ ({ data }) => `${data.title}-duplicate`
+ ],
},
},
],
}
```
1. All Payload React components have been moved from the `payload` package to `@payloadcms/ui`. If you were previously importing components into your app from the `payload` package, for example to create Custom Components, you will need to change your import paths:
```diff
- import { TextField, useField, etc. } from 'payload'
+ import { TextField, useField, etc. } from '@payloadcms/ui'
```
*Note: for brevity, not _all_ modules are listed here*
1. The `BlockField` and related types have been renamed to `BlocksField` for semantic accuracy.
```diff
- import type { BlockField, BlockFieldProps } from 'payload'
+ import type { BlocksField, BlocksFieldProps } from 'payload'
```
1. All Custom Components are now defined as _file paths_ instead of direct imports. If you are using Custom Components in your Payload Config, remove the imported module and point to the file's path instead:
```diff
import { buildConfig } from 'payload'
- import { MyComponent } from './src/components/Logout'
const config = buildConfig({
// ...
admin: {
components: {
logout: {
- Button: MyComponent,
+ Button: '/src/components/Logout#MyComponent'
}
}
},
})
```
For more details, see the [Documentation](https://payloadcms.com/docs/beta/admin/components#component-paths).
1. All Custom Components are now server-rendered by default, and therefore, cannot use state or hooks directly. If youre using Custom Components in your app that requires state or hooks, add the `'use client'` directive at the top of the file.
```diff
// components/MyClientComponent.tsx
+ 'use client'
import React, { useState } from 'react'
export const MyClientComponent = () => {
const [state, setState] = useState()
return (
<div>
{state}
</div>
)
}
```
For more details, see the [Documentation](https://payloadcms.com/docs/beta/admin/components#client-components).
1. The `custom` property in the Payload Config, i.e. Collections, Globals, and Fields is now **server only** and will **not** appear in the client-side bundle. To add custom properties to the client bundle, use the new `admin.custom` property, which will be available on _both_ the server and the client.
```diff
// payload.config.ts
import { buildConfig } from 'payload'
export default buildConfig({
custom: {
someProperty: 'My Server Prop' // Now server only!
},
admin: {
+ custom: {
+ name: 'My Client Prop' // Available in server AND client
+ }
},
})
```
1. The `useTitle` hook has been consolidated into the `useDocumentInfo` hook. Instead, you can get title directly from document info context:
```diff
'use client'
- import { useTitle } from 'payload'
+ import { useDocumentInfo } from '@payloadcms/ui'
export const MyComponent = () => {
- const title = useTitle()
+ const { title } = useDocumentInfo()
// ...
}
```
1. The `Fields` type was renamed to `FormState` for improved semantics. If you were previously importing this type in your own application, simply change the import name:
```diff
- import type { Fields } from 'payload'
+ import type { FormState } from 'payload'
```
1. The `useDocumentInfo` hook no longer returns `collection` or `global`. Instead, various properties of the config are passed, like `collectionSlug` and `globalSlug`. You can use these to access a client-side config, if needed, through the `useConfig` hook (see next bullet).
```diff
'use client'
import { useDocumentInfo } from '@payloadcms/ui'
export const MyComponent = () => {
const {
- collection,
- global,
+ collectionSlug,
+ globalSlug
} = useDocumentInfo()
// ...
}
```
1. The `useConfig` hook now returns a `ClientConfig` and not a `SanitizedConfig`. This is because the config itself is not serializable and so it is not able to be thread through to the client. This means that all non-serializable props have been omitted from the Client Config, such as `db`, `bundler`, etc.
```diff
'use client'
- import { useConfig } from 'payload'
+ import { useConfig } from '@payloadcms/ui'
export const MyComponent = () => {
- const config = useConfig() // used to be a 'SanitizedConfig'
+ const { config } = useConfig() // now is a 'ClientConfig'
// ...
}
```
For more details, see the [Documentation](https://payloadcms.com/docs/beta/admin/components#accessing-the-payload-config).
1. The args of the `admin.livePreview.url` function have changed. It no longer receives `documentInfo` as an arg, and instead, now has `collectionConfig` and `globalConfig`.
```diff
// payload.config.ts
import { buildConfig } from 'payload'
export default buildConfig({
// ...
admin: {
// ...
livePreview: ({
- documentInfo,
+ collectionConfig,
+ globalConfig
}) => ''
}
})
```
1. The `href` and `isActive` functions on View Tabs no longer includes the `match` or `location` arguments. This is is a property specific to React Router, not Next.js. If you need to do URL matching similar to this, use a custom tab that fires of some hooks, i.e. `usePathname()` and run it through your own utility functions:
```diff
// collections/Posts.ts
import type { CollectionConfig } from 'payload'
export const PostsCollection: CollectionConfig = {
slug: 'posts',
admin: {
components: {
views: {
- Edit: {
- Tab: {
- isActive: ({ href, location, match }) => true,
- href: ({ href, location, match }) => ''
- },
- },
+ edit: {
+ tab: {
+ isActive: ({ href }) => true,
+ href: ({ href }) => ''
+ Component: './path/to/CustomComponent.tsx', // Or use a Custom Component
+ }
+ },
},
},
},
}
```
1. The `admin.components.views[key].Tab.pillLabel` has been replaced with `admin.components.views[key].tab.Pill`:
```diff
// collections/Posts.ts
import type { CollectionConfig } from 'payload'
export const PostsCollection: CollectionConfig = {
slug: 'posts',
admin: {
components: {
- views: {
- edit: {
- Tab: {
- pillLabel: 'Hello, world!',
- },
- },
+ Edit: {
+ tab: {
+ Pill: './path/to/CustomPill.tsx',
+ }
+ },
},
},
},
}
```
1. Unique fields will now automatically be appended with “- Copy” through the new `admin.beforeDuplicate` field hooks (detailed aboved).
1. The `useCollapsible` hook has had slight changes to its property names. `collapsed` is now `isCollapsed` and `withinCollapsible` is now `isWithinCollapsible`.
```diff
'use client'
import { useCollapsible } from '@payloadcms/ui'
export const MyComponent = () => {
- const { collapsed, withinCollapsible } = useCollapsible()
+ const { isCollapsed, isWithinCollapsible } = useCollapsible()
}
```
1. The `admin.favicon` property is now `admin.icons` and the types have changed:
```diff
// payload.config.ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
admin: {
- favicon: 'path-to-favicon.svg',
+ icons: [{
+ path: 'path-to-favicon.svg',
+ sizes: '32x32'
+ }]
}
})
```
For more details, see the [Documentation](https://payloadcms.com/docs/beta/admin/metadata#icons).
1. The `admin.meta.ogImage` property has been replaced by `admin.meta.openGraph.images`:
```diff
// payload.config.ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
admin: {
meta: {
- ogImage: '',
+ openGraph: {
+ images: []
+ }
}
}
})
```
For more details, see the [Documentation](https://payloadcms.com/docs/beta/admin/metadata#open-graph).
1. The `admin.logoutRoute` and `admin.inactivityRoute` properties have been consolidated into a single `admin.routes` property. To migrate, simply move those two keys as follows:
```diff
// payload.config.ts
import { buildConfig } from 'payload'
const config = buildConfig({
// ...
admin: {
- logoutRoute: '/custom-logout',
+ inactivityRoute: '/custom-inactivity'
+ routes: {
+ logout: '/custom-logout',
+ inactivity: '/custom-inactivity'
+ }
}
})
```
1. Environment variables prefixed with `PAYLOAD_PUBLIC` will no longer be available on the client. In order to access them on the client, those will now have to be prefixed with `NEXT_PUBLIC` instead.
```diff
'use client'
- const var = process.env.PAYLOAD_PUBLIC_MY_ENV_VAR
+ const var = process.env.NEXT_PUBLIC_MY_ENV_VAR
```
For more details, see the [Documentation](https://payloadcms.com/docs/beta/configuration/environment-vars).
1. The `useTranslation` hook no longer takes any options, any translations using shorthand accessors will need to use the entire `group:key`
```diff
'use client'
- import { useTranslation } from 'payload'
+ import { useTranslation } from '@payloadcms/ui'
export const MyComponent = () => {
- const { i18n, t } = useTranslation('general')
+ const { i18n, t } = useTranslation()
- return <p>{t('cancel')}</p>
+ return <p>{t('general:cancel')}</p>
}
```
1. `react-i18n` was removed, the `Trans` component from `react-i18n` has been replaced with a Payload-provided solution:
```diff
'use client'
- import { Trans } from "react-i18n"
+ import { Translation } from "@payloadcms/ui"
// Example string to translate:
// "loggedInChangePassword": "To change your password, go to your <0>account</0> and edit your password there."
export const MyComponent = () => {
return (
- <Trans i18nKey="loggedInChangePassword" t={t}>
- <Link to={`${admin}/account`}>account</Link>
- </Trans>
+ <Translation
+ t={t}
+ i18nKey="authentication:loggedInChangePassword"
+ elements={{
+ '0': ({ children }) => <Link href={`${admin}/account`} children={children} />,
+ }}
+ />
)
}
```
1. All other endpoint handlers have changed. The args no longer include `res`, and `next`, and the return type now expects a valid HTTP [`Response`](https://developer.mozilla.org/en-US/docs/Web/API/Response) instead of `res.json`, `res.send`, etc.:
```diff
// collections/Posts.ts
import type { CollectionConfig } from 'payload'
export const PostsCollection: CollectionConfig = {
slug: 'posts',
endpoints: [
- {
- path: '/whoami/:parameter',
- method: 'post',
- handler: (req, res) => {
- res.json({
- parameter: req.params.parameter,
- name: req.body.name,
- age: req.body.age,
- })
- }
- },
+ {
+ path: '/whoami/:parameter',
+ method: 'post',
+ handler: (req) => {
+ return Response.json({
+ parameter: req.routeParams.parameter,
+ // ^^ `params` is now `routeParams`
+ name: req.data.name,
+ age: req.data.age,
+ })
+ }
+ }
]
}
```
1. Endpoint handlers no longer resolves `data`, `locale`, or `fallbackLocale` for you on the request. Instead, you must resolve them yourself or use the Payload-provided utilities:
```diff
// collections/Posts.ts
import type { CollectionConfig } from 'payload'
+ import { addDataAndFileToRequest } from '@payloadcms/next/utilities'
+ import { addLocalesToRequest } from '@payloadcms/next/utilities'
export const PostsCollection: CollectionConfig = {
slug: 'posts',
endpoints: [
- {
- path: '/whoami/:parameter',
- method: 'post',
- handler: async (req) => {
- return Response.json({
- name: req.data.name, // data will be undefined
- })
- }
- },
+ {
+ path: '/whoami/:parameter',
+ method: 'post',
+ handler: async (req) => {
+ // mutates req, must be awaited
+ await addDataAndFileToRequest(req)
+ await addLocalesToRequest(req)
+
+ return Response.json({
+ name: req.data.name, // data is now available
+ fallbackLocale: req.fallbackLocale,
+ locale: req.locale,
+ })
+ }
+ }
]
}
```
1. The `req` object used to extend the [Express Request](https://expressjs.com/), but now extends the [Web Request](https://developer.mozilla.org/en-US/docs/Web/API/Request). You may need to update your code accordingly to reflect this change. For example:
```diff
- req.headers['content-type']
+ req.headers.get('content-type')
```
1. `staticDir` must now be an absolute path. Before it would attempt to use the location of the Payload Config and merge the relative path set for staticDir.
```diff
// collections/Media.ts
import type { CollectionConfig } from 'payload'
import path from 'path'
+ import { fileURLToPath } from 'url'
+ const filename = fileURLToPath(import.meta.url)
+ const dirname = path.dirname(filename)
export const MediaCollection: CollectionConfig = {
slug: 'media',
upload: {
- staticDir: path.resolve(__dirname, './uploads'),
+ staticDir: path.resolve(dirname, '../uploads'),
},
}
```
1. `staticURL` has been removed. If you were using this format URLs when using an external provider, you can leverage the `generateFileURL` functions in order to do the same.
```diff
// collections/Media.ts
import type { CollectionConfig } from 'payload'
export const MediaCollection: CollectionConfig = {
slug: 'media',
upload: {
- staticURL: '',
},
}
```
## Email Adapters
Email functionality has been abstracted out into email adapters.
- All existing nodemailer functionality was abstracted into the `@payloadcms/email-nodemailer` package
- No longer configured with ethereal.email by default.
- Ability to pass email into the `init` function has been removed.
- Warning will be given on startup if email not configured. Any `sendEmail` call will simply log the To address and subject.
- A Resend adapter is now also available via the `@payloadcms/email-resend` package.
### If you used the default email configuration in 2.0 (nodemailer):
```tsx
// ❌ Before
// via payload.init
payload.init({
email: {
transport: someNodemailerTransport
fromName: 'hello',
fromAddress: 'hello@example.com',
},
})
// or via email in payload.config.ts
export default buildConfig({
email: {
transport: someNodemailerTransport
fromName: 'hello',
fromAddress: 'hello@example.com',
},
})
// ✅ After
// Using new nodemailer adapter package
import { nodemailerAdapter } from '@payloadcms/email-nodemailer'
export default buildConfig({
email: nodemailerAdapter() // This will be the old ethereal.email functionality
})
// or pass in transport
export default buildConfig({
email: nodemailerAdapter({
defaultFromAddress: 'info@payloadcms.com',
defaultFromName: 'Payload',
transport: await nodemailer.createTransport({
host: process.env.SMTP_HOST,
port: 587,
auth: {
user: process.env.SMTP_USER,
pass: process.env.SMTP_PASS,
},
})
})
})
```
### Removal of rate-limiting
- Now only available if using custom server and using express or similar
# Plugins
1. *All* plugins have been standardized to use _named exports_ (as opposed to default exports). Most also have a suffix of `Plugin` to make it clear what is being imported.
```diff
- import seo from '@payloadcms/plugin-seo'
+ import { seoPlugin } from '@payloadcms/plugin-seo'
- import stripePlugin from '@payloadcms/plugin-stripe'
+ import { stripePlugin } from '@payloadcms/plugin-stripe'
// and so on for every plugin
```
## `@payloadcms/plugin-cloud-storage`
- The adapters that are exported from `@payloadcms/plugin-cloud-storage` (ie. `@payloadcms/plugin-cloud-storage/s3`) package have been removed.
- New *standalone* packages have been created for each of the existing adapters. Please see the documentation for the one that you use.
- `@payloadcms/plugin-cloud-storage` is still fully supported but should only to be used if you are providing a custom adapter that does not have a dedicated package.
- If you have created a custom adapter, the type must now provide a `name` property.
| Service | Package |
| -------------------- | ---------------------------------------------------------------------------- |
| Vercel Blob | https://github.com/payloadcms/payload/tree/beta/packages/storage-vercel-blob |
| AWS S3 | https://github.com/payloadcms/payload/tree/beta/packages/storage-s3 |
| Azure | https://github.com/payloadcms/payload/tree/beta/packages/storage-azure |
| Google Cloud Storage | https://github.com/payloadcms/payload/tree/beta/packages/storage-gcs |
```tsx
// ❌ Before (required peer dependencies depending on adapter)
import { cloudStorage } from '@payloadcms/plugin-cloud-storage'
import { s3Adapter } from '@payloadcms/plugin-cloud-storage/s3'
plugins: [
cloudStorage({
collections: {
[mediaSlug]: {
adapter: s3Adapter({
bucket: process.env.S3_BUCKET,
config: {
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
},
region: process.env.S3_REGION,
},
}),
},
},
}),
],
// ✅ After
import { s3Storage } from '@payloadcms/storage-s3'
plugins: [
s3Storage({
collections: {
[mediaSlug]: true,
},
bucket: process.env.S3_BUCKET,
config: {
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
},
region: process.env.S3_REGION,
},
}),
],
```
## `@payloadcms/plugin-form-builder`
1. Field overrides for form and form submission collections now accept a function with a `defaultFields` inside the args instead of an array of config
```diff
// payload.config.ts
import { buildConfig } from 'payload'
import { formBuilderPlugin } from '@payloadcms/plugin-form-builder'
const config = buildConfig({
// ...
plugins: formBuilderPlugin({
- fields: [
- {
- name: 'custom',
- type: 'text',
- }
- ],
+ fields: ({ defaultFields }) => {
+ return [
+ ...defaultFields,
+ {
+ name: 'custom',
+ type: 'text',
+ },
+ ]
+ }
})
})
```
## `@payloadcms/plugin-redirects`
1. Field overrides for the redirects collection now accepts a function with a `defaultFields` inside the args instead of an array of config
```diff
// payload.config.ts
import { buildConfig } from 'payload'
import { redirectsPlugin } from '@payloadcms/plugin-redirects'
const config = buildConfig({
// ...
plugins: redirectsPlugin({
- fields: [
- {
- name: 'custom',
- type: 'text',
- }
- ],
+ fields: ({ defaultFields }) => {
+ return [
+ ...defaultFields,
+ {
+ name: 'custom',
+ type: 'text',
+ },
+ ]
+ }
})
})
```
## `@payloadcms/richtext-lexical`
// TODO: Add migration guide for `@payloadcms/richtext-lexical`