blocks-dusted 0.1.6 → 0.1.8

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/README.md CHANGED
@@ -356,6 +356,8 @@ pnpm dlx blocks-dusted doctor
356
356
  pnpm dlx blocks-dusted add DD-Hero --dry-run
357
357
  pnpm dlx blocks-dusted add DD-Hero
358
358
  pnpm dlx blocks-dusted add RichText
359
+ pnpm dlx blocks-dusted header add HeaderDD02 --dry-run
360
+ pnpm dlx blocks-dusted header add HeaderDD02
359
361
  ```
360
362
 
361
363
  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.
@@ -378,6 +380,8 @@ blocks-dusted add DD-Hero --dry-run
378
380
  blocks-dusted add DD-Hero --no-register
379
381
  blocks-dusted add DD-Hero --cwd C:\Projects\my-payload-site
380
382
  blocks-dusted add RichText --cwd C:\Projects\my-payload-site
383
+ blocks-dusted header add HeaderDD02 --dry-run
384
+ blocks-dusted header add HeaderDD02
381
385
  ```
382
386
 
383
387
  `--cwd` targets a Payload project directory without changing your shell directory. Relative `--cwd` values resolve from the directory where you launched the command.
@@ -403,6 +407,17 @@ pnpm payload generate:importmap
403
407
 
404
408
  The CLI does not run migrations or build the target project.
405
409
 
410
+ ### Header workflows
411
+
412
+ Headers are active Payload globals and layout wiring, not ordinary page blocks. Use the dedicated header workflow when you want to activate a reusable header implementation.
413
+
414
+ ```bash
415
+ pnpm dlx blocks-dusted header add HeaderDD02 --dry-run
416
+ pnpm dlx blocks-dusted header add HeaderDD02
417
+ ```
418
+
419
+ The HeaderDD02 workflow backs up the active Header config, Nav renderer, and RowLabel before patching them. It installs the HeaderDD02 variant files, copies missing shared UI primitives, patches `src/Header/Nav/index.tsx` to render HeaderDD02, and patches `src/Header/config.ts` to expose HeaderDD02 fields while retaining legacy starter `navItems` as hidden data. Run Payload type and import-map generation after reviewing the changes.
420
+
406
421
  ## Payload Block Standard
407
422
 
408
423
  ### Purpose
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "blocks-dusted",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Manifest-driven CLI for installing reusable Payload CMS blocks.",
5
5
  "type": "module",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -2,6 +2,7 @@ import { readFile } from 'node:fs/promises'
2
2
  import { resolve } from 'node:path'
3
3
  import { addTemplate } from './commands/add.js'
4
4
  import { doctor } from './commands/doctor.js'
5
+ import { addHeader } from './commands/header.js'
5
6
  import { list } from './commands/list.js'
6
7
  import { error, info } from './utils/logger.js'
7
8
  import { packageRoot } from './utils/paths.js'
@@ -12,7 +13,8 @@ function printHelp() {
12
13
  info('Usage:')
13
14
  info(' blocks-dusted list')
14
15
  info(' blocks-dusted doctor [--cwd <path>]')
15
- info(' blocks-dusted add <BlockName> [--dry-run] [--no-register] [--cwd <path>]')
16
+ info(' blocks-dusted add <TemplateName> [--dry-run] [--no-register] [--cwd <path>]')
17
+ info(' blocks-dusted header add <HeaderName> [--dry-run] [--cwd <path>]')
16
18
  info(' blocks-dusted --version')
17
19
  }
18
20
 
@@ -34,6 +36,17 @@ export async function runCli(args, options = {}) {
34
36
 
35
37
  if (command === 'list') return list()
36
38
  if (command === 'doctor') return doctor({ targetDirectory })
39
+ if (command === 'header') {
40
+ const [subcommand, ...headerRest] = rest
41
+ if (subcommand !== 'add') throw new Error('Usage: blocks-dusted header add <HeaderName> [--dry-run]')
42
+ const headerName = headerRest.find((argument) => !argument.startsWith('-'))
43
+ return addHeader({
44
+ headerName,
45
+ targetDirectory,
46
+ dryRun: headerRest.includes('--dry-run'),
47
+ })
48
+ }
49
+
37
50
  if (command === 'add') {
38
51
  const templateName = rest.find((argument) => !argument.startsWith('-'))
39
52
  return addTemplate({
@@ -0,0 +1,145 @@
1
+ import { writeFile } from 'node:fs/promises'
2
+ import { copySharedFiles, copyTemplateFiles, backupExistingDestination, plannedBackupPath } from '../installers/copyTemplateFiles.js'
3
+ import { checkDependencies } from '../installers/installDependencies.js'
4
+ import { checkRequirements } from '../installers/checkRequirements.js'
5
+ import { loadTemplateManifest } from '../registry/loadTemplateManifest.js'
6
+ import { validateTemplateManifest } from '../registry/validateTemplateManifest.js'
7
+ import { heading, info, item, status } from '../utils/logger.js'
8
+ import { packageRoot } from '../utils/paths.js'
9
+ import { fileExists, validateTargetProject } from '../utils/project.js'
10
+
11
+ const SUPPORTED_HEADERS = new Set(['HeaderDD02'])
12
+ const ACTIVE_HEADER_FILES = [
13
+ 'src/Header/config.ts',
14
+ 'src/Header/Nav/index.tsx',
15
+ 'src/Header/RowLabel.tsx',
16
+ ]
17
+
18
+ export async function addHeader({ headerName, targetDirectory = process.cwd(), dryRun = false }) {
19
+ if (!headerName) throw new Error('A header name is required. Example: blocks-dusted header add HeaderDD02 --dry-run')
20
+ if (!SUPPORTED_HEADERS.has(headerName)) throw new Error(`Unsupported header workflow: ${headerName}`)
21
+
22
+ const { manifest, templateDirectory } = await loadTemplateManifest(headerName)
23
+ validateTemplateManifest(manifest)
24
+ if (manifest.type !== 'component') throw new Error(`${headerName} must be a component template.`)
25
+
26
+ const checks = await checkRequirements({ manifest, targetDirectory })
27
+ const dependencyChecks = await checkDependencies({ manifest, targetDirectory })
28
+ const project = await validateTargetProject(targetDirectory)
29
+ const missingRequired = checks.missingRequirements ?? []
30
+ if (missingRequired.length > 0) {
31
+ throw new Error(`Required project files are missing:\n${missingRequired.map(formatMissingRequirement).join('\n')}`)
32
+ }
33
+
34
+ const activeFileChecks = await Promise.all(
35
+ ACTIVE_HEADER_FILES.map(async (path) => ({ path, exists: await fileExists(`${targetDirectory}/${path}`) })),
36
+ )
37
+ const missingActive = activeFileChecks.filter((check) => !check.exists)
38
+ if (missingActive.length > 0) {
39
+ throw new Error(`Active Header files are missing:\n${missingActive.map((check) => `- ${check.path}`).join('\n')}`)
40
+ }
41
+
42
+ const destinationBackupPath = checks.collisions.destinationExists
43
+ ? await plannedBackupPath({ destinationPath: checks.collisions.destinationPath, targetDirectory })
44
+ : null
45
+ const activeBackups = await Promise.all(
46
+ ACTIVE_HEADER_FILES.map(async (path) => ({
47
+ path,
48
+ backupPath: await plannedBackupPath({ destinationPath: path, targetDirectory }),
49
+ })),
50
+ )
51
+
52
+ heading('Header workflow')
53
+ info(`Name: ${manifest.name}`)
54
+ info(`Target directory: ${targetDirectory}`)
55
+ info(`Payload config: ${project.payloadConfigPath}`)
56
+ info(`Source template path: ${templateDirectory}`)
57
+ info(`Target install path: ${checks.collisions.destinationPath}`)
58
+
59
+ heading(dryRun ? 'Files that would be installed' : 'Files to install')
60
+ for (const file of manifest.files) item(`${file.from} -> ${file.to}`)
61
+
62
+ heading('Required project files')
63
+ for (const check of checks.requiredFiles) status(check.exists, check.path, check.reason)
64
+
65
+ heading('Shared files')
66
+ if ((manifest.sharedFiles ?? []).length === 0) item('None')
67
+ for (const file of manifest.sharedFiles ?? []) item(`${file.from} -> ${file.to}`)
68
+
69
+ heading('Package dependencies')
70
+ for (const dependency of dependencyChecks.declared) {
71
+ const isMissing = dependencyChecks.missing.some((missing) => missing.name === dependency.name)
72
+ status(!isMissing, formatDependency(dependency), isMissing ? 'missing' : 'already listed')
73
+ }
74
+ if (dependencyChecks.missing.length > 0) item(`Install command: ${installCommandText(dependencyChecks.packageManager, dependencyChecks.missing)}`)
75
+
76
+ heading('Active Header backups')
77
+ for (const backup of activeBackups) {
78
+ item(dryRun ? `Would back up ${backup.path} to ${backup.backupPath}` : `Will back up ${backup.path} to ${backup.backupPath}`)
79
+ }
80
+ if (destinationBackupPath) {
81
+ item(dryRun ? `Would back up ${checks.collisions.destinationPath} to ${destinationBackupPath}` : `Will back up ${checks.collisions.destinationPath} to ${destinationBackupPath}`)
82
+ }
83
+
84
+ heading('Activation patches')
85
+ item('Patch src/Header/Nav/index.tsx to render HeaderDD02')
86
+ item('Patch src/Header/config.ts to expose HeaderDD02 fields and preserve legacy navItems as hidden data')
87
+
88
+ if (dryRun) {
89
+ const sharedResults = await copySharedFiles({ manifest, packageRoot, targetDirectory, dryRun: true })
90
+ heading('Dry-run shared file results')
91
+ if (sharedResults.length === 0) item('None')
92
+ sharedResults.forEach((result) => item(`${result.status}: ${result.to}${result.reason ? ` - ${result.reason}` : ''}`))
93
+ info('Dry run complete: no files were changed.')
94
+ return
95
+ }
96
+
97
+ const sharedResults = await copySharedFiles({ manifest, packageRoot, targetDirectory })
98
+
99
+ if (checks.collisions.destinationExists) {
100
+ await backupExistingDestination({ destinationPath: checks.collisions.destinationPath, targetDirectory })
101
+ }
102
+ for (const file of ACTIVE_HEADER_FILES) {
103
+ await backupExistingDestination({ destinationPath: file, targetDirectory })
104
+ }
105
+
106
+ const copiedFiles = await copyTemplateFiles({ manifest, templateDirectory, targetDirectory })
107
+ await writeFile(`${targetDirectory}/src/Header/Nav/index.tsx`, renderHeaderNav())
108
+ await writeFile(`${targetDirectory}/src/Header/config.ts`, renderHeaderConfig())
109
+
110
+ heading('Copied files')
111
+ copiedFiles.forEach((file) => item(file))
112
+ heading('Shared file results')
113
+ if (sharedResults.length === 0) item('None')
114
+ sharedResults.forEach((result) => item(`${result.status}: ${result.to}${result.reason ? ` - ${result.reason}` : ''}`))
115
+ heading('Dependency install')
116
+ item('Skipped: dependency installation is manual')
117
+ if (dependencyChecks.missing.length > 0) item(`Run manually: ${installCommandText(dependencyChecks.packageManager, dependencyChecks.missing)}`)
118
+ heading('Payload commands to run manually')
119
+ const payloadCommand = dependencyChecks.packageManager === 'npm' ? 'npx payload' : `${dependencyChecks.packageManager} payload`
120
+ item(`${payloadCommand} generate:types`)
121
+ item(`${payloadCommand} generate:importmap`)
122
+ info('Header workflow complete.')
123
+ }
124
+
125
+ function renderHeaderNav() {
126
+ return `import React from 'react'\n\nimport type { Header as HeaderType } from '@/payload-types'\n\nimport HeaderDD02 from '@/Header/variants/HeaderDD02'\n\nexport const HeaderNav: React.FC<{ data: HeaderType }> = ({ data }) => {\n return <HeaderDD02 data={data} />\n}\n`
127
+ }
128
+
129
+ function renderHeaderConfig() {
130
+ return `import type { GlobalConfig } from 'payload'\n\nimport { iconPicker } from '@/fields/lucide-icon-picker/field'\nimport { link } from '@/fields/link'\nimport { HeaderDD02 } from '@/Header/variants/HeaderDD02/config'\nimport { revalidateHeader } from './hooks/revalidateHeader'\n\nexport const Header: GlobalConfig = {\n slug: 'header',\n access: {\n read: () => true,\n update: () => true,\n },\n fields: [\n {\n name: 'variant',\n label: 'Variant',\n type: 'select',\n defaultValue: 'header02',\n options: [{ label: 'Header DD 02', value: 'header02' }],\n admin: {\n position: 'sidebar',\n },\n },\n {\n name: 'header02',\n type: 'group',\n fields: HeaderDD02,\n },\n {\n name: 'navItems',\n type: 'array',\n admin: {\n hidden: true,\n description: 'Legacy starter Header navItems retained so existing stored header data is not discarded.',\n },\n fields: [\n link({ appearances: false }),\n iconPicker({\n name: 'icon',\n required: false,\n description: 'Legacy optional icon to display with the navigation item',\n }),\n ],\n },\n ],\n hooks: {\n afterChange: [revalidateHeader],\n },\n}\n`
131
+ }
132
+
133
+ function formatMissingRequirement(check) {
134
+ const resolution = check.resolution ? `\n Resolve: ${check.resolution}` : ''
135
+ return `- ${check.path}: ${check.reason}${resolution}`
136
+ }
137
+
138
+ function formatDependency(dependency) {
139
+ return `${dependency.name}${dependency.version ? `@${dependency.version}` : ''}`
140
+ }
141
+
142
+ function installCommandText(packageManager, dependencies) {
143
+ const command = packageManager === 'yarn' ? ['yarn', 'add'] : packageManager === 'bun' ? ['bun', 'add'] : packageManager === 'npm' ? ['npm', 'install'] : ['pnpm', 'add']
144
+ return [...command, ...dependencies.map(formatDependency)].join(' ')
145
+ }
@@ -133,7 +133,7 @@ export default function MobileBottomNav({ data }: MobileBottomNavProps) {
133
133
  const pathname = usePathname()
134
134
  const normalizedCurrentPathname = useMemo(() => normalizePath(pathname), [pathname])
135
135
 
136
- const navItems = data?.header02?.navItems || []
136
+ const navItems = data?.header02?.navItems || (data as any)?.navItems || []
137
137
  const showLabels = data?.header02?.showMobileLabels ?? true
138
138
 
139
139
  useEffect(() => {
@@ -251,7 +251,7 @@ export default function HeaderDD02({ data, variant = 'default', sticky = true }:
251
251
  }, [enableHideOnScroll, scrollThreshold])
252
252
 
253
253
  // Extract navItems from header02 data
254
- const navItems = data?.header02?.navItems || []
254
+ const navItems = data?.header02?.navItems || (data as any)?.navItems || []
255
255
 
256
256
  return (
257
257
  <>