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
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Designs Dusted
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,68 @@
1
+ # blocks-dusted
2
+
3
+ `blocks-dusted` installs reusable Payload CMS blocks into an existing Payload project.
4
+
5
+ The GitHub source repository is private. That does not prevent installation from npm after the package is published, including when the published npm package is public.
6
+
7
+ ## Main Workflow
8
+
9
+ Run commands from the root of your Payload project:
10
+
11
+ ```bash
12
+ pnpm dlx blocks-dusted list
13
+ pnpm dlx blocks-dusted doctor
14
+ pnpm dlx blocks-dusted add DD-Hero --dry-run
15
+ pnpm dlx blocks-dusted add DD-Hero
16
+ ```
17
+
18
+ Use `blocks-dusted list` first to see the exact available block names in the package. `DD-Hero` is an existing block in the current manifest registry.
19
+
20
+ The preferred install command after publication is:
21
+
22
+ ```bash
23
+ pnpm dlx blocks-dusted add <BlockName>
24
+ ```
25
+
26
+ ## Commands
27
+
28
+ ```bash
29
+ blocks-dusted --help
30
+ blocks-dusted --version
31
+ blocks-dusted list
32
+ blocks-dusted doctor
33
+ blocks-dusted doctor --cwd C:\Projects\my-payload-site
34
+ blocks-dusted add DD-Hero --dry-run
35
+ blocks-dusted add DD-Hero --no-register
36
+ blocks-dusted add DD-Hero --cwd C:\Projects\my-payload-site
37
+ ```
38
+
39
+ `--cwd` targets a Payload project directory without changing your shell directory. Relative `--cwd` values resolve from the directory where you launched the command.
40
+
41
+ ## Safety Behavior
42
+
43
+ `--dry-run` performs no writes.
44
+
45
+ `--no-register` copies block files and safe shared files but skips automatic collection and `RenderBlocks` registration.
46
+
47
+ Existing block destinations are protected from overwrite. Existing shared files are never overwritten.
48
+
49
+ Before supported source patches, the CLI creates backups such as `.bak`, `.bak.1`, and later numbered backups.
50
+
51
+ Dependencies are reported but not installed. Install the reported packages manually after reviewing the output.
52
+
53
+ Payload generators are never run automatically. Run these manually after reviewing changes:
54
+
55
+ ```bash
56
+ pnpm payload generate:types
57
+ pnpm payload generate:importmap
58
+ ```
59
+
60
+ The CLI does not run migrations or build the target project.
61
+
62
+ ## Publishing Identities
63
+
64
+ - GitHub repository: `https://github.com/Hadizainal/blocks-dusted.git`
65
+ - npm package: `blocks-dusted`
66
+ - CLI executable: `blocks-dusted`
67
+
68
+ The repository can remain private while the npm package is published separately.
package/bin/index.js ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runCli } from '../src/cli.js'
4
+
5
+ runCli(process.argv.slice(2)).catch((error) => {
6
+ console.error(`Error: ${error.message}`)
7
+ process.exitCode = 1
8
+ })
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "blocks-dusted",
3
+ "version": "0.1.0",
4
+ "description": "Manifest-driven CLI for installing reusable Payload CMS blocks.",
5
+ "type": "module",
6
+ "bin": {
7
+ "blocks-dusted": "./bin/index.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "src",
12
+ "templates",
13
+ "README.md",
14
+ "LICENSE"
15
+ ],
16
+ "scripts": {
17
+ "start": "node ./bin/index.js",
18
+ "dev": "node ./bin/index.js",
19
+ "test": "node --test test/cli.test.js",
20
+ "validate:package": "node ./scripts/validate-package.mjs",
21
+ "test:pack": "node --test test/pack.test.js",
22
+ "prepack": "npm run validate:package",
23
+ "test:add": "node ./bin/index.js add DD-Hero --dry-run",
24
+ "test:list": "node ./bin/index.js list",
25
+ "test:doctor": "node ./bin/index.js doctor",
26
+ "convert:studio": "node ./scripts/convertStudioBlock.mjs"
27
+ },
28
+ "engines": {
29
+ "node": "\u003e=20"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/Hadizainal/blocks-dusted.git"
34
+ },
35
+ "bugs": {
36
+ "url": "https://github.com/Hadizainal/blocks-dusted/issues"
37
+ },
38
+ "homepage": "https://github.com/Hadizainal/blocks-dusted#readme",
39
+ "license": "MIT"
40
+ }
package/src/cli.js ADDED
@@ -0,0 +1,79 @@
1
+ import { readFile } from 'node:fs/promises'
2
+ import { resolve } from 'node:path'
3
+ import { addTemplate } from './commands/add.js'
4
+ import { doctor } from './commands/doctor.js'
5
+ import { list } from './commands/list.js'
6
+ import { error, info } from './utils/logger.js'
7
+ import { packageRoot } from './utils/paths.js'
8
+
9
+ function printHelp() {
10
+ info('blocks-dusted - reusable Payload CMS block installer')
11
+ info('')
12
+ info('Usage:')
13
+ info(' blocks-dusted list')
14
+ info(' blocks-dusted doctor [--cwd <path>]')
15
+ info(' blocks-dusted add <BlockName> [--dry-run] [--no-register] [--cwd <path>]')
16
+ info(' blocks-dusted --version')
17
+ }
18
+
19
+ export async function runCli(args, options = {}) {
20
+ const originalDirectory = options.cwd ?? process.cwd()
21
+
22
+ if (args.length === 0 || args.includes('--help') || args.includes('-h')) {
23
+ printHelp()
24
+ return
25
+ }
26
+
27
+ if (args.includes('--version') || args.includes('-v')) {
28
+ info(await packageVersion())
29
+ return
30
+ }
31
+
32
+ const { commandArgs, targetDirectory } = parseGlobalOptions(args, originalDirectory)
33
+ const [command, ...rest] = commandArgs
34
+
35
+ if (command === 'list') return list()
36
+ if (command === 'doctor') return doctor({ targetDirectory })
37
+ if (command === 'add') {
38
+ const templateName = rest.find((argument) => !argument.startsWith('-'))
39
+ return addTemplate({
40
+ templateName,
41
+ targetDirectory,
42
+ dryRun: rest.includes('--dry-run'),
43
+ noRegister: rest.includes('--no-register'),
44
+ })
45
+ }
46
+
47
+ error(`Unknown command: ${command}`)
48
+ printHelp()
49
+ process.exitCode = 1
50
+ }
51
+
52
+ function parseGlobalOptions(args, originalDirectory) {
53
+ const commandArgs = []
54
+ let targetDirectory = originalDirectory
55
+
56
+ for (let index = 0; index < args.length; index += 1) {
57
+ const argument = args[index]
58
+ if (argument === '--cwd') {
59
+ const value = args[index + 1]
60
+ if (!value) throw new Error('--cwd requires a directory path')
61
+ targetDirectory = resolve(originalDirectory, value)
62
+ index += 1
63
+ continue
64
+ }
65
+ if (argument.startsWith('--cwd=')) {
66
+ targetDirectory = resolve(originalDirectory, argument.slice('--cwd='.length))
67
+ continue
68
+ }
69
+ commandArgs.push(argument)
70
+ }
71
+
72
+ return { commandArgs, targetDirectory }
73
+ }
74
+
75
+ async function packageVersion() {
76
+ const packageJson = JSON.parse(await readFile(resolve(packageRoot, 'package.json'), 'utf8'))
77
+ return packageJson.version
78
+ }
79
+
@@ -0,0 +1,327 @@
1
+ import { access } from 'node:fs/promises'
2
+ import { join } from 'node:path'
3
+ import { checkRequirements } from '../installers/checkRequirements.js'
4
+ import {
5
+ copySharedFiles,
6
+ copyTemplateFiles,
7
+ } from '../installers/copyTemplateFiles.js'
8
+ import { checkDependencies } from '../installers/installDependencies.js'
9
+ import { writeInstalledBlockReadme } from '../installers/writeInstalledBlockReadme.js'
10
+ import { patchPayloadBlocksArray } from '../installers/patchPayloadBlocksArray.js'
11
+ import { patchRenderBlocks } from '../installers/patchRenderBlocks.js'
12
+ import { loadTemplateManifest } from '../registry/loadTemplateManifest.js'
13
+ import { validateTemplateManifest } from '../registry/validateTemplateManifest.js'
14
+ import { heading, info, item, status } from '../utils/logger.js'
15
+ import { packageRoot } from '../utils/paths.js'
16
+ import { fileExists, validateTargetProject } from '../utils/project.js'
17
+
18
+ export async function addTemplate({
19
+ templateName,
20
+ targetDirectory = process.cwd(),
21
+ dryRun = false,
22
+ noRegister = false,
23
+ }) {
24
+ if (!templateName) {
25
+ throw new Error(
26
+ 'A block name is required. Example: blocks-dusted add DD-Hero --dry-run',
27
+ )
28
+ }
29
+
30
+ const { manifest, templateDirectory } = await loadTemplateManifest(templateName)
31
+ validateTemplateManifest(manifest)
32
+ await validateManifestSourceFiles({ manifest, templateDirectory, packageRoot })
33
+ const checks = await checkRequirements({ manifest, targetDirectory })
34
+ const dependencyChecks = await checkDependencies({ manifest, targetDirectory })
35
+
36
+ if (!dryRun) {
37
+ await validateTargetProject(targetDirectory)
38
+ const missingRequired = checks.missingRequirements ?? []
39
+ if (missingRequired.length > 0) {
40
+ throw new Error(
41
+ `Required project files are missing:\n${missingRequired.map((check) => `- ${check.path}: ${check.reason}`).join('\n')}`,
42
+ )
43
+ }
44
+ }
45
+
46
+ heading('Block')
47
+ info(`Name: ${manifest.name}`)
48
+ if (manifest.sourceName) info(`Source: ${manifest.sourceName}`)
49
+ if (manifest.portableName) info(`Portable name: ${manifest.portableName}`)
50
+ info(`Slug: ${manifest.slug}`)
51
+ info(`Type: ${manifest.type}`)
52
+ info(`Version: ${manifest.version}`)
53
+ info(`Target directory: ${targetDirectory}`)
54
+ info(`Register: ${noRegister ? 'no' : 'yes'}`)
55
+ info(`Source template path: ${templateDirectory}`)
56
+ info(`Target install path: ${checks.collisions.destinationPath}`)
57
+
58
+ heading(dryRun ? 'Files that would be copied' : 'Files to copy')
59
+ for (const file of manifest.files) item(`${file.from} -> ${file.to}`)
60
+
61
+ heading('Required project files')
62
+ if (checks.requiredFiles.length === 0) item('None')
63
+ for (const check of checks.requiredFiles) {
64
+ status(check.exists, check.path, check.reason)
65
+ }
66
+
67
+ heading('Optional project files')
68
+ if (checks.optionalFiles.length === 0) item('None')
69
+ for (const check of checks.optionalFiles) {
70
+ status(check.exists, check.path, check.reason)
71
+ }
72
+
73
+ heading('Package dependencies')
74
+ if (dependencyChecks.declared.length === 0) item('None')
75
+ for (const dependency of dependencyChecks.declared) {
76
+ const isMissing = dependencyChecks.missing.some(
77
+ (missing) => missing.name === dependency.name,
78
+ )
79
+ status(
80
+ !isMissing,
81
+ formatDependency(dependency),
82
+ isMissing ? 'missing' : 'already listed',
83
+ )
84
+ }
85
+ if (dependencyChecks.missing.length > 0) {
86
+ item(
87
+ `Install command: ${installCommandText(dependencyChecks.packageManager, dependencyChecks.missing)}`,
88
+ )
89
+ }
90
+
91
+ heading('Shared files')
92
+ const sharedFiles = manifest.sharedFiles ?? []
93
+ if (sharedFiles.length === 0) item('None')
94
+ for (const file of sharedFiles) {
95
+ item(
96
+ `${file.from} -> ${file.to}${file.safeToCopy === false ? ' (manual)' : ''}`,
97
+ )
98
+ }
99
+
100
+ heading('Collision checks')
101
+ 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}`,
120
+ )
121
+
122
+ if (checks.collisions.destinationExists) {
123
+ throw new Error(
124
+ `Refusing to install because ${checks.collisions.destinationPath} already exists.`,
125
+ )
126
+ }
127
+
128
+ heading('Manual steps')
129
+ manualSteps(manifest).forEach((step, index) => item(`${index + 1}. ${step}`))
130
+ heading('Generator commands')
131
+ item(
132
+ `${dependencyChecks.packageManager === 'npm' ? 'npx payload' : `${dependencyChecks.packageManager} payload`} generate:types`,
133
+ )
134
+ item(
135
+ `${dependencyChecks.packageManager === 'npm' ? 'npx payload' : `${dependencyChecks.packageManager} payload`} generate:importmap`,
136
+ )
137
+
138
+ heading('Patch targets')
139
+ for (const [name, target] of Object.entries(manifest.patchTargets ?? {})) {
140
+ item(`${name}: ${target.description}`)
141
+ }
142
+ const routeChecks = await checkPageRouteIntegration(manifest, targetDirectory)
143
+ if (
144
+ manifest.pageRouteIntegration?.description ||
145
+ manifest.pageRouteIntegration?.manualSteps?.length > 0 ||
146
+ routeChecks.length > 0
147
+ ) {
148
+ heading('Page route integration')
149
+ if (manifest.pageRouteIntegration.description)
150
+ item(manifest.pageRouteIntegration.description)
151
+ for (const check of routeChecks)
152
+ status(check.exists, check.path, check.reason)
153
+ for (const step of manifest.pageRouteIntegration.manualSteps ?? [])
154
+ item(step)
155
+ }
156
+
157
+ info('')
158
+ info(`Manifest loaded from: ${templateDirectory}`)
159
+
160
+ if (dryRun) {
161
+ const dryRunSharedResults = await copySharedFiles({
162
+ manifest,
163
+ packageRoot,
164
+ targetDirectory,
165
+ dryRun: true,
166
+ })
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
+ })
181
+
182
+ heading('Dry-run shared file results')
183
+ if (dryRunSharedResults.length === 0) item('None')
184
+ dryRunSharedResults.forEach((result) =>
185
+ item(
186
+ `${result.status}: ${result.to}${result.reason ? ` - ${result.reason}` : ''}`,
187
+ ),
188
+ )
189
+ heading('Dry-run collection registration')
190
+ dryRunCollectionResults.forEach((result) =>
191
+ item(`${result.status}: ${result.file ?? result.reason}`),
192
+ )
193
+ heading('Dry-run RenderBlocks registration')
194
+ dryRunRenderBlocksResults.forEach((result) =>
195
+ item(`${result.status}: ${result.file ?? result.reason}`),
196
+ )
197
+ info('Dry run complete: no files were changed.')
198
+ return
199
+ }
200
+
201
+ const sharedResults = await copySharedFiles({
202
+ manifest,
203
+ packageRoot,
204
+ targetDirectory,
205
+ })
206
+
207
+ const copiedFiles = await copyTemplateFiles({
208
+ manifest,
209
+ templateDirectory,
210
+ targetDirectory,
211
+ })
212
+
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 })
219
+ const readmePath = await writeInstalledBlockReadme({
220
+ manifest,
221
+ targetDirectory,
222
+ copiedFiles,
223
+ sharedResults,
224
+ dependencyChecks,
225
+ collectionResults,
226
+ renderBlocksResults,
227
+ packageManager: dependencyChecks.packageManager,
228
+ installCommand:
229
+ dependencyChecks.missing.length > 0
230
+ ? installCommandText(
231
+ dependencyChecks.packageManager,
232
+ dependencyChecks.missing,
233
+ )
234
+ : undefined,
235
+ })
236
+
237
+ heading('Copied files')
238
+ copiedFiles.forEach((file) => item(file))
239
+ heading('Shared file results')
240
+ if (sharedResults.length === 0) item('None')
241
+ sharedResults.forEach((result) =>
242
+ item(
243
+ `${result.status}: ${result.to}${result.reason ? ` - ${result.reason}` : ''}`,
244
+ ),
245
+ )
246
+ heading('Dependency install')
247
+ item('Skipped: dependency installation is manual')
248
+ if (dependencyChecks.missing.length > 0)
249
+ item(
250
+ `Run manually: ${installCommandText(dependencyChecks.packageManager, dependencyChecks.missing)}`,
251
+ )
252
+ heading('Collection registration')
253
+ collectionResults.forEach((result) =>
254
+ item(
255
+ `${result.status}: ${result.file ?? result.reason}${result.backupPath ? ` (backup: ${result.backupPath})` : ''}`,
256
+ ),
257
+ )
258
+ heading('RenderBlocks registration')
259
+ renderBlocksResults.forEach((result) =>
260
+ item(
261
+ `${result.status}: ${result.file ?? result.reason}${result.backupPath ? ` (backup: ${result.backupPath})` : ''}`,
262
+ ),
263
+ )
264
+ heading('Installed README')
265
+ item(readmePath)
266
+ info('')
267
+ info('Install complete.')
268
+ info(
269
+ 'Payload generators were not run. Run them manually after reviewing changes.',
270
+ )
271
+ }
272
+
273
+ function formatDependency(dependency) {
274
+ return typeof dependency === 'string'
275
+ ? dependency
276
+ : `${dependency.name}${dependency.version ? `@${dependency.version}` : ''}`
277
+ }
278
+
279
+ function installCommandText(packageManager, dependencies) {
280
+ const command =
281
+ packageManager === 'yarn'
282
+ ? ['yarn', 'add']
283
+ : packageManager === 'bun'
284
+ ? ['bun', 'add']
285
+ : packageManager === 'npm'
286
+ ? ['npm', 'install']
287
+ : ['pnpm', 'add']
288
+ return [...command, ...dependencies.map(formatDependency)].join(' ')
289
+ }
290
+
291
+ function manualSteps(manifest) {
292
+ return manifest.manual ?? manifest.manualSteps ?? []
293
+ }
294
+
295
+ function collisionStatus(ok, label) {
296
+ console.log(`${ok ? '[ok]' : '[collision]'} ${label}`)
297
+ }
298
+
299
+ async function checkPageRouteIntegration(manifest, targetDirectory) {
300
+ const files = manifest.pageRouteIntegration?.requiredFiles ?? []
301
+ return Promise.all(
302
+ files.map(async (file) => ({
303
+ ...file,
304
+ exists: await fileExists(join(targetDirectory, file.path)),
305
+ })),
306
+ )
307
+ }
308
+
309
+ async function validateManifestSourceFiles({ manifest, templateDirectory, packageRoot }) {
310
+ const checks = [
311
+ ...manifest.files.map((file) => join(templateDirectory, file.from)),
312
+ ...(manifest.sharedFiles ?? []).map((file) => join(packageRoot, file.from)),
313
+ ]
314
+
315
+ const missing = []
316
+ for (const file of checks) {
317
+ try {
318
+ await access(file)
319
+ } catch {
320
+ missing.push(file)
321
+ }
322
+ }
323
+
324
+ if (missing.length > 0) {
325
+ throw new Error(`Manifest source files are missing:\n${missing.map((file) => `- ${file}`).join('\n')}`)
326
+ }
327
+ }
@@ -0,0 +1,21 @@
1
+ import { inspectProject } from '../utils/project.js'
2
+ import { heading, info, status } from '../utils/logger.js'
3
+
4
+ export async function doctor({ targetDirectory } = {}) {
5
+ const project = await inspectProject(targetDirectory ?? process.cwd())
6
+ heading('Payload project doctor')
7
+ info(`Directory: ${project.targetDirectory}`)
8
+ status(project.hasPackageJson, 'package.json')
9
+ status(project.hasPayloadDependency, 'payload dependency')
10
+ status(project.hasSupportedSourceStructure, 'supported Payload source structure')
11
+ status(project.hasBlocksDirectory, 'src/blocks')
12
+ status(Boolean(project.payloadConfigPath), 'Payload config', project.payloadConfigPath || 'No likely config found')
13
+ status(project.isWritable, 'target directory writable')
14
+ status(!project.isCliRepository, 'not blocks-dusted source repository')
15
+ info(`Package manager: ${project.packageManager || 'not detected'}`)
16
+ info(`Appears to be a Payload project: ${project.isPayloadProject && !project.isCliRepository ? 'yes' : 'no'}`)
17
+
18
+ if (!project.isPayloadProject || project.isCliRepository || !project.isWritable) {
19
+ process.exitCode = 1
20
+ }
21
+ }
@@ -0,0 +1,18 @@
1
+ import { listTemplates } from '../registry/listTemplates.js'
2
+ import { heading, info, item } from '../utils/logger.js'
3
+
4
+ export async function list() {
5
+ const templates = await listTemplates()
6
+ heading('Available blocks')
7
+
8
+ if (templates.length === 0) {
9
+ item('None')
10
+ return
11
+ }
12
+
13
+ for (const template of templates) {
14
+ item(`${template.name} (${template.type}, ${template.slug}, v${template.version})`)
15
+ }
16
+
17
+ info(`Total: ${templates.length}`)
18
+ }
@@ -0,0 +1,101 @@
1
+ import { access, readdir, readFile } from 'node:fs/promises'
2
+ import { basename, dirname, join } from 'node:path'
3
+
4
+ const SEARCH_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs', '.json'])
5
+ const IGNORED_DIRECTORIES = new Set(['.git', '.next', 'dist', 'build', 'node_modules'])
6
+
7
+ export async function checkRequirements({ manifest, targetDirectory }) {
8
+ const requiredFiles = await checkFiles(manifest.requiredProjectFiles ?? [], targetDirectory)
9
+ const optionalFiles = await checkFiles(manifest.optionalProjectFiles ?? [], targetDirectory)
10
+ const destinationPath = commonDestination(manifest.files)
11
+ const searchableContent = await readSearchableProjectFiles(targetDirectory)
12
+ const exportName = manifest.exportName ?? manifest.name
13
+ const interfaceName = manifest.interfaceName ?? exportName
14
+
15
+ return {
16
+ requiredFiles,
17
+ optionalFiles,
18
+ missingRequirements: requiredFiles.filter((item) => !item.exists),
19
+ collisions: {
20
+ destinationPath,
21
+ destinationExists: await exists(join(targetDirectory, destinationPath)),
22
+ slugFound: contains(searchableContent, manifest.slug),
23
+ configExportFound: contains(searchableContent, `export const ${exportName}`),
24
+ interfaceNameFound: contains(searchableContent, `interfaceName: '${interfaceName}'`) || contains(searchableContent, `interfaceName: \"${interfaceName}\"`),
25
+ renderBlocksMappingFound: searchableContent.some(({ path, content }) => basename(path).toLowerCase().includes('renderblocks') && content.includes(manifest.slug)),
26
+ },
27
+ }
28
+ }
29
+
30
+ async function checkFiles(requirements, targetDirectory) {
31
+ return Promise.all(requirements.map(async (requirement) => ({
32
+ ...requirement,
33
+ exists: await exists(join(targetDirectory, requirement.path)),
34
+ })))
35
+ }
36
+
37
+ export async function destinationExists(targetDirectory, destinationPath) {
38
+ return exists(join(targetDirectory, destinationPath))
39
+ }
40
+
41
+ export async function projectContains(targetDirectory, text) {
42
+ return contains(await readSearchableProjectFiles(targetDirectory), text)
43
+ }
44
+
45
+ function contains(files, text) {
46
+ return files.some(({ content }) => content.includes(text))
47
+ }
48
+
49
+ function commonDestination(files) {
50
+ if (files.length === 0) return '.'
51
+ return dirname(files[0].to)
52
+ }
53
+
54
+ async function exists(path) {
55
+ try {
56
+ await access(path)
57
+ return true
58
+ } catch {
59
+ return false
60
+ }
61
+ }
62
+
63
+ async function readSearchableProjectFiles(targetDirectory) {
64
+ const files = []
65
+ await walk(join(targetDirectory, 'src'), files)
66
+
67
+ for (const fileName of ['payload.config.ts', 'payload.config.js']) {
68
+ const path = join(targetDirectory, fileName)
69
+ try {
70
+ files.push({ path, content: await readFile(path, 'utf8') })
71
+ } catch {
72
+ // Root-level Payload config is optional.
73
+ }
74
+ }
75
+
76
+ return files
77
+ }
78
+
79
+ async function walk(directory, output) {
80
+ let entries
81
+ try {
82
+ entries = await readdir(directory, { withFileTypes: true })
83
+ } catch {
84
+ return
85
+ }
86
+
87
+ for (const entry of entries) {
88
+ const path = join(directory, entry.name)
89
+ if (entry.isDirectory()) {
90
+ if (!IGNORED_DIRECTORIES.has(entry.name)) await walk(path, output)
91
+ continue
92
+ }
93
+ const extension = entry.name.slice(entry.name.lastIndexOf('.'))
94
+ if (!SEARCH_EXTENSIONS.has(extension)) continue
95
+ try {
96
+ output.push({ path, content: await readFile(path, 'utf8') })
97
+ } catch {
98
+ // Ignore unreadable files during diagnostics.
99
+ }
100
+ }
101
+ }