Skip to content

Commit 1b29f7c

Browse files
authored
refactor(next)!: move templates and elements to @payloadcms/ui (#16765)
Requires #16763 and #16753. Moves all elements and templates defined in the `@payloadcms/next` package to `@payloadcms/ui`. This is in effort to convert the `@payloadcms/next` package into a framework adapter. **Relocated exports** — code physically moved from `@payloadcms/next` to `@payloadcms/ui`: | Component | Old source | New source | | -------------------------- | ----------------------------- | ---------------------- | | `DefaultTemplate` | `@payloadcms/next/templates` | `@payloadcms/ui/rsc` | | `DefaultTemplateProps` | `@payloadcms/next/templates` | `@payloadcms/ui/rsc` | | `MinimalTemplate` | `@payloadcms/next/templates` | `@payloadcms/ui/rsc` | | `MinimalTemplateProps` | `@payloadcms/next/templates` | `@payloadcms/ui/rsc` | | `DocumentHeader` | `@payloadcms/next/rsc` | `@payloadcms/ui/rsc` | | `DefaultNav` | `@payloadcms/next/rsc` | `@payloadcms/ui/rsc` | | `Logo` | `@payloadcms/next/rsc` | `@payloadcms/ui/rsc` | | `HierarchyTypeFieldServer` | `@payloadcms/next/rsc` | `@payloadcms/ui/rsc` | | `DefaultNavClient` | `@payloadcms/next/client` | `@payloadcms/ui` | | `HierarchyTypeField` | `@payloadcms/next/client` | `@payloadcms/ui` | | `NavSidebarToggle` | `@payloadcms/next/client` | `@payloadcms/ui` | | `NavWrapper` | `@payloadcms/next/client` | `@payloadcms/ui` | This PR also removes the now unneeded export aliases. These were in place for backwards compatibility during v3. Now, we enforce that all imports point to its canonical source, not the aliased export. | Component | Old source | New source | | -------------------------- | ------------------------- | --------------------- | | `CollectionCards` | `@payloadcms/next/rsc` | `@payloadcms/ui/rsc` | | `SlugField` | `@payloadcms/next/client` | `@payloadcms/ui` | | `QueryPresetsAccessCell` | `@payloadcms/next/client` | `@payloadcms/ui` | | `QueryPresetsColumnField` | `@payloadcms/next/client` | `@payloadcms/ui` | | `QueryPresetsColumnsCell` | `@payloadcms/next/client` | `@payloadcms/ui` | | `QueryPresetsGroupByCell` | `@payloadcms/next/client` | `@payloadcms/ui` | | `QueryPresetsGroupByField` | `@payloadcms/next/client` | `@payloadcms/ui` | | `QueryPresetsWhereCell` | `@payloadcms/next/client` | `@payloadcms/ui` | | `QueryPresetsWhereField` | `@payloadcms/next/client` | `@payloadcms/ui` | The `./client`, `./rsc`, and `./templates` subpath exports on `@payloadcms/next` are removed entirely. #### Codemod To migrate automatically, there's a codemod for this change available by running: ```bash npx @payloadcms/codemod --transform migrate-next-subpath-exports ```
1 parent 66018c2 commit 1b29f7c

77 files changed

Lines changed: 422 additions & 142 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/custom-components/custom-views.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,8 +177,8 @@ Here is an example of how to use the Default Template in your Custom View:
177177
```tsx
178178
import type { AdminViewServerProps } from 'payload'
179179

180-
import { DefaultTemplate } from '@payloadcms/next/templates'
181180
import { Gutter } from '@payloadcms/ui'
181+
import { DefaultTemplate } from '@payloadcms/ui/rsc'
182182
import React from 'react'
183183

184184
export function MyCustomView({

docs/migration-guide/v4.mdx

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,44 @@ If you were relying on Next-specific extras like `prefetch()` on the router inst
205205
/>
206206
```
207207

208+
### `@payloadcms/next/client`, `@payloadcms/next/rsc`, and `@payloadcms/next/templates` removed
209+
210+
The `./client`, `./rsc`, and `./templates` subpath exports on `@payloadcms/next` were thin re-exports of components that now live in `@payloadcms/ui`. All admin elements (DocumentHeader, FormHeader, HierarchyTypeField, Logo, Nav) and templates (DefaultTemplate, MinimalTemplate) have been relocated to `@payloadcms/ui` and no longer depend on `next/*` modules. Import them directly from the canonical source.
211+
212+
| Symbol | Old source | New source |
213+
| -------------------------- | ---------------------------- | -------------------- |
214+
| `DefaultTemplate` | `@payloadcms/next/templates` | `@payloadcms/ui/rsc` |
215+
| `MinimalTemplate` | `@payloadcms/next/templates` | `@payloadcms/ui/rsc` |
216+
| `CollectionCards` | `@payloadcms/next/rsc` | `@payloadcms/ui/rsc` |
217+
| `DefaultNav` | `@payloadcms/next/rsc` | `@payloadcms/ui/rsc` |
218+
| `DocumentHeader` | `@payloadcms/next/rsc` | `@payloadcms/ui/rsc` |
219+
| `HierarchyTypeFieldServer` | `@payloadcms/next/rsc` | `@payloadcms/ui/rsc` |
220+
| `Logo` | `@payloadcms/next/rsc` | `@payloadcms/ui/rsc` |
221+
| `DefaultNavClient` | `@payloadcms/next/client` | `@payloadcms/ui` |
222+
| `HierarchyTypeField` | `@payloadcms/next/client` | `@payloadcms/ui` |
223+
| `NavSidebarToggle` | `@payloadcms/next/client` | `@payloadcms/ui` |
224+
| `NavWrapper` | `@payloadcms/next/client` | `@payloadcms/ui` |
225+
| `QueryPresetsAccessCell` | `@payloadcms/next/client` | `@payloadcms/ui` |
226+
| `QueryPresetsColumnField` | `@payloadcms/next/client` | `@payloadcms/ui` |
227+
| `QueryPresetsColumnsCell` | `@payloadcms/next/client` | `@payloadcms/ui` |
228+
| `QueryPresetsGroupByCell` | `@payloadcms/next/client` | `@payloadcms/ui` |
229+
| `QueryPresetsGroupByField` | `@payloadcms/next/client` | `@payloadcms/ui` |
230+
| `QueryPresetsWhereCell` | `@payloadcms/next/client` | `@payloadcms/ui` |
231+
| `QueryPresetsWhereField` | `@payloadcms/next/client` | `@payloadcms/ui` |
232+
| `SlugField` | `@payloadcms/next/client` | `@payloadcms/ui` |
233+
234+
```diff
235+
- import { DefaultTemplate, MinimalTemplate } from '@payloadcms/next/templates'
236+
- import { DocumentHeader, Logo } from '@payloadcms/next/rsc'
237+
- import { HierarchyTypeField, SlugField } from '@payloadcms/next/client'
238+
+ import { DefaultTemplate, DocumentHeader, Logo, MinimalTemplate } from '@payloadcms/ui/rsc'
239+
+ import { HierarchyTypeField, SlugField } from '@payloadcms/ui'
240+
```
241+
242+
If you reference any of these components by string path in your Payload config (e.g. `Component: '@payloadcms/next/rsc#CollectionCards'`), update the path string too — then regenerate the import map with `payload generate:importmap`.
243+
244+
Run `npx @payloadcms/codemod --transform migrate-next-subpath-exports` to migrate automatically. It rewrites both `import` declarations and string component paths.
245+
208246
### `title` and `setDocumentTitle` removed from `useDocumentInfo`
209247

210248
For performance reasons, the document title state has been split out of `DocumentInfoContext` into its own `DocumentTitleContext`. Access it through the `useDocumentTitle` hook, which exposes the same `title` and `setDocumentTitle` API. Components that subscribed to `useDocumentInfo` solely for the title will no longer re-render when unrelated document state changes.

examples/custom-components/src/components/views/CustomDefaultRootView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { AdminViewProps } from 'payload'
22

3-
import { DefaultTemplate } from '@payloadcms/next/templates'
43
import { Gutter } from '@payloadcms/ui'
4+
import { DefaultTemplate } from '@payloadcms/ui/rsc'
55
import React from 'react'
66

77
export const CustomDefaultRootView: React.FC<AdminViewProps> = ({

examples/custom-components/src/components/views/CustomMinimalRootView.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { AdminViewProps } from 'payload'
22

3-
import { MinimalTemplate } from '@payloadcms/next/templates'
43
import { Gutter } from '@payloadcms/ui'
4+
import { MinimalTemplate } from '@payloadcms/ui/rsc'
55
import React from 'react'
66

77
export const CustomMinimalRootView: React.FC<AdminViewProps> = () => {

packages/codemod/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ The tool loads your project via [ts-morph](https://ts-morph.com/), using your `t
3636
- `rename-storage-adapters-to-storage` — renames the top-level `storageAdapters` config property to `storage`. Skips any object that already has a `storage` property. Run this if you previously ran `migrate-storage-adapters-to-config` and need to update the property name.
3737
- `migrate-import-export-hooks` — migrates the deprecated `toCSV` and `fromCSV` field options in `custom['plugin-import-export']` to `hooks.beforeExport` and `hooks.beforeImport`. If a `hooks` object already exists it is merged into; if `hooks.beforeExport`/`hooks.beforeImport` already exist the deprecated sibling is dropped without overwriting. Review argument shapes after migration: `beforeExport` uses `siblingData` (not `row`) and `data` is the top-level document (previously `doc`).
3838
- `migrate-db-types-subpath` — rewrites imports from the removed `/types` subpath exports of `@payloadcms/drizzle`, `@payloadcms/db-postgres`, `@payloadcms/db-sqlite`, `@payloadcms/db-vercel-postgres`, and `@payloadcms/db-d1-sqlite` to their main entry points. Also handles re-export declarations and `declare module` augmentations.
39+
- `migrate-next-subpath-exports` — rewrites imports, re-exports, and string-literal component paths from the removed `@payloadcms/next/client`, `@payloadcms/next/rsc`, and `@payloadcms/next/templates` subpaths to their canonical `@payloadcms/ui` or `@payloadcms/ui/rsc` sources. After running, regenerate the import map with `payload generate:importmap`.
3940
- `rename-typescript-schema-to-json-schema` — renames the `typescriptSchema` field-config property to `jsonSchema` (it always accepted JSON Schema, not TypeScript). Skips any object that already defines a `jsonSchema` sibling and surfaces it as a note for manual review.
4041

4142
## Contributing

packages/codemod/src/registry.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { migrateForceSelect } from './transforms/migrate-force-select/index.js'
1010
import { migrateHideAPIURL } from './transforms/migrate-hide-api-url/index.js'
1111
import { migrateImportExportHooks } from './transforms/migrate-import-export-hooks/index.js'
1212
import { migrateListViewSelectAPI } from './transforms/migrate-list-view-select-api/index.js'
13+
import { migrateNextSubpathExports } from './transforms/migrate-next-subpath-exports/index.js'
1314
import { migrateStorageAdaptersToConfig } from './transforms/migrate-storage-adapters-to-config/index.js'
1415
import { renameStorageAdaptersToStorage } from './transforms/rename-storage-adapters-to-storage/index.js'
1516
import { renameTypescriptSchemaToJsonSchema } from './transforms/rename-typescript-schema-to-json-schema/index.js'
@@ -27,5 +28,6 @@ export const transforms: Transform[] = [
2728
renameStorageAdaptersToStorage,
2829
migrateImportExportHooks,
2930
migrateDbTypesSubpath,
31+
migrateNextSubpathExports,
3032
renameTypescriptSchemaToJsonSchema,
3133
]
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { DefaultTemplate, MinimalTemplate } from '@payloadcms/next/templates'
2+
import { CollectionCards, DefaultNav, DocumentHeader, Logo } from '@payloadcms/next/rsc'
3+
import { HierarchyTypeField, SlugField } from '@payloadcms/next/client'
4+
import type { CollectionConfig } from 'payload'
5+
6+
export const widget = { Component: '@payloadcms/next/rsc#CollectionCards' }
7+
export const slug: CollectionConfig['fields'][number] = {
8+
name: 'slug',
9+
type: 'text',
10+
admin: {
11+
components: {
12+
Field: {
13+
path: '@payloadcms/next/client#SlugField',
14+
},
15+
},
16+
},
17+
}
18+
19+
export {
20+
DefaultTemplate,
21+
MinimalTemplate,
22+
CollectionCards,
23+
DefaultNav,
24+
DocumentHeader,
25+
Logo,
26+
HierarchyTypeField,
27+
SlugField,
28+
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { DefaultTemplate, MinimalTemplate } from '@payloadcms/ui/rsc'
2+
import { CollectionCards, DefaultNav, DocumentHeader, Logo } from '@payloadcms/ui/rsc'
3+
import { HierarchyTypeField, SlugField } from '@payloadcms/ui'
4+
import type { CollectionConfig } from 'payload'
5+
6+
export const widget = { Component: '@payloadcms/ui/rsc#CollectionCards' }
7+
export const slug: CollectionConfig['fields'][number] = {
8+
name: 'slug',
9+
type: 'text',
10+
admin: {
11+
components: {
12+
Field: {
13+
path: '@payloadcms/ui#SlugField',
14+
},
15+
},
16+
},
17+
}
18+
19+
export {
20+
DefaultTemplate,
21+
MinimalTemplate,
22+
CollectionCards,
23+
DefaultNav,
24+
DocumentHeader,
25+
Logo,
26+
HierarchyTypeField,
27+
SlugField,
28+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import { readFile } from 'node:fs/promises'
2+
import { dirname, join } from 'node:path'
3+
import { fileURLToPath } from 'node:url'
4+
import { describe, expect, it } from 'vitest'
5+
6+
import { runTransform } from '../../utils/test-helpers.js'
7+
import { migrateNextSubpathExports } from './index.js'
8+
9+
const here = dirname(fileURLToPath(import.meta.url))
10+
const fixture = (name: string) => readFile(join(here, name), 'utf8')
11+
12+
describe('migrate-next-subpath-exports', () => {
13+
it('rewrites import declarations and string component paths', async () => {
14+
const input = await fixture('basic.input.ts')
15+
const output = await fixture('basic.output.ts')
16+
17+
const result = await runTransform({ source: input, transform: migrateNextSubpathExports })
18+
19+
expect(result).toBe(output)
20+
})
21+
22+
it('rewrites re-export declarations from the removed subpaths', async () => {
23+
const input = await fixture('re-export.input.ts')
24+
const output = await fixture('re-export.output.ts')
25+
26+
const result = await runTransform({ source: input, transform: migrateNextSubpathExports })
27+
28+
expect(result).toBe(output)
29+
})
30+
31+
it('is idempotent on already-migrated source', async () => {
32+
const output = await fixture('basic.output.ts')
33+
34+
const result = await runTransform({ source: output, transform: migrateNextSubpathExports })
35+
36+
expect(result).toBe(output)
37+
})
38+
39+
it('leaves unrelated imports and plain prose strings untouched', async () => {
40+
const input = await fixture('non-matching.input.ts')
41+
42+
const result = await runTransform({ source: input, transform: migrateNextSubpathExports })
43+
44+
expect(result).toBe(input)
45+
})
46+
})
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
import { Node, SyntaxKind } from 'ts-morph'
2+
3+
import type { Transform } from '../../types.js'
4+
5+
/**
6+
* Maps the removed `@payloadcms/next` subpaths to their new `@payloadcms/ui`
7+
* targets. All admin elements and templates were relocated to `@payloadcms/ui`
8+
* in v4; the `./client`, `./rsc`, and `./templates` subpaths on
9+
* `@payloadcms/next` have been removed.
10+
*/
11+
const SUBPATH_TO_TARGET: Record<string, string> = {
12+
'@payloadcms/next/client': '@payloadcms/ui',
13+
'@payloadcms/next/rsc': '@payloadcms/ui/rsc',
14+
'@payloadcms/next/templates': '@payloadcms/ui/rsc',
15+
}
16+
17+
// `<package>` or `<package>#<exportName>` — the export identifier follows
18+
// JavaScript identifier rules so we reject anything with whitespace or
19+
// punctuation that would indicate it's actually prose containing the path.
20+
const COMPONENT_PATH_PATTERN = /^@payloadcms\/next\/(client|rsc|templates)(#[A-Za-z_$][\w$]*)?$/
21+
22+
function mapComponentPath(value: string): string | undefined {
23+
const match = COMPONENT_PATH_PATTERN.exec(value)
24+
if (!match) {
25+
return undefined
26+
}
27+
const subpath = `@payloadcms/next/${match[1]}`
28+
const target = SUBPATH_TO_TARGET[subpath]
29+
if (!target) {
30+
return undefined
31+
}
32+
return match[2] ? `${target}${match[2]}` : target
33+
}
34+
35+
export const migrateNextSubpathExports: Transform = {
36+
name: 'migrate-next-subpath-exports',
37+
apply: ({ project }) => {
38+
const filesChanged = new Set<string>()
39+
40+
for (const file of project.getSourceFiles()) {
41+
let mutated = false
42+
43+
for (const importDecl of file.getImportDeclarations()) {
44+
const target = SUBPATH_TO_TARGET[importDecl.getModuleSpecifierValue()]
45+
if (target) {
46+
importDecl.setModuleSpecifier(target)
47+
mutated = true
48+
}
49+
}
50+
51+
for (const exportDecl of file.getExportDeclarations()) {
52+
const specifier = exportDecl.getModuleSpecifierValue()
53+
if (specifier) {
54+
const target = SUBPATH_TO_TARGET[specifier]
55+
if (target) {
56+
exportDecl.setModuleSpecifier(target)
57+
mutated = true
58+
}
59+
}
60+
}
61+
62+
// Rewrite string-literal component paths used in Payload config
63+
// (e.g. `Component: '@payloadcms/next/rsc#CollectionCards'`) and
64+
// import-map keys.
65+
for (const stringLit of file.getDescendantsOfKind(SyntaxKind.StringLiteral)) {
66+
if (isImportOrExportSpecifier(stringLit)) {
67+
continue
68+
}
69+
const value = stringLit.getLiteralValue()
70+
const replacement = mapComponentPath(value)
71+
if (replacement && replacement !== value) {
72+
stringLit.setLiteralValue(replacement)
73+
mutated = true
74+
}
75+
}
76+
77+
if (mutated) {
78+
filesChanged.add(file.getFilePath())
79+
}
80+
}
81+
82+
return { filesChanged: [...filesChanged] }
83+
},
84+
description:
85+
'Rewrites imports, re-exports, and string-literal component paths from the removed `@payloadcms/next/client`, `@payloadcms/next/rsc`, and `@payloadcms/next/templates` subpaths to their canonical `@payloadcms/ui` or `@payloadcms/ui/rsc` sources. After running, regenerate the import map with `payload generate:importmap`.',
86+
}
87+
88+
/**
89+
* Skip string literals that form the module specifier of an import/export
90+
* declaration — those are already handled by the dedicated traversals above
91+
* and rewriting them through `setLiteralValue` would double-process.
92+
*/
93+
function isImportOrExportSpecifier(node: Node): boolean {
94+
const parent = node.getParent()
95+
if (!parent) {
96+
return false
97+
}
98+
if (Node.isImportDeclaration(parent) || Node.isExportDeclaration(parent)) {
99+
return parent.getModuleSpecifier() === node
100+
}
101+
return false
102+
}

0 commit comments

Comments
 (0)