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.
- package/LICENSE +21 -0
- package/README.md +68 -0
- package/bin/index.js +8 -0
- package/package.json +40 -0
- package/src/cli.js +79 -0
- package/src/commands/add.js +327 -0
- package/src/commands/doctor.js +21 -0
- package/src/commands/list.js +18 -0
- package/src/installers/checkRequirements.js +101 -0
- package/src/installers/copyTemplateFiles.js +79 -0
- package/src/installers/installDependencies.js +69 -0
- package/src/installers/patchPayloadBlocksArray.js +122 -0
- package/src/installers/patchRenderBlocks.js +110 -0
- package/src/installers/runGenerators.js +3 -0
- package/src/installers/writeInstalledBlockReadme.js +117 -0
- package/src/registry/listTemplates.js +17 -0
- package/src/registry/loadTemplateManifest.js +20 -0
- package/src/registry/validateTemplateManifest.js +69 -0
- package/src/utils/logger.js +8 -0
- package/src/utils/packageManager.js +42 -0
- package/src/utils/paths.js +7 -0
- package/src/utils/project.js +122 -0
- package/src/utils/strings.js +7 -0
- package/templates/blocks/DD-BookCall/Component.tsx +573 -0
- package/templates/blocks/DD-BookCall/config.ts +239 -0
- package/templates/blocks/DD-BookCall/manifest.json +112 -0
- package/templates/blocks/DD-CardTemplate01/Component.tsx +144 -0
- package/templates/blocks/DD-CardTemplate01/config.ts +106 -0
- package/templates/blocks/DD-CardTemplate01/manifest.json +84 -0
- package/templates/blocks/DD-Carousel-A/Component.tsx +266 -0
- package/templates/blocks/DD-Carousel-A/config.ts +163 -0
- package/templates/blocks/DD-Carousel-A/manifest.json +86 -0
- package/templates/blocks/DD-Carousel-B/Component.tsx +432 -0
- package/templates/blocks/DD-Carousel-B/config.ts +176 -0
- package/templates/blocks/DD-Carousel-B/manifest.json +86 -0
- package/templates/blocks/DD-ComparisonTable/Component.tsx +272 -0
- package/templates/blocks/DD-ComparisonTable/config.ts +105 -0
- package/templates/blocks/DD-ComparisonTable/manifest.json +78 -0
- package/templates/blocks/DD-Contact/Component.tsx +439 -0
- package/templates/blocks/DD-Contact/config.ts +217 -0
- package/templates/blocks/DD-Contact/manifest.json +107 -0
- package/templates/blocks/DD-FeatCat/Component.tsx +361 -0
- package/templates/blocks/DD-FeatCat/config.ts +204 -0
- package/templates/blocks/DD-FeatCat/manifest.json +87 -0
- package/templates/blocks/DD-FeatStrip/Component.tsx +615 -0
- package/templates/blocks/DD-FeatStrip/config.ts +134 -0
- package/templates/blocks/DD-FeatStrip/manifest.json +82 -0
- package/templates/blocks/DD-Hero/Component.tsx +317 -0
- package/templates/blocks/DD-Hero/config.ts +267 -0
- package/templates/blocks/DD-Hero/manifest.json +92 -0
- package/templates/blocks/DD-HorizontalScroll/Component.tsx +995 -0
- package/templates/blocks/DD-HorizontalScroll/config.ts +231 -0
- package/templates/blocks/DD-HorizontalScroll/manifest.json +85 -0
- package/templates/blocks/DD-MasonaryMedia/Component.tsx +571 -0
- package/templates/blocks/DD-MasonaryMedia/config.ts +390 -0
- package/templates/blocks/DD-MasonaryMedia/manifest.json +101 -0
- package/templates/blocks/DD-MasonaryMedia/vendor.d.ts +2 -0
- package/templates/blocks/DD-Pricing/Component.tsx +380 -0
- package/templates/blocks/DD-Pricing/config.ts +215 -0
- package/templates/blocks/DD-Pricing/manifest.json +78 -0
- package/templates/blocks/DD-Process/Component.tsx +155 -0
- package/templates/blocks/DD-Process/config.ts +150 -0
- package/templates/blocks/DD-Process/manifest.json +82 -0
- package/templates/blocks/DD-Section/Component.tsx +327 -0
- package/templates/blocks/DD-Section/config.ts +214 -0
- package/templates/blocks/DD-Section/manifest.json +85 -0
- package/templates/blocks/DD-Services/Component.tsx +188 -0
- package/templates/blocks/DD-Services/config.ts +149 -0
- package/templates/blocks/DD-Services/manifest.json +95 -0
- package/templates/blocks/DD-ShowcaseGrid/Component.tsx +317 -0
- package/templates/blocks/DD-ShowcaseGrid/config.ts +197 -0
- package/templates/blocks/DD-ShowcaseGrid/manifest.json +80 -0
- package/templates/blocks/DD-Slider-A/Component.tsx +252 -0
- package/templates/blocks/DD-Slider-A/config.ts +118 -0
- package/templates/blocks/DD-Slider-A/manifest.json +86 -0
- package/templates/blocks/DD-StackCards/Component.tsx +496 -0
- package/templates/blocks/DD-StackCards/config.ts +399 -0
- package/templates/blocks/DD-StackCards/manifest.json +87 -0
- package/templates/blocks/DD-Team/Component.tsx +265 -0
- package/templates/blocks/DD-Team/config.ts +173 -0
- package/templates/blocks/DD-Team/manifest.json +82 -0
- package/templates/blocks/DD-TechStack/Component.tsx +189 -0
- package/templates/blocks/DD-TechStack/config.ts +153 -0
- package/templates/blocks/DD-TechStack/manifest.json +118 -0
- package/templates/blocks/DD-Testimonials/Component.tsx +147 -0
- package/templates/blocks/DD-Testimonials/config.ts +120 -0
- package/templates/blocks/DD-Testimonials/manifest.json +87 -0
- package/templates/blocks/DD-Work/Component.tsx +328 -0
- package/templates/blocks/DD-Work/config.ts +165 -0
- package/templates/blocks/DD-Work/manifest.json +87 -0
- package/templates/blocks/DD-Work-B/Component.tsx +320 -0
- package/templates/blocks/DD-Work-B/config.ts +165 -0
- package/templates/blocks/DD-Work-B/manifest.json +75 -0
- package/templates/shared/components/ui/anims/fade.tsx +83 -0
- package/templates/shared/components/ui/button.tsx +233 -0
- package/templates/shared/components/ui/input.tsx +41 -0
- package/templates/shared/fields/lucide-icon-picker/Cell.tsx +24 -0
- package/templates/shared/fields/lucide-icon-picker/README.md +160 -0
- package/templates/shared/fields/lucide-icon-picker/field.ts +40 -0
- package/templates/shared/fields/lucide-icon-picker/index.tsx +203 -0
- package/templates/shared/utilities/canUseDOM.ts +1 -0
- 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
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
|
+
}
|