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
|
@@ -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,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')
|