blocks-dusted 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/README.md +967 -3
  2. package/package.json +2 -2
  3. package/src/commands/add.js +143 -45
  4. package/src/commands/list.js +2 -2
  5. package/src/installers/checkRequirements.js +1 -0
  6. package/src/installers/copyTemplateFiles.js +23 -2
  7. package/src/installers/patchPayloadConfigCollections.js +92 -0
  8. package/src/installers/writeInstalledBlockReadme.js +17 -4
  9. package/src/registry/listTemplates.js +16 -7
  10. package/src/registry/loadTemplateManifest.js +20 -11
  11. package/src/registry/validateTemplateManifest.js +6 -5
  12. package/src/utils/paths.js +7 -0
  13. package/templates/blocks/DD-BookCall/Component.tsx +535 -573
  14. package/templates/blocks/DD-CardTemplate01/Component.tsx +45 -49
  15. package/templates/blocks/DD-Carousel-A/Component.tsx +249 -266
  16. package/templates/blocks/DD-Carousel-B/Component.tsx +403 -432
  17. package/templates/blocks/DD-ComparisonTable/Component.tsx +256 -272
  18. package/templates/blocks/DD-Contact/Component.tsx +434 -439
  19. package/templates/blocks/DD-Contact/config.ts +8 -3
  20. package/templates/blocks/DD-FeatCat/Component.tsx +302 -361
  21. package/templates/blocks/DD-FeatCat/config.ts +201 -204
  22. package/templates/blocks/DD-FeatStrip/Component.tsx +171 -201
  23. package/templates/blocks/DD-Hero/Component.tsx +297 -317
  24. package/templates/blocks/DD-HorizontalScroll/Component.tsx +244 -303
  25. package/templates/blocks/DD-MasonaryMedia/Component.tsx +202 -228
  26. package/templates/blocks/DD-MasonaryMedia/config.ts +13 -14
  27. package/templates/blocks/DD-Pricing/Component.tsx +372 -380
  28. package/templates/blocks/DD-Process/Component.tsx +149 -155
  29. package/templates/blocks/DD-Section/Component.tsx +266 -327
  30. package/templates/blocks/DD-Services/Component.tsx +176 -188
  31. package/templates/blocks/DD-ShowcaseGrid/Component.tsx +307 -317
  32. package/templates/blocks/DD-Slider-A/Component.tsx +237 -252
  33. package/templates/blocks/DD-StackCards/Component.tsx +137 -160
  34. package/templates/blocks/DD-Team/Component.tsx +247 -265
  35. package/templates/blocks/DD-TechStack/Component.tsx +57 -72
  36. package/templates/blocks/DD-Testimonials/Component.tsx +147 -147
  37. package/templates/blocks/DD-Testimonials/config.ts +66 -66
  38. package/templates/blocks/DD-Work/Component.tsx +315 -328
  39. package/templates/blocks/DD-Work-B/Component.tsx +294 -320
  40. package/templates/collections/Testimonials/README.md +11 -0
  41. package/templates/collections/Testimonials/Testimonials.ts +161 -0
  42. package/templates/collections/Testimonials/manifest.json +55 -0
  43. package/templates/components/RichText/README.md +13 -0
  44. package/templates/components/RichText/converter/componentConverter/blocks.tsx +43 -0
  45. package/templates/components/RichText/converter/componentConverter/types.ts +11 -0
  46. package/templates/components/RichText/converter/index.tsx +18 -0
  47. package/templates/components/RichText/converter/internalLinks.tsx +45 -0
  48. package/templates/components/RichText/converter/textConverter.tsx +27 -0
  49. package/templates/components/RichText/index.tsx +35 -0
  50. package/templates/components/RichText/manifest.json +80 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blocks-dusted",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Manifest-driven CLI for installing reusable Payload CMS blocks.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -37,4 +37,4 @@
37
37
  },
38
38
  "homepage": "https://github.com/Hadizainal/blocks-dusted#readme",
39
39
  "license": "MIT"
40
- }
40
+ }
@@ -2,12 +2,15 @@ import { access } from 'node:fs/promises'
2
2
  import { join } from 'node:path'
3
3
  import { checkRequirements } from '../installers/checkRequirements.js'
4
4
  import {
5
+ backupExistingDestination,
5
6
  copySharedFiles,
6
7
  copyTemplateFiles,
8
+ plannedBackupPath,
7
9
  } from '../installers/copyTemplateFiles.js'
8
10
  import { checkDependencies } from '../installers/installDependencies.js'
9
11
  import { writeInstalledBlockReadme } from '../installers/writeInstalledBlockReadme.js'
10
12
  import { patchPayloadBlocksArray } from '../installers/patchPayloadBlocksArray.js'
13
+ import { patchPayloadConfigCollections } from '../installers/patchPayloadConfigCollections.js'
11
14
  import { patchRenderBlocks } from '../installers/patchRenderBlocks.js'
12
15
  import { loadTemplateManifest } from '../registry/loadTemplateManifest.js'
13
16
  import { validateTemplateManifest } from '../registry/validateTemplateManifest.js'
@@ -23,7 +26,7 @@ export async function addTemplate({
23
26
  }) {
24
27
  if (!templateName) {
25
28
  throw new Error(
26
- 'A block name is required. Example: blocks-dusted add DD-Hero --dry-run',
29
+ 'A template name is required. Example: blocks-dusted add DD-Hero --dry-run',
27
30
  )
28
31
  }
29
32
 
@@ -32,6 +35,9 @@ export async function addTemplate({
32
35
  await validateManifestSourceFiles({ manifest, templateDirectory, packageRoot })
33
36
  const checks = await checkRequirements({ manifest, targetDirectory })
34
37
  const dependencyChecks = await checkDependencies({ manifest, targetDirectory })
38
+ const isBlockTemplate = manifest.type === 'block'
39
+ const isCollectionTemplate = manifest.type === 'collection'
40
+ const canBackupDestination = manifest.backupExistingDestination === true
35
41
 
36
42
  if (!dryRun) {
37
43
  await validateTargetProject(targetDirectory)
@@ -43,7 +49,7 @@ export async function addTemplate({
43
49
  }
44
50
  }
45
51
 
46
- heading('Block')
52
+ heading('Template')
47
53
  info(`Name: ${manifest.name}`)
48
54
  if (manifest.sourceName) info(`Source: ${manifest.sourceName}`)
49
55
  if (manifest.portableName) info(`Portable name: ${manifest.portableName}`)
@@ -51,7 +57,7 @@ export async function addTemplate({
51
57
  info(`Type: ${manifest.type}`)
52
58
  info(`Version: ${manifest.version}`)
53
59
  info(`Target directory: ${targetDirectory}`)
54
- info(`Register: ${noRegister ? 'no' : 'yes'}`)
60
+ info(`Register: ${isBlockTemplate && !noRegister ? 'yes' : 'no'}`)
55
61
  info(`Source template path: ${templateDirectory}`)
56
62
  info(`Target install path: ${checks.collisions.destinationPath}`)
57
63
 
@@ -98,28 +104,50 @@ export async function addTemplate({
98
104
  }
99
105
 
100
106
  heading('Collision checks')
107
+ const destinationWillBeBackedUp =
108
+ checks.collisions.destinationExists && canBackupDestination
101
109
  collisionStatus(
102
- !checks.collisions.destinationExists,
103
- `Destination path: ${checks.collisions.destinationPath}`,
104
- )
105
- collisionStatus(
106
- !checks.collisions.slugFound,
107
- `Payload slug: ${manifest.slug}`,
108
- )
109
- collisionStatus(
110
- !checks.collisions.configExportFound,
111
- `Config export: ${manifest.exportName ?? manifest.name}`,
112
- )
113
- collisionStatus(
114
- !checks.collisions.interfaceNameFound,
115
- `interfaceName: ${manifest.interfaceName ?? manifest.exportName ?? manifest.name}`,
116
- )
117
- collisionStatus(
118
- !checks.collisions.renderBlocksMappingFound,
119
- `RenderBlocks mapping: ${manifest.slug}`,
110
+ !checks.collisions.destinationExists || destinationWillBeBackedUp,
111
+ destinationWillBeBackedUp
112
+ ? `Destination path: ${checks.collisions.destinationPath} (will be backed up)`
113
+ : `Destination path: ${checks.collisions.destinationPath}`,
120
114
  )
115
+ if (isBlockTemplate) {
116
+ collisionStatus(
117
+ !checks.collisions.slugFound,
118
+ `Payload slug: ${manifest.slug}`,
119
+ )
120
+ collisionStatus(
121
+ !checks.collisions.configExportFound,
122
+ `Config export: ${manifest.exportName ?? manifest.name}`,
123
+ )
124
+ collisionStatus(
125
+ !checks.collisions.interfaceNameFound,
126
+ `interfaceName: ${manifest.interfaceName ?? manifest.exportName ?? manifest.name}`,
127
+ )
128
+ collisionStatus(
129
+ !checks.collisions.renderBlocksMappingFound,
130
+ `RenderBlocks mapping: ${manifest.slug}`,
131
+ )
132
+ }
133
+
134
+ const destinationBackupPath = destinationWillBeBackedUp
135
+ ? await plannedBackupPath({
136
+ destinationPath: checks.collisions.destinationPath,
137
+ targetDirectory,
138
+ })
139
+ : null
140
+
141
+ if (destinationBackupPath) {
142
+ heading('Existing destination backup')
143
+ item(
144
+ dryRun
145
+ ? `Would back up existing ${checks.collisions.destinationPath} to ${destinationBackupPath}`
146
+ : `Existing ${checks.collisions.destinationPath} will be backed up to ${destinationBackupPath}`,
147
+ )
148
+ }
121
149
 
122
- if (checks.collisions.destinationExists) {
150
+ if (checks.collisions.destinationExists && !canBackupDestination) {
123
151
  throw new Error(
124
152
  `Refusing to install because ${checks.collisions.destinationPath} already exists.`,
125
153
  )
@@ -164,20 +192,37 @@ export async function addTemplate({
164
192
  targetDirectory,
165
193
  dryRun: true,
166
194
  })
167
- const dryRunCollectionResults = noRegister
168
- ? [{ status: 'skipped', reason: '--no-register was passed' }]
169
- : await patchPayloadBlocksArray({
170
- manifest,
171
- targetDirectory,
172
- dryRun: true,
173
- })
174
- const dryRunRenderBlocksResults = noRegister
175
- ? [{ status: 'skipped', reason: '--no-register was passed' }]
176
- : await patchRenderBlocks({
177
- manifest,
178
- targetDirectory,
179
- dryRun: true,
180
- })
195
+ const dryRunCollectionResults = registrationResults({
196
+ isBlockTemplate,
197
+ manifest,
198
+ noRegister,
199
+ registration: 'collection block registration',
200
+ action: () => patchPayloadBlocksArray({
201
+ manifest,
202
+ targetDirectory,
203
+ dryRun: true,
204
+ }),
205
+ })
206
+ const dryRunPayloadConfigResults = collectionRegistrationResults({
207
+ isCollectionTemplate,
208
+ manifest,
209
+ action: () => patchPayloadConfigCollections({
210
+ manifest,
211
+ targetDirectory,
212
+ dryRun: true,
213
+ }),
214
+ })
215
+ const dryRunRenderBlocksResults = registrationResults({
216
+ isBlockTemplate,
217
+ manifest,
218
+ noRegister,
219
+ registration: 'RenderBlocks registration',
220
+ action: () => patchRenderBlocks({
221
+ manifest,
222
+ targetDirectory,
223
+ dryRun: true,
224
+ }),
225
+ })
181
226
 
182
227
  heading('Dry-run shared file results')
183
228
  if (dryRunSharedResults.length === 0) item('None')
@@ -186,12 +231,16 @@ export async function addTemplate({
186
231
  `${result.status}: ${result.to}${result.reason ? ` - ${result.reason}` : ''}`,
187
232
  ),
188
233
  )
234
+ heading('Dry-run Payload config registration')
235
+ ;(await dryRunPayloadConfigResults).forEach((result) =>
236
+ item(`${result.status}: ${result.file ?? result.reason}${result.backupPath ? ` (backup: ${result.backupPath})` : ''}`),
237
+ )
189
238
  heading('Dry-run collection registration')
190
- dryRunCollectionResults.forEach((result) =>
239
+ ;(await dryRunCollectionResults).forEach((result) =>
191
240
  item(`${result.status}: ${result.file ?? result.reason}`),
192
241
  )
193
242
  heading('Dry-run RenderBlocks registration')
194
- dryRunRenderBlocksResults.forEach((result) =>
243
+ ;(await dryRunRenderBlocksResults).forEach((result) =>
195
244
  item(`${result.status}: ${result.file ?? result.reason}`),
196
245
  )
197
246
  info('Dry run complete: no files were changed.')
@@ -204,24 +253,45 @@ export async function addTemplate({
204
253
  targetDirectory,
205
254
  })
206
255
 
256
+ const actualDestinationBackupPath = checks.collisions.destinationExists
257
+ ? await backupExistingDestination({
258
+ destinationPath: checks.collisions.destinationPath,
259
+ targetDirectory,
260
+ })
261
+ : null
262
+
207
263
  const copiedFiles = await copyTemplateFiles({
208
264
  manifest,
209
265
  templateDirectory,
210
266
  targetDirectory,
211
267
  })
212
268
 
213
- const collectionResults = noRegister
214
- ? [{ status: 'skipped', reason: '--no-register was passed' }]
215
- : await patchPayloadBlocksArray({ manifest, targetDirectory })
216
- const renderBlocksResults = noRegister
217
- ? [{ status: 'skipped', reason: '--no-register was passed' }]
218
- : await patchRenderBlocks({ manifest, targetDirectory })
269
+ const payloadConfigResults = await collectionRegistrationResults({
270
+ isCollectionTemplate,
271
+ manifest,
272
+ action: () => patchPayloadConfigCollections({ manifest, targetDirectory }),
273
+ })
274
+ const collectionResults = await registrationResults({
275
+ isBlockTemplate,
276
+ manifest,
277
+ noRegister,
278
+ registration: 'collection block registration',
279
+ action: () => patchPayloadBlocksArray({ manifest, targetDirectory }),
280
+ })
281
+ const renderBlocksResults = await registrationResults({
282
+ isBlockTemplate,
283
+ manifest,
284
+ noRegister,
285
+ registration: 'RenderBlocks registration',
286
+ action: () => patchRenderBlocks({ manifest, targetDirectory }),
287
+ })
219
288
  const readmePath = await writeInstalledBlockReadme({
220
289
  manifest,
221
290
  targetDirectory,
222
291
  copiedFiles,
223
292
  sharedResults,
224
293
  dependencyChecks,
294
+ payloadConfigResults,
225
295
  collectionResults,
226
296
  renderBlocksResults,
227
297
  packageManager: dependencyChecks.packageManager,
@@ -236,6 +306,13 @@ export async function addTemplate({
236
306
 
237
307
  heading('Copied files')
238
308
  copiedFiles.forEach((file) => item(file))
309
+ if (actualDestinationBackupPath) {
310
+ heading('Existing destination backup')
311
+ item(actualDestinationBackupPath)
312
+ item(
313
+ `Review ${actualDestinationBackupPath} and copy any project-specific RichText behaviour missing from the newly installed ${checks.collisions.destinationPath}.`,
314
+ )
315
+ }
239
316
  heading('Shared file results')
240
317
  if (sharedResults.length === 0) item('None')
241
318
  sharedResults.forEach((result) =>
@@ -249,6 +326,12 @@ export async function addTemplate({
249
326
  item(
250
327
  `Run manually: ${installCommandText(dependencyChecks.packageManager, dependencyChecks.missing)}`,
251
328
  )
329
+ heading('Payload config registration')
330
+ payloadConfigResults.forEach((result) =>
331
+ item(
332
+ `${result.status}: ${result.file ?? result.reason}${result.backupPath ? ` (backup: ${result.backupPath})` : ''}`,
333
+ ),
334
+ )
252
335
  heading('Collection registration')
253
336
  collectionResults.forEach((result) =>
254
337
  item(
@@ -270,6 +353,21 @@ export async function addTemplate({
270
353
  )
271
354
  }
272
355
 
356
+ function collectionRegistrationResults({ isCollectionTemplate, manifest, action }) {
357
+ if (!isCollectionTemplate) {
358
+ return [{ status: 'skipped', reason: `${manifest.type} templates do not use Payload config collection registration` }]
359
+ }
360
+ return action()
361
+ }
362
+
363
+ function registrationResults({ isBlockTemplate, manifest, noRegister, registration, action }) {
364
+ if (!isBlockTemplate) {
365
+ return [{ status: 'skipped', reason: `${manifest.type} templates do not use ${registration}` }]
366
+ }
367
+ if (noRegister) return [{ status: 'skipped', reason: '--no-register was passed' }]
368
+ return action()
369
+ }
370
+
273
371
  function formatDependency(dependency) {
274
372
  return typeof dependency === 'string'
275
373
  ? dependency
@@ -324,4 +422,4 @@ async function validateManifestSourceFiles({ manifest, templateDirectory, packag
324
422
  if (missing.length > 0) {
325
423
  throw new Error(`Manifest source files are missing:\n${missing.map((file) => `- ${file}`).join('\n')}`)
326
424
  }
327
- }
425
+ }
@@ -3,7 +3,7 @@ import { heading, info, item } from '../utils/logger.js'
3
3
 
4
4
  export async function list() {
5
5
  const templates = await listTemplates()
6
- heading('Available blocks')
6
+ heading('Available templates')
7
7
 
8
8
  if (templates.length === 0) {
9
9
  item('None')
@@ -15,4 +15,4 @@ export async function list() {
15
15
  }
16
16
 
17
17
  info(`Total: ${templates.length}`)
18
- }
18
+ }
@@ -48,6 +48,7 @@ function contains(files, text) {
48
48
 
49
49
  function commonDestination(files) {
50
50
  if (files.length === 0) return '.'
51
+ if (files.length === 1) return files[0].to
51
52
  return dirname(files[0].to)
52
53
  }
53
54
 
@@ -1,5 +1,5 @@
1
1
  import { constants } from 'node:fs'
2
- import { access, copyFile, cp, mkdir, readFile, stat } from 'node:fs/promises'
2
+ import { access, copyFile, cp, mkdir, readFile, rename, stat } from 'node:fs/promises'
3
3
  import { dirname, join } from 'node:path'
4
4
 
5
5
  export async function copyTemplateFiles({ manifest, templateDirectory, targetDirectory }) {
@@ -17,6 +17,17 @@ export async function copyTemplateFiles({ manifest, templateDirectory, targetDir
17
17
  return copiedFiles
18
18
  }
19
19
 
20
+ export async function backupExistingDestination({ destinationPath, targetDirectory }) {
21
+ const targetPath = join(targetDirectory, destinationPath)
22
+ const backupPath = await nextBackupPath(targetPath)
23
+ await rename(targetPath, backupPath)
24
+ return backupPath
25
+ }
26
+
27
+ export async function plannedBackupPath({ destinationPath, targetDirectory }) {
28
+ return nextBackupPath(join(targetDirectory, destinationPath))
29
+ }
30
+
20
31
  export async function copySharedFiles({ manifest, packageRoot, targetDirectory, dryRun = false }) {
21
32
  const results = []
22
33
  const sharedFiles = manifest.sharedFiles ?? []
@@ -54,6 +65,16 @@ export async function copySharedFiles({ manifest, packageRoot, targetDirectory,
54
65
  return results
55
66
  }
56
67
 
68
+ async function nextBackupPath(path) {
69
+ let candidate = `${path}.bak`
70
+ let index = 1
71
+ while (await pathExists(candidate)) {
72
+ candidate = `${path}.bak.${index}`
73
+ index += 1
74
+ }
75
+ return candidate
76
+ }
77
+
57
78
  async function collisionReason(sourcePath, targetPath) {
58
79
  try {
59
80
  const [sourceStats, targetStats] = await Promise.all([stat(sourcePath), stat(targetPath)])
@@ -76,4 +97,4 @@ async function pathExists(path) {
76
97
  } catch {
77
98
  return false
78
99
  }
79
- }
100
+ }
@@ -0,0 +1,92 @@
1
+ import { access, copyFile, readFile, writeFile } from 'node:fs/promises'
2
+ import { join } from 'node:path'
3
+
4
+ export async function patchPayloadConfigCollections({ manifest, targetDirectory, dryRun = false }) {
5
+ const registration = manifest.payloadConfigRegistration ?? {}
6
+ if (registration.enabled === false) {
7
+ return [{ status: 'skipped', reason: 'Payload config registration disabled by manifest' }]
8
+ }
9
+
10
+ const file = await findPayloadConfig(targetDirectory)
11
+ if (!file) return [{ status: 'skipped', reason: 'No payload.config.ts or payload.config.js found' }]
12
+
13
+ const exportName = registration.exportName ?? manifest.exportName ?? manifest.name
14
+ const importPath = registration.importPath ?? `./collections/${manifest.name}`
15
+ const content = await readFile(file, 'utf8')
16
+ const patched = patchPayloadConfigContent(content, { exportName, importPath })
17
+
18
+ if (!patched.changed) return [{ file, status: 'skipped', reason: patched.reason }]
19
+
20
+ const backupPath = await nextBackupPath(file)
21
+ if (!dryRun) {
22
+ await copyFile(file, backupPath)
23
+ await writeFile(file, patched.content)
24
+ }
25
+
26
+ return [{ file, status: dryRun ? 'would-patch' : 'patched', backupPath }]
27
+ }
28
+
29
+ async function findPayloadConfig(targetDirectory) {
30
+ for (const fileName of ['payload.config.ts', 'payload.config.js', 'src/payload.config.ts', 'src/payload.config.js']) {
31
+ const file = join(targetDirectory, fileName)
32
+ if (await exists(file)) return file
33
+ }
34
+ return null
35
+ }
36
+
37
+ function patchPayloadConfigContent(content, { exportName, importPath }) {
38
+ if (content.includes(importPath) && content.includes(exportName)) {
39
+ return { changed: false, reason: 'Collection already appears to be registered' }
40
+ }
41
+
42
+ const match = selectCollectionsArray(content)
43
+ if (!match) return { changed: false, reason: 'No clear Payload collections array found' }
44
+ if (match.items.includes(exportName)) return { changed: false, reason: 'Collection already exists in collections array' }
45
+
46
+ const importLine = `import { ${exportName} } from '${importPath}'\n`
47
+ const nextWithImport = content.includes(importPath) ? content : insertImport(content, importLine)
48
+ const adjustedMatch = selectCollectionsArray(nextWithImport)
49
+ if (!adjustedMatch) return { changed: false, reason: 'No clear Payload collections array found after import insertion' }
50
+
51
+ const existingItems = adjustedMatch.items.replace(/,\s*$/, '').trim()
52
+ const insertion = existingItems
53
+ ? `collections: [${existingItems}, ${exportName}]`
54
+ : `collections: [${exportName}]`
55
+ const next = `${nextWithImport.slice(0, adjustedMatch.start)}${insertion}${nextWithImport.slice(adjustedMatch.end)}`
56
+ return { changed: next !== content, content: next }
57
+ }
58
+
59
+ function insertImport(content, importLine) {
60
+ const importMatches = [...content.matchAll(/^import .+$/gm)]
61
+ if (importMatches.length === 0) return `${importLine}${content}`
62
+ const last = importMatches[importMatches.length - 1]
63
+ const insertAt = last.index + last[0].length
64
+ return `${content.slice(0, insertAt)}\n${importLine}${content.slice(insertAt)}`
65
+ }
66
+
67
+ function selectCollectionsArray(content) {
68
+ const matches = [...content.matchAll(/collections:\s*\[([\s\S]*?)\]/gm)].map((match) => ({
69
+ start: match.index,
70
+ end: match.index + match[0].length,
71
+ items: match[1],
72
+ }))
73
+ if (matches.length !== 1) return null
74
+ return matches[0]
75
+ }
76
+
77
+ async function nextBackupPath(file) {
78
+ const base = `${file}.bak`
79
+ if (!(await exists(base))) return base
80
+ let index = 1
81
+ while (await exists(`${base}.${index}`)) index += 1
82
+ return `${base}.${index}`
83
+ }
84
+
85
+ async function exists(path) {
86
+ try {
87
+ await access(path)
88
+ return true
89
+ } catch {
90
+ return false
91
+ }
92
+ }
@@ -7,18 +7,20 @@ export async function writeInstalledBlockReadme({
7
7
  copiedFiles,
8
8
  sharedResults,
9
9
  dependencyChecks,
10
+ payloadConfigResults = [],
10
11
  collectionResults,
11
12
  renderBlocksResults,
12
13
  packageManager,
13
14
  installCommand,
14
15
  }) {
15
- const readmePath = join(targetDirectory, 'src', 'blocks', manifest.name, 'README.md')
16
+ const readmePath = join(targetDirectory, destinationDirectory(manifest.files), 'README.md')
16
17
  await mkdir(dirname(readmePath), { recursive: true })
17
18
  await writeFile(readmePath, renderReadme({
18
19
  manifest,
19
20
  copiedFiles,
20
21
  sharedResults,
21
22
  dependencyChecks,
23
+ payloadConfigResults,
22
24
  collectionResults,
23
25
  renderBlocksResults,
24
26
  packageManager,
@@ -32,6 +34,7 @@ function renderReadme({
32
34
  copiedFiles,
33
35
  sharedResults,
34
36
  dependencyChecks,
37
+ payloadConfigResults = [],
35
38
  collectionResults,
36
39
  renderBlocksResults,
37
40
  packageManager,
@@ -42,6 +45,7 @@ function renderReadme({
42
45
 
43
46
  return `# ${manifest.name}
44
47
 
48
+ Template type: \`${manifest.type}\`
45
49
  Payload slug: \`${manifest.slug}\`
46
50
 
47
51
  ## Files installed
@@ -66,6 +70,10 @@ Install missing dependencies manually:
66
70
  ${dependencyCommand}
67
71
  \`\`\`
68
72
 
73
+ ## Payload config registration status
74
+
75
+ ${list(payloadConfigResults.map(formatPatchResult))}
76
+
69
77
  ## Collection registration status
70
78
 
71
79
  ${list(collectionResults.map(formatPatchResult))}
@@ -88,17 +96,22 @@ ${generatorPrefix} generate:importmap
88
96
  ## Copy-paste Codex prompt for finishing integration
89
97
 
90
98
  \`\`\`text
91
- Review the installed ${manifest.name} block integration.
99
+ Review the installed ${manifest.name} ${manifest.type} integration.
92
100
  Only fix issues directly related to ${manifest.name}.
93
101
  Check imports, Pages collection, Posts collection, RenderBlocks, shared utilities, shared components, and missing dependencies.
94
102
  Run Payload generators manually if schema or admin imports changed.
95
103
  Do not rewrite unrelated application logic.
96
104
  Do not change environment files.
97
- Do not rewrite routes unless explicitly required by the block README.
105
+ Do not rewrite routes unless explicitly required by the installed README.
98
106
  \`\`\`
99
107
  `
100
108
  }
101
109
 
110
+ function destinationDirectory(files) {
111
+ if (!files || files.length === 0) return '.'
112
+ return dirname(files[0].to)
113
+ }
114
+
102
115
  function formatDependency(dependency) {
103
116
  return `${dependency.name}${dependency.version ? `@${dependency.version}` : ''}`
104
117
  }
@@ -114,4 +127,4 @@ function manualSteps(manifest) {
114
127
  function list(values) {
115
128
  if (!values || values.length === 0) return '- None'
116
129
  return values.map((value) => `- ${value}`).join('\n')
117
- }
130
+ }
@@ -1,17 +1,26 @@
1
1
  import { readdir } from 'node:fs/promises'
2
+ import { join } from 'node:path'
2
3
  import { loadTemplateManifest } from './loadTemplateManifest.js'
3
4
  import { validateTemplateManifest } from './validateTemplateManifest.js'
4
- import { blocksDirectory } from '../utils/paths.js'
5
+ import { templateDirectories } from '../utils/paths.js'
5
6
 
6
7
  export async function listTemplates() {
7
- const entries = await readdir(blocksDirectory, { withFileTypes: true })
8
8
  const templates = []
9
9
 
10
- for (const entry of entries.filter((item) => item.isDirectory())) {
11
- const { manifest } = await loadTemplateManifest(entry.name)
12
- validateTemplateManifest(manifest)
13
- templates.push(manifest)
10
+ for (const directory of templateDirectories) {
11
+ let entries = []
12
+ try {
13
+ entries = await readdir(directory.path, { withFileTypes: true })
14
+ } catch {
15
+ continue
16
+ }
17
+
18
+ for (const entry of entries.filter((item) => item.isDirectory())) {
19
+ const { manifest } = await loadTemplateManifest(entry.name)
20
+ validateTemplateManifest(manifest)
21
+ templates.push(manifest)
22
+ }
14
23
  }
15
24
 
16
25
  return templates.sort((a, b) => a.name.localeCompare(b.name))
17
- }
26
+ }
@@ -1,20 +1,29 @@
1
1
  import { readFile } from 'node:fs/promises'
2
2
  import { join } from 'node:path'
3
- import { blocksDirectory } from '../utils/paths.js'
3
+ import { templateDirectories } from '../utils/paths.js'
4
4
  import { isSafeTemplateName } from '../utils/strings.js'
5
5
 
6
6
  export async function loadTemplateManifest(templateName) {
7
7
  if (!isSafeTemplateName(templateName)) throw new Error(`Invalid template name: ${templateName}`)
8
8
 
9
- const templateDirectory = join(blocksDirectory, templateName)
10
- const manifestPath = join(templateDirectory, 'manifest.json')
9
+ const misses = []
11
10
 
12
- try {
13
- const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
14
- return { manifest, manifestPath, templateDirectory }
15
- } catch (error) {
16
- if (error.code === 'ENOENT') throw new Error(`Template not found: ${templateName}`)
17
- if (error instanceof SyntaxError) throw new Error(`Invalid JSON in ${manifestPath}: ${error.message}`)
18
- throw error
11
+ for (const directory of templateDirectories) {
12
+ const templateDirectory = join(directory.path, templateName)
13
+ const manifestPath = join(templateDirectory, 'manifest.json')
14
+
15
+ try {
16
+ const manifest = JSON.parse(await readFile(manifestPath, 'utf8'))
17
+ return { manifest, manifestPath, templateDirectory }
18
+ } catch (error) {
19
+ if (error.code === 'ENOENT') {
20
+ misses.push(manifestPath)
21
+ continue
22
+ }
23
+ if (error instanceof SyntaxError) throw new Error(`Invalid JSON in ${manifestPath}: ${error.message}`)
24
+ throw error
25
+ }
19
26
  }
20
- }
27
+
28
+ throw new Error(`Template not found: ${templateName}`)
29
+ }
@@ -1,4 +1,5 @@
1
1
  const REQUIRED_STRINGS = ['name', 'type', 'slug', 'version']
2
+ const TEMPLATE_TYPES = new Set(['block', 'component', 'collection'])
2
3
  const OPTIONAL_ARRAYS = [
3
4
  'files',
4
5
  'requiredProjectFiles',
@@ -14,13 +15,13 @@ const OPTIONAL_ARRAYS = [
14
15
  export function validateTemplateManifest(manifest) {
15
16
  const errors = []
16
17
  if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
17
- throw new Error('Block manifest must be a JSON object.')
18
+ throw new Error('Template manifest must be a JSON object.')
18
19
  }
19
20
 
20
21
  for (const key of REQUIRED_STRINGS) {
21
22
  if (typeof manifest[key] !== 'string' || manifest[key].trim() === '') errors.push(`${key} must be a non-empty string`)
22
23
  }
23
- if (manifest.type !== 'block') errors.push('type must be "block"')
24
+ if (!TEMPLATE_TYPES.has(manifest.type)) errors.push('type must be one of: block, component, collection')
24
25
  if (manifest.label !== undefined && typeof manifest.label !== 'string') errors.push('label must be a string when present')
25
26
  if (manifest.sourceName !== undefined && typeof manifest.sourceName !== 'string') errors.push('sourceName must be a string when present')
26
27
  if (manifest.portableName !== undefined && typeof manifest.portableName !== 'string') errors.push('portableName must be a string when present')
@@ -33,7 +34,7 @@ export function validateTemplateManifest(manifest) {
33
34
  if (!Array.isArray(manifest.files)) errors.push('files must be an array')
34
35
  if (manifest.source !== undefined && (!manifest.source || typeof manifest.source !== 'object' || Array.isArray(manifest.source))) errors.push('source must be an object when present')
35
36
  if (manifest.patchTargets !== undefined && (!manifest.patchTargets || typeof manifest.patchTargets !== 'object' || Array.isArray(manifest.patchTargets))) errors.push('patchTargets must be an object when present')
36
- for (const key of ['collectionRegistration', 'renderBlocksRegistration', 'renderBlocksBehavior', 'pageRouteIntegration']) {
37
+ for (const key of ['collectionRegistration', 'renderBlocksRegistration', 'renderBlocksBehavior', 'pageRouteIntegration', 'payloadConfigRegistration']) {
37
38
  if (manifest[key] !== undefined && (!manifest[key] || typeof manifest[key] !== 'object' || Array.isArray(manifest[key]))) errors.push(`${key} must be an object when present`)
38
39
  }
39
40
 
@@ -64,6 +65,6 @@ export function validateTemplateManifest(manifest) {
64
65
  })
65
66
  }
66
67
 
67
- if (errors.length > 0) throw new Error(`Invalid block manifest:\n- ${errors.join('\n- ')}`)
68
+ if (errors.length > 0) throw new Error(`Invalid template manifest:\n- ${errors.join('\n- ')}`)
68
69
  return manifest
69
- }
70
+ }