blocks-dusted 0.1.0

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 (102) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +68 -0
  3. package/bin/index.js +8 -0
  4. package/package.json +40 -0
  5. package/src/cli.js +79 -0
  6. package/src/commands/add.js +327 -0
  7. package/src/commands/doctor.js +21 -0
  8. package/src/commands/list.js +18 -0
  9. package/src/installers/checkRequirements.js +101 -0
  10. package/src/installers/copyTemplateFiles.js +79 -0
  11. package/src/installers/installDependencies.js +69 -0
  12. package/src/installers/patchPayloadBlocksArray.js +122 -0
  13. package/src/installers/patchRenderBlocks.js +110 -0
  14. package/src/installers/runGenerators.js +3 -0
  15. package/src/installers/writeInstalledBlockReadme.js +117 -0
  16. package/src/registry/listTemplates.js +17 -0
  17. package/src/registry/loadTemplateManifest.js +20 -0
  18. package/src/registry/validateTemplateManifest.js +69 -0
  19. package/src/utils/logger.js +8 -0
  20. package/src/utils/packageManager.js +42 -0
  21. package/src/utils/paths.js +7 -0
  22. package/src/utils/project.js +122 -0
  23. package/src/utils/strings.js +7 -0
  24. package/templates/blocks/DD-BookCall/Component.tsx +573 -0
  25. package/templates/blocks/DD-BookCall/config.ts +239 -0
  26. package/templates/blocks/DD-BookCall/manifest.json +112 -0
  27. package/templates/blocks/DD-CardTemplate01/Component.tsx +144 -0
  28. package/templates/blocks/DD-CardTemplate01/config.ts +106 -0
  29. package/templates/blocks/DD-CardTemplate01/manifest.json +84 -0
  30. package/templates/blocks/DD-Carousel-A/Component.tsx +266 -0
  31. package/templates/blocks/DD-Carousel-A/config.ts +163 -0
  32. package/templates/blocks/DD-Carousel-A/manifest.json +86 -0
  33. package/templates/blocks/DD-Carousel-B/Component.tsx +432 -0
  34. package/templates/blocks/DD-Carousel-B/config.ts +176 -0
  35. package/templates/blocks/DD-Carousel-B/manifest.json +86 -0
  36. package/templates/blocks/DD-ComparisonTable/Component.tsx +272 -0
  37. package/templates/blocks/DD-ComparisonTable/config.ts +105 -0
  38. package/templates/blocks/DD-ComparisonTable/manifest.json +78 -0
  39. package/templates/blocks/DD-Contact/Component.tsx +439 -0
  40. package/templates/blocks/DD-Contact/config.ts +217 -0
  41. package/templates/blocks/DD-Contact/manifest.json +107 -0
  42. package/templates/blocks/DD-FeatCat/Component.tsx +361 -0
  43. package/templates/blocks/DD-FeatCat/config.ts +204 -0
  44. package/templates/blocks/DD-FeatCat/manifest.json +87 -0
  45. package/templates/blocks/DD-FeatStrip/Component.tsx +615 -0
  46. package/templates/blocks/DD-FeatStrip/config.ts +134 -0
  47. package/templates/blocks/DD-FeatStrip/manifest.json +82 -0
  48. package/templates/blocks/DD-Hero/Component.tsx +317 -0
  49. package/templates/blocks/DD-Hero/config.ts +267 -0
  50. package/templates/blocks/DD-Hero/manifest.json +92 -0
  51. package/templates/blocks/DD-HorizontalScroll/Component.tsx +995 -0
  52. package/templates/blocks/DD-HorizontalScroll/config.ts +231 -0
  53. package/templates/blocks/DD-HorizontalScroll/manifest.json +85 -0
  54. package/templates/blocks/DD-MasonaryMedia/Component.tsx +571 -0
  55. package/templates/blocks/DD-MasonaryMedia/config.ts +390 -0
  56. package/templates/blocks/DD-MasonaryMedia/manifest.json +101 -0
  57. package/templates/blocks/DD-MasonaryMedia/vendor.d.ts +2 -0
  58. package/templates/blocks/DD-Pricing/Component.tsx +380 -0
  59. package/templates/blocks/DD-Pricing/config.ts +215 -0
  60. package/templates/blocks/DD-Pricing/manifest.json +78 -0
  61. package/templates/blocks/DD-Process/Component.tsx +155 -0
  62. package/templates/blocks/DD-Process/config.ts +150 -0
  63. package/templates/blocks/DD-Process/manifest.json +82 -0
  64. package/templates/blocks/DD-Section/Component.tsx +327 -0
  65. package/templates/blocks/DD-Section/config.ts +214 -0
  66. package/templates/blocks/DD-Section/manifest.json +85 -0
  67. package/templates/blocks/DD-Services/Component.tsx +188 -0
  68. package/templates/blocks/DD-Services/config.ts +149 -0
  69. package/templates/blocks/DD-Services/manifest.json +95 -0
  70. package/templates/blocks/DD-ShowcaseGrid/Component.tsx +317 -0
  71. package/templates/blocks/DD-ShowcaseGrid/config.ts +197 -0
  72. package/templates/blocks/DD-ShowcaseGrid/manifest.json +80 -0
  73. package/templates/blocks/DD-Slider-A/Component.tsx +252 -0
  74. package/templates/blocks/DD-Slider-A/config.ts +118 -0
  75. package/templates/blocks/DD-Slider-A/manifest.json +86 -0
  76. package/templates/blocks/DD-StackCards/Component.tsx +496 -0
  77. package/templates/blocks/DD-StackCards/config.ts +399 -0
  78. package/templates/blocks/DD-StackCards/manifest.json +87 -0
  79. package/templates/blocks/DD-Team/Component.tsx +265 -0
  80. package/templates/blocks/DD-Team/config.ts +173 -0
  81. package/templates/blocks/DD-Team/manifest.json +82 -0
  82. package/templates/blocks/DD-TechStack/Component.tsx +189 -0
  83. package/templates/blocks/DD-TechStack/config.ts +153 -0
  84. package/templates/blocks/DD-TechStack/manifest.json +118 -0
  85. package/templates/blocks/DD-Testimonials/Component.tsx +147 -0
  86. package/templates/blocks/DD-Testimonials/config.ts +120 -0
  87. package/templates/blocks/DD-Testimonials/manifest.json +87 -0
  88. package/templates/blocks/DD-Work/Component.tsx +328 -0
  89. package/templates/blocks/DD-Work/config.ts +165 -0
  90. package/templates/blocks/DD-Work/manifest.json +87 -0
  91. package/templates/blocks/DD-Work-B/Component.tsx +320 -0
  92. package/templates/blocks/DD-Work-B/config.ts +165 -0
  93. package/templates/blocks/DD-Work-B/manifest.json +75 -0
  94. package/templates/shared/components/ui/anims/fade.tsx +83 -0
  95. package/templates/shared/components/ui/button.tsx +233 -0
  96. package/templates/shared/components/ui/input.tsx +41 -0
  97. package/templates/shared/fields/lucide-icon-picker/Cell.tsx +24 -0
  98. package/templates/shared/fields/lucide-icon-picker/README.md +160 -0
  99. package/templates/shared/fields/lucide-icon-picker/field.ts +40 -0
  100. package/templates/shared/fields/lucide-icon-picker/index.tsx +203 -0
  101. package/templates/shared/utilities/canUseDOM.ts +1 -0
  102. package/templates/shared/utilities/getURL.ts +31 -0
@@ -0,0 +1,79 @@
1
+ import { constants } from 'node:fs'
2
+ import { access, copyFile, cp, mkdir, readFile, stat } from 'node:fs/promises'
3
+ import { dirname, join } from 'node:path'
4
+
5
+ export async function copyTemplateFiles({ manifest, templateDirectory, targetDirectory }) {
6
+ const copiedFiles = []
7
+
8
+ for (const file of manifest.files) {
9
+ const sourcePath = join(templateDirectory, file.from)
10
+ const destinationPath = join(targetDirectory, file.to)
11
+
12
+ await mkdir(dirname(destinationPath), { recursive: true })
13
+ await copyFile(sourcePath, destinationPath, constants.COPYFILE_EXCL)
14
+ copiedFiles.push(file.to)
15
+ }
16
+
17
+ return copiedFiles
18
+ }
19
+
20
+ export async function copySharedFiles({ manifest, packageRoot, targetDirectory, dryRun = false }) {
21
+ const results = []
22
+ const sharedFiles = manifest.sharedFiles ?? []
23
+
24
+ for (const file of sharedFiles) {
25
+ const sourcePath = join(packageRoot, file.from)
26
+ const targetPath = join(targetDirectory, file.to)
27
+ const exists = await pathExists(targetPath)
28
+
29
+ if (exists) {
30
+ results.push({
31
+ ...file,
32
+ status: 'skipped',
33
+ reason: await collisionReason(sourcePath, targetPath),
34
+ })
35
+ continue
36
+ }
37
+
38
+ if (file.safeToCopy === false) {
39
+ results.push({ ...file, status: 'manual', reason: 'Manifest marks this shared file as manual' })
40
+ continue
41
+ }
42
+
43
+ if (dryRun) {
44
+ results.push({ ...file, status: 'would-copy' })
45
+ continue
46
+ }
47
+
48
+ await mkdir(dirname(targetPath), { recursive: true })
49
+ if ((await stat(sourcePath)).isDirectory()) await cp(sourcePath, targetPath, { recursive: true, errorOnExist: true, force: false })
50
+ else await copyFile(sourcePath, targetPath, constants.COPYFILE_EXCL)
51
+ results.push({ ...file, status: 'copied' })
52
+ }
53
+
54
+ return results
55
+ }
56
+
57
+ async function collisionReason(sourcePath, targetPath) {
58
+ try {
59
+ const [sourceStats, targetStats] = await Promise.all([stat(sourcePath), stat(targetPath)])
60
+ if (sourceStats.isFile() && targetStats.isFile()) {
61
+ const [sourceContent, targetContent] = await Promise.all([readFile(sourcePath), readFile(targetPath)])
62
+ return Buffer.compare(sourceContent, targetContent) === 0
63
+ ? 'Target already exists with identical contents'
64
+ : 'Target already exists with different contents; not overwritten'
65
+ }
66
+ return 'Target already exists; not overwritten'
67
+ } catch {
68
+ return 'Target already exists; not overwritten'
69
+ }
70
+ }
71
+
72
+ async function pathExists(path) {
73
+ try {
74
+ await access(path)
75
+ return true
76
+ } catch {
77
+ return false
78
+ }
79
+ }
@@ -0,0 +1,69 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import { join } from 'node:path'
3
+ import { detectPackageManager, installCommand } from '../utils/packageManager.js'
4
+
5
+ export async function checkDependencies({ manifest, targetDirectory }) {
6
+ const declared = normalizeDependencies(manifest)
7
+ const packageJson = await readPackageJson(targetDirectory)
8
+ const installed = {
9
+ ...packageJson.dependencies,
10
+ ...packageJson.devDependencies,
11
+ ...packageJson.peerDependencies,
12
+ }
13
+
14
+ const missing = declared.filter((dependency) => !installed[dependency.name])
15
+
16
+ return {
17
+ declared,
18
+ missing,
19
+ installed: declared.filter((dependency) => installed[dependency.name]),
20
+ packageManager: await detectPackageManager(targetDirectory),
21
+ }
22
+ }
23
+
24
+ export async function installDependencies({ dependencies, targetDirectory }) {
25
+ const names = dependencies.map(formatDependency)
26
+ const packageManager = await detectPackageManager(targetDirectory)
27
+ const command = installCommand(packageManager, names)
28
+ return { command, installed: [], skipped: names }
29
+ }
30
+
31
+ function normalizeDependencies(manifest) {
32
+ const dependencies = manifest.dependencies ?? manifest.packageDependencies ?? []
33
+ return dependencies.map((dependency) => {
34
+ if (typeof dependency === 'string') return { name: packageName(dependency), version: versionPart(dependency) }
35
+ return {
36
+ name: dependency.name,
37
+ version: dependency.version,
38
+ }
39
+ }).filter((dependency) => dependency.name)
40
+ }
41
+
42
+ function formatDependency(dependency) {
43
+ return `${dependency.name}${dependency.version ? `@${dependency.version}` : ''}`
44
+ }
45
+
46
+ function packageName(value) {
47
+ if (value.startsWith('@')) {
48
+ const parts = value.split('@')
49
+ return `@${parts[1]}`
50
+ }
51
+ return value.split('@')[0]
52
+ }
53
+
54
+ function versionPart(value) {
55
+ if (value.startsWith('@')) {
56
+ const parts = value.split('@')
57
+ return parts.length > 2 ? parts.slice(2).join('@') : undefined
58
+ }
59
+ const parts = value.split('@')
60
+ return parts.length > 1 ? parts.slice(1).join('@') : undefined
61
+ }
62
+
63
+ async function readPackageJson(targetDirectory) {
64
+ try {
65
+ return JSON.parse(await readFile(join(targetDirectory, 'package.json'), 'utf8'))
66
+ } catch {
67
+ return {}
68
+ }
69
+ }
@@ -0,0 +1,122 @@
1
+ import { access, copyFile, readdir, readFile, writeFile } from 'node:fs/promises'
2
+ import { basename, join } from 'node:path'
3
+
4
+ export async function patchPayloadBlocksArray({ manifest, targetDirectory, dryRun = false }) {
5
+ const registration = manifest.collectionRegistration ?? {}
6
+ if (registration.enabled === false) return [{ status: 'skipped', reason: 'Collection registration disabled by manifest' }]
7
+
8
+ const files = await findCollectionFiles(join(targetDirectory, 'src', 'collections'))
9
+ const wanted = new Set(registration.collections ?? ['Pages', 'Posts'])
10
+ const results = []
11
+
12
+ for (const file of files) {
13
+ const collectionName = collectionNameFromPath(file)
14
+ if (!wanted.has(collectionName)) continue
15
+
16
+ const content = await readFile(file, 'utf8')
17
+ const patched = patchCollectionContent(content, manifest.exportName ?? manifest.name, manifest.name)
18
+ if (!patched.changed) {
19
+ results.push({ file, collection: collectionName, status: 'skipped', reason: patched.reason })
20
+ continue
21
+ }
22
+
23
+ const backupPath = await nextBackupPath(file)
24
+ if (!dryRun) {
25
+ await copyFile(file, backupPath)
26
+ await writeFile(file, patched.content)
27
+ }
28
+ results.push({ file, collection: collectionName, status: dryRun ? 'would-patch' : 'patched', backupPath })
29
+ }
30
+
31
+ if (results.length === 0) results.push({ status: 'skipped', reason: 'No confident Pages or Posts collection block arrays found' })
32
+ return results
33
+ }
34
+
35
+ async function nextBackupPath(file) {
36
+ const base = `${file}.bak`
37
+ if (!(await exists(base))) return base
38
+ let index = 1
39
+ while (await exists(`${base}.${index}`)) index += 1
40
+ return `${base}.${index}`
41
+ }
42
+
43
+ async function exists(path) {
44
+ try {
45
+ await access(path)
46
+ return true
47
+ } catch {
48
+ return false
49
+ }
50
+ }
51
+
52
+ function patchCollectionContent(content, exportName, blockPath = exportName) {
53
+ if (!content.includes("type: 'blocks'") && !content.includes('type: "blocks"')) {
54
+ return { changed: false, reason: "No Payload field with type: 'blocks' found" }
55
+ }
56
+ if (content.includes(`@/blocks/${blockPath}/config`) && content.includes(exportName)) {
57
+ return { changed: false, reason: 'Block already appears to be registered' }
58
+ }
59
+
60
+ const importLine = `import { ${exportName} } from '@/blocks/${blockPath}/config'\n`
61
+ let next = content.includes(`@/blocks/${blockPath}/config`) ? content : `${importLine}${content}`
62
+ const match = selectBlocksArray(next)
63
+ if (!match) return { changed: false, reason: 'No blocks array found near a blocks field' }
64
+ if (match.ambiguous) return { changed: false, reason: match.reason }
65
+ if (match.items.includes(exportName)) return { changed: false, reason: 'Block already exists in blocks array' }
66
+
67
+ const existingItems = match.items.replace(/,\s*$/, '').trim()
68
+ const insertion = existingItems
69
+ ? `\n ${existingItems},\n ${exportName},\n `
70
+ : `\n ${exportName},\n `
71
+ next = `${next.slice(0, match.start)}blocks: [${insertion}]${next.slice(match.end)}`
72
+ return { changed: next !== content, content: next }
73
+ }
74
+
75
+ function selectBlocksArray(content) {
76
+ const matches = [...content.matchAll(/blocks:\s*\[([\s\S]*?)\]/gm)].map((match) => ({
77
+ start: match.index,
78
+ end: match.index + match[0].length,
79
+ items: match[1],
80
+ context: content.slice(Math.max(0, match.index - 700), match.index),
81
+ }))
82
+
83
+ if (matches.length === 0) return null
84
+ if (matches.length === 1) return matches[0]
85
+
86
+ const layoutMatches = matches.filter((match) => /name:\s*['"]layout['"][\s\S]*?type:\s*['"]blocks['"]/.test(match.context))
87
+ if (layoutMatches.length === 1) return layoutMatches[0]
88
+
89
+ return {
90
+ ambiguous: true,
91
+ reason: layoutMatches.length > 1
92
+ ? 'Multiple layout blocks arrays found; manual registration required'
93
+ : 'Multiple blocks arrays found and no clear layout field; manual registration required',
94
+ }
95
+ }
96
+
97
+ async function findCollectionFiles(directory) {
98
+ const files = []
99
+ await walk(directory, files)
100
+ return files.filter((file) => /\.(ts|js)$/.test(file))
101
+ }
102
+
103
+ async function walk(directory, output) {
104
+ let entries
105
+ try {
106
+ entries = await readdir(directory, { withFileTypes: true })
107
+ } catch {
108
+ return
109
+ }
110
+ for (const entry of entries) {
111
+ const path = join(directory, entry.name)
112
+ if (entry.isDirectory()) await walk(path, output)
113
+ else output.push(path)
114
+ }
115
+ }
116
+
117
+ function collectionNameFromPath(file) {
118
+ const parts = file.split(/[\\/]/)
119
+ const index = parts.lastIndexOf('collections')
120
+ if (index >= 0 && parts[index + 1]) return parts[index + 1]
121
+ return basename(file, /\.(ts|js)$/)
122
+ }
@@ -0,0 +1,110 @@
1
+ import { access, copyFile, readdir, readFile, writeFile } from 'node:fs/promises'
2
+ import { join } from 'node:path'
3
+
4
+ export async function patchRenderBlocks({ manifest, targetDirectory, dryRun = false }) {
5
+ const registration = manifest.renderBlocksRegistration ?? {}
6
+ if (registration.enabled === false) return [{ status: 'skipped', reason: 'RenderBlocks registration disabled by manifest' }]
7
+
8
+ const files = await findRenderBlocksFiles(join(targetDirectory, 'src', 'blocks'))
9
+ const results = []
10
+
11
+ for (const file of files) {
12
+ const content = await readFile(file, 'utf8')
13
+ const patched = patchRenderBlocksContent(content, manifest)
14
+ if (!patched.changed) {
15
+ results.push({ file, status: 'skipped', reason: patched.reason })
16
+ continue
17
+ }
18
+
19
+ const backupPath = await nextBackupPath(file)
20
+ if (!dryRun) {
21
+ await copyFile(file, backupPath)
22
+ await writeFile(file, patched.content)
23
+ }
24
+ results.push({ file, status: dryRun ? 'would-patch' : 'patched', backupPath })
25
+ }
26
+
27
+ if (results.length === 0) results.push({ status: 'skipped', reason: 'No RenderBlocks file found under src/blocks' })
28
+ return results
29
+ }
30
+
31
+ async function nextBackupPath(file) {
32
+ const base = `${file}.bak`
33
+ if (!(await exists(base))) return base
34
+ let index = 1
35
+ while (await exists(`${base}.${index}`)) index += 1
36
+ return `${base}.${index}`
37
+ }
38
+
39
+ async function exists(path) {
40
+ try {
41
+ await access(path)
42
+ return true
43
+ } catch {
44
+ return false
45
+ }
46
+ }
47
+
48
+ function patchRenderBlocksContent(content, manifest) {
49
+ const exportName = manifest.exportName ?? manifest.name
50
+ if (content.includes(manifest.slug) || content.includes(`@/blocks/${manifest.name}/Component`)) {
51
+ return { changed: false, reason: 'RenderBlocks already appears to contain this block' }
52
+ }
53
+
54
+ const usesDynamic = content.includes('next/dynamic') || content.includes('dynamic(')
55
+ let next = content
56
+ if (usesDynamic) {
57
+ if (!content.includes('dynamic(')) return { changed: false, reason: 'Dynamic import style unclear' }
58
+ const importLine = `const ${exportName} = dynamic(() => import('@/blocks/${manifest.name}/Component').then((mod) => mod.${exportName}))\n`
59
+ next = insertAfterImports(next, importLine)
60
+ } else {
61
+ const importLine = `import { ${exportName} } from '@/blocks/${manifest.name}/Component'\n`
62
+ next = insertAfterImports(next, importLine)
63
+ }
64
+
65
+ const mapPattern = /(blockComponents\s*=\s*\{)([\s\S]*?)(\s*\})/m
66
+ const match = next.match(mapPattern)
67
+ if (!match) return { changed: false, reason: 'No blockComponents object found' }
68
+
69
+ const existingMappings = match[2].replace(/,\s*$/, '').trim()
70
+ const mappingInsertion = existingMappings
71
+ ? `\n ${existingMappings},\n '${manifest.slug}': ${exportName},\n`
72
+ : `\n '${manifest.slug}': ${exportName},\n`
73
+ next = next.replace(mapPattern, `$1${mappingInsertion}$3`)
74
+
75
+ if (manifest.renderBlocksBehavior?.wrapperless === true) {
76
+ if (next.includes('wrapperlessBlockTypes') && !next.includes(`'${manifest.slug}'`)) {
77
+ next = next.replace(/(wrapperlessBlockTypes\s*=\s*new Set\(\[)([\s\S]*?)(\]\))/m, `$1$2\n '${manifest.slug}',$3`)
78
+ }
79
+ }
80
+
81
+ return { changed: next !== content, content: next }
82
+ }
83
+
84
+ function insertAfterImports(content, line) {
85
+ const importMatches = [...content.matchAll(/^import\s.+$/gm)]
86
+ if (importMatches.length === 0) return `${line}${content}`
87
+ const last = importMatches[importMatches.length - 1]
88
+ const insertAt = last.index + last[0].length
89
+ return `${content.slice(0, insertAt)}\n${line}${content.slice(insertAt)}`
90
+ }
91
+
92
+ async function findRenderBlocksFiles(directory) {
93
+ const files = []
94
+ await walk(directory, files)
95
+ return files.filter((file) => /[\\/]src[\\/]blocks[\\/].*renderblocks\.(ts|tsx)$/i.test(file))
96
+ }
97
+
98
+ async function walk(directory, output) {
99
+ let entries
100
+ try {
101
+ entries = await readdir(directory, { withFileTypes: true })
102
+ } catch {
103
+ return
104
+ }
105
+ for (const entry of entries) {
106
+ const path = join(directory, entry.name)
107
+ if (entry.isDirectory()) await walk(path, output)
108
+ else output.push(path)
109
+ }
110
+ }
@@ -0,0 +1,3 @@
1
+ export async function runGenerators() {
2
+ throw new Error('Payload generator execution is not implemented.')
3
+ }
@@ -0,0 +1,117 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises'
2
+ import { dirname, join } from 'node:path'
3
+
4
+ export async function writeInstalledBlockReadme({
5
+ manifest,
6
+ targetDirectory,
7
+ copiedFiles,
8
+ sharedResults,
9
+ dependencyChecks,
10
+ collectionResults,
11
+ renderBlocksResults,
12
+ packageManager,
13
+ installCommand,
14
+ }) {
15
+ const readmePath = join(targetDirectory, 'src', 'blocks', manifest.name, 'README.md')
16
+ await mkdir(dirname(readmePath), { recursive: true })
17
+ await writeFile(readmePath, renderReadme({
18
+ manifest,
19
+ copiedFiles,
20
+ sharedResults,
21
+ dependencyChecks,
22
+ collectionResults,
23
+ renderBlocksResults,
24
+ packageManager,
25
+ installCommand,
26
+ }))
27
+ return readmePath
28
+ }
29
+
30
+ function renderReadme({
31
+ manifest,
32
+ copiedFiles,
33
+ sharedResults,
34
+ dependencyChecks,
35
+ collectionResults,
36
+ renderBlocksResults,
37
+ packageManager,
38
+ installCommand,
39
+ }) {
40
+ const generatorPrefix = packageManager === 'npm' ? 'npx payload' : `${packageManager} payload`
41
+ const dependencyCommand = installCommand ?? 'No missing dependencies detected.'
42
+
43
+ return `# ${manifest.name}
44
+
45
+ Payload slug: \`${manifest.slug}\`
46
+
47
+ ## Files installed
48
+
49
+ ${list(copiedFiles)}
50
+
51
+ ## Shared files installed or required
52
+
53
+ ${list(sharedResults.map((result) => `${result.status}: ${result.to}${result.reason ? ` - ${result.reason}` : ''}`))}
54
+
55
+ ## Dependencies required
56
+
57
+ ${list(dependencyChecks.declared.map(formatDependency))}
58
+
59
+ ## Dependencies missing
60
+
61
+ ${dependencyChecks.missing.length > 0 ? list(dependencyChecks.missing.map(formatDependency)) : '- None'}
62
+
63
+ Install missing dependencies manually:
64
+
65
+ \`\`\`bash
66
+ ${dependencyCommand}
67
+ \`\`\`
68
+
69
+ ## Collection registration status
70
+
71
+ ${list(collectionResults.map(formatPatchResult))}
72
+
73
+ ## RenderBlocks registration status
74
+
75
+ ${list(renderBlocksResults.map(formatPatchResult))}
76
+
77
+ ## Manual steps remaining
78
+
79
+ ${list(manualSteps(manifest))}
80
+
81
+ ## Payload commands to run manually
82
+
83
+ \`\`\`bash
84
+ ${generatorPrefix} generate:types
85
+ ${generatorPrefix} generate:importmap
86
+ \`\`\`
87
+
88
+ ## Copy-paste Codex prompt for finishing integration
89
+
90
+ \`\`\`text
91
+ Review the installed ${manifest.name} block integration.
92
+ Only fix issues directly related to ${manifest.name}.
93
+ Check imports, Pages collection, Posts collection, RenderBlocks, shared utilities, shared components, and missing dependencies.
94
+ Run Payload generators manually if schema or admin imports changed.
95
+ Do not rewrite unrelated application logic.
96
+ Do not change environment files.
97
+ Do not rewrite routes unless explicitly required by the block README.
98
+ \`\`\`
99
+ `
100
+ }
101
+
102
+ function formatDependency(dependency) {
103
+ return `${dependency.name}${dependency.version ? `@${dependency.version}` : ''}`
104
+ }
105
+
106
+ function formatPatchResult(result) {
107
+ return `${result.status}: ${result.file ?? result.reason}${result.backupPath ? ` (backup: ${result.backupPath})` : ''}`
108
+ }
109
+
110
+ function manualSteps(manifest) {
111
+ return manifest.manual ?? manifest.manualSteps ?? []
112
+ }
113
+
114
+ function list(values) {
115
+ if (!values || values.length === 0) return '- None'
116
+ return values.map((value) => `- ${value}`).join('\n')
117
+ }
@@ -0,0 +1,17 @@
1
+ import { readdir } from 'node:fs/promises'
2
+ import { loadTemplateManifest } from './loadTemplateManifest.js'
3
+ import { validateTemplateManifest } from './validateTemplateManifest.js'
4
+ import { blocksDirectory } from '../utils/paths.js'
5
+
6
+ export async function listTemplates() {
7
+ const entries = await readdir(blocksDirectory, { withFileTypes: true })
8
+ const templates = []
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)
14
+ }
15
+
16
+ return templates.sort((a, b) => a.name.localeCompare(b.name))
17
+ }
@@ -0,0 +1,20 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import { join } from 'node:path'
3
+ import { blocksDirectory } from '../utils/paths.js'
4
+ import { isSafeTemplateName } from '../utils/strings.js'
5
+
6
+ export async function loadTemplateManifest(templateName) {
7
+ if (!isSafeTemplateName(templateName)) throw new Error(`Invalid template name: ${templateName}`)
8
+
9
+ const templateDirectory = join(blocksDirectory, templateName)
10
+ const manifestPath = join(templateDirectory, 'manifest.json')
11
+
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
19
+ }
20
+ }
@@ -0,0 +1,69 @@
1
+ const REQUIRED_STRINGS = ['name', 'type', 'slug', 'version']
2
+ const OPTIONAL_ARRAYS = [
3
+ 'files',
4
+ 'requiredProjectFiles',
5
+ 'optionalProjectFiles',
6
+ 'packageDependencies',
7
+ 'dependencies',
8
+ 'sharedFiles',
9
+ 'manualSteps',
10
+ 'manual',
11
+ 'notes',
12
+ ]
13
+
14
+ export function validateTemplateManifest(manifest) {
15
+ const errors = []
16
+ if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
17
+ throw new Error('Block manifest must be a JSON object.')
18
+ }
19
+
20
+ for (const key of REQUIRED_STRINGS) {
21
+ if (typeof manifest[key] !== 'string' || manifest[key].trim() === '') errors.push(`${key} must be a non-empty string`)
22
+ }
23
+ if (manifest.type !== 'block') errors.push('type must be "block"')
24
+ if (manifest.label !== undefined && typeof manifest.label !== 'string') errors.push('label must be a string when present')
25
+ if (manifest.sourceName !== undefined && typeof manifest.sourceName !== 'string') errors.push('sourceName must be a string when present')
26
+ if (manifest.portableName !== undefined && typeof manifest.portableName !== 'string') errors.push('portableName must be a string when present')
27
+ if (manifest.exportName !== undefined && typeof manifest.exportName !== 'string') errors.push('exportName must be a string when present')
28
+ if (manifest.interfaceName !== undefined && typeof manifest.interfaceName !== 'string') errors.push('interfaceName must be a string when present')
29
+
30
+ for (const key of OPTIONAL_ARRAYS) {
31
+ if (manifest[key] !== undefined && !Array.isArray(manifest[key])) errors.push(`${key} must be an array when present`)
32
+ }
33
+ if (!Array.isArray(manifest.files)) errors.push('files must be an array')
34
+ if (manifest.source !== undefined && (!manifest.source || typeof manifest.source !== 'object' || Array.isArray(manifest.source))) errors.push('source must be an object when present')
35
+ 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
+ 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
+
40
+ if (Array.isArray(manifest.files)) {
41
+ manifest.files.forEach((file, index) => {
42
+ if (!file || typeof file.from !== 'string' || typeof file.to !== 'string') {
43
+ errors.push(`files[${index}] must contain string from and to values`)
44
+ }
45
+ })
46
+ }
47
+
48
+ for (const key of ['requiredProjectFiles', 'optionalProjectFiles']) {
49
+ if (!Array.isArray(manifest[key])) continue
50
+ manifest[key].forEach((requirement, index) => {
51
+ if (!requirement || typeof requirement.path !== 'string' || typeof requirement.reason !== 'string') {
52
+ errors.push(`${key}[${index}] must contain string path and reason values`)
53
+ }
54
+ })
55
+ }
56
+
57
+ if (Array.isArray(manifest.sharedFiles)) {
58
+ manifest.sharedFiles.forEach((file, index) => {
59
+ if (!file || typeof file.from !== 'string' || typeof file.to !== 'string') {
60
+ errors.push(`sharedFiles[${index}] must contain string from and to values`)
61
+ }
62
+ if (file.required !== undefined && typeof file.required !== 'boolean') errors.push(`sharedFiles[${index}].required must be a boolean when present`)
63
+ if (file.safeToCopy !== undefined && typeof file.safeToCopy !== 'boolean') errors.push(`sharedFiles[${index}].safeToCopy must be a boolean when present`)
64
+ })
65
+ }
66
+
67
+ if (errors.length > 0) throw new Error(`Invalid block manifest:\n- ${errors.join('\n- ')}`)
68
+ return manifest
69
+ }
@@ -0,0 +1,8 @@
1
+ export const info = (message) => console.log(message)
2
+ export const error = (message) => console.error(message)
3
+ export const heading = (message) => console.log(`\n${message}`)
4
+ export const item = (message) => console.log(`- ${message}`)
5
+ export const status = (ok, label, detail) => {
6
+ const suffix = detail ? ` — ${detail}` : ''
7
+ console.log(`${ok ? '[ok]' : '[missing]'} ${label}${suffix}`)
8
+ }
@@ -0,0 +1,42 @@
1
+ import { access } from 'node:fs/promises'
2
+ import { spawnSync } from 'node:child_process'
3
+ import { join } from 'node:path'
4
+
5
+ const LOCKFILES = [
6
+ ['pnpm', 'pnpm-lock.yaml'],
7
+ ['npm', 'package-lock.json'],
8
+ ['yarn', 'yarn.lock'],
9
+ ['bun', 'bun.lockb'],
10
+ ['bun', 'bun.lock'],
11
+ ]
12
+
13
+ export async function detectPackageManager(targetDirectory) {
14
+ for (const [manager, lockfile] of LOCKFILES) {
15
+ if (await exists(join(targetDirectory, lockfile))) return manager
16
+ }
17
+ if (commandExists('pnpm')) return 'pnpm'
18
+ return 'npm'
19
+ }
20
+
21
+ export function installCommand(packageManager, dependencies) {
22
+ if (packageManager === 'yarn') return ['yarn', 'add', ...dependencies]
23
+ if (packageManager === 'bun') return ['bun', 'add', ...dependencies]
24
+ if (packageManager === 'npm') return ['npm', 'install', ...dependencies]
25
+ return ['pnpm', 'add', ...dependencies]
26
+ }
27
+
28
+ function commandExists(command) {
29
+ const check = process.platform === 'win32' ? 'where' : 'command'
30
+ const args = process.platform === 'win32' ? [command] : ['-v', command]
31
+ const result = spawnSync(check, args, { stdio: 'ignore' })
32
+ return result.status === 0
33
+ }
34
+
35
+ async function exists(path) {
36
+ try {
37
+ await access(path)
38
+ return true
39
+ } catch {
40
+ return false
41
+ }
42
+ }
@@ -0,0 +1,7 @@
1
+ import { dirname, resolve } from 'node:path'
2
+ import { fileURLToPath } from 'node:url'
3
+
4
+ const currentDirectory = dirname(fileURLToPath(import.meta.url))
5
+ export const packageRoot = resolve(currentDirectory, '..', '..')
6
+ export const templatesDirectory = resolve(packageRoot, 'templates')
7
+ export const blocksDirectory = resolve(templatesDirectory, 'blocks')