diff --git a/claude.md b/claude.md index 057a630..38b62af 100644 --- a/claude.md +++ b/claude.md @@ -42,6 +42,12 @@ separate service. It powers `/blog`; `/about` is a plain coded page. - **New env vars:** `DATABASE_URL` and `PAYLOAD_SECRET` on the frontend service. Staging must use a different `PAYLOAD_SECRET` from production. - Publishing workflow and house style: `nextjs-app/docs/PUBLISHING.md`. +- **Admin field components resolve through a generated import map** + (`app/(payload)/admin/importMap.js`). Payload hands the client a *path* per + field and looks it up there; a missing entry renders no field and reports no + error, while `required` still blocks the save. After adding or changing any + field, editor or lexical feature, run `npm run generate:importmap` in + `nextjs-app/` and commit the result. ### Two route groups diff --git a/nextjs-app/__tests__/payload/import-map.test.ts b/nextjs-app/__tests__/payload/import-map.test.ts new file mode 100644 index 0000000..ee63647 --- /dev/null +++ b/nextjs-app/__tests__/payload/import-map.test.ts @@ -0,0 +1,67 @@ +/** + * The admin panel does not import field components directly. Payload sends the + * client a *path* for each one — a richText field's is + * `@payloadcms/richtext-lexical/rsc#RscEntryLexicalField` — and resolves it + * through this generated map. An entry that is missing from the map is not an + * error the panel reports: the field simply does not render. + * + * That failure is quietly awful, because `required: true` is enforced on the + * server regardless. A writer gets a new-post form with no Content editor and + * a save that refuses on a field they were never shown. + * + * The map is generated by `npx payload generate:importmap`, so it drifts every + * time a field or a lexical feature is added and nobody re-runs it. These + * assert the entries the current config needs. + */ +import fs from 'fs'; +import path from 'path'; + +const MAP = fs.readFileSync( + path.join(__dirname, '..', '..', 'app', '(payload)', 'admin', 'importMap.js'), + 'utf8', +); +const POSTS = fs.readFileSync( + path.join(__dirname, '..', '..', 'collections', 'Posts.ts'), + 'utf8', +); + +describe('admin import map', () => { + it('resolves the richText field, so Content renders in the editor', () => { + // Guarded because Posts.content is required: without this entry the field + // is invisible and the post is unsaveable. + expect(POSTS).toMatch(/type:\s*'richText'/); + expect(MAP).toContain('@payloadcms/richtext-lexical/rsc#RscEntryLexicalField'); + }); + + it('resolves the richText cell, so the list view can render the column', () => { + expect(MAP).toContain('@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell'); + }); + + it('resolves the diff component, which the drafts UI needs', () => { + // versions.drafts is on, so the panel offers version comparison. + expect(POSTS).toMatch(/drafts:\s*true/); + expect(MAP).toContain('@payloadcms/richtext-lexical/rsc#LexicalDiffComponent'); + }); + + it('resolves BlocksFeature, so the Callout block is insertable', () => { + expect(POSTS).toContain('BlocksFeature'); + expect(MAP).toContain('@payloadcms/richtext-lexical/client#BlocksFeatureClient'); + }); + + it('resolves the default toolbar features the editor is built with', () => { + // defaultFeatures is spread into the editor config; each one contributes a + // client component the toolbar cannot render without. + for (const feature of [ + 'BoldFeatureClient', + 'ItalicFeatureClient', + 'HeadingFeatureClient', + 'LinkFeatureClient', + 'UploadFeatureClient', + 'UnorderedListFeatureClient', + 'OrderedListFeatureClient', + 'InlineToolbarFeatureClient', + ]) { + expect(MAP).toContain(`@payloadcms/richtext-lexical/client#${feature}`); + } + }); +}); diff --git a/nextjs-app/app/(payload)/admin/importMap.js b/nextjs-app/app/(payload)/admin/importMap.js index af86423..a39dff5 100644 --- a/nextjs-app/app/(payload)/admin/importMap.js +++ b/nextjs-app/app/(payload)/admin/importMap.js @@ -1,6 +1,54 @@ +import { RscEntryLexicalCell as RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc' +import { RscEntryLexicalField as RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc' +import { LexicalDiffComponent as LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e } from '@payloadcms/richtext-lexical/rsc' +import { BlocksFeatureClient as BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { BoldFeatureClient as BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { ItalicFeatureClient as ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { UnderlineFeatureClient as UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { StrikethroughFeatureClient as StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { SubscriptFeatureClient as SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { SuperscriptFeatureClient as SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { InlineCodeFeatureClient as InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { ParagraphFeatureClient as ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { HeadingFeatureClient as HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { AlignFeatureClient as AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { IndentFeatureClient as IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { UnorderedListFeatureClient as UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { OrderedListFeatureClient as OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { ChecklistFeatureClient as ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { LinkFeatureClient as LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { RelationshipFeatureClient as RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { BlockquoteFeatureClient as BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { UploadFeatureClient as UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { HorizontalRuleFeatureClient as HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' +import { InlineToolbarFeatureClient as InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864 } from '@payloadcms/richtext-lexical/client' import { CollectionCards as CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } from '@payloadcms/next/rsc' /** @type import('payload').ImportMap */ export const importMap = { + "@payloadcms/richtext-lexical/rsc#RscEntryLexicalCell": RscEntryLexicalCell_44fe37237e0ebf4470c9990d8cb7b07e, + "@payloadcms/richtext-lexical/rsc#RscEntryLexicalField": RscEntryLexicalField_44fe37237e0ebf4470c9990d8cb7b07e, + "@payloadcms/richtext-lexical/rsc#LexicalDiffComponent": LexicalDiffComponent_44fe37237e0ebf4470c9990d8cb7b07e, + "@payloadcms/richtext-lexical/client#BlocksFeatureClient": BlocksFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#BoldFeatureClient": BoldFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#ItalicFeatureClient": ItalicFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#UnderlineFeatureClient": UnderlineFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#StrikethroughFeatureClient": StrikethroughFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#SubscriptFeatureClient": SubscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#SuperscriptFeatureClient": SuperscriptFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#InlineCodeFeatureClient": InlineCodeFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#ParagraphFeatureClient": ParagraphFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#HeadingFeatureClient": HeadingFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#AlignFeatureClient": AlignFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#IndentFeatureClient": IndentFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#UnorderedListFeatureClient": UnorderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#OrderedListFeatureClient": OrderedListFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#ChecklistFeatureClient": ChecklistFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#LinkFeatureClient": LinkFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#RelationshipFeatureClient": RelationshipFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#BlockquoteFeatureClient": BlockquoteFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#UploadFeatureClient": UploadFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#HorizontalRuleFeatureClient": HorizontalRuleFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, + "@payloadcms/richtext-lexical/client#InlineToolbarFeatureClient": InlineToolbarFeatureClient_e70f5e05f09f93e00b997edb1ef0c864, "@payloadcms/next/rsc#CollectionCards": CollectionCards_f9c02e79a4aed9a3924487c0cd4cafb1 } diff --git a/nextjs-app/package.json b/nextjs-app/package.json index ce43585..a9f6b5e 100644 --- a/nextjs-app/package.json +++ b/nextjs-app/package.json @@ -9,6 +9,7 @@ "build": "next build", "start": "next start", "typecheck": "tsc --noEmit", + "generate:importmap": "payload generate:importmap", "test": "jest", "test:watch": "jest --watch", "test:coverage": "jest --coverage"