dynamic-zone 1.0.5 → 1.0.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 +78 -1
- package/index.js +109 -83
- package/package.json +1 -1
- package/templates/section-renderer.template +52 -0
package/README.md
CHANGED
|
@@ -1 +1,78 @@
|
|
|
1
|
-
#
|
|
1
|
+
# dynamic-zone
|
|
2
|
+
|
|
3
|
+
Scaffold a CMS dynamic zone for Next.js apps. Zero dependencies, zero config.
|
|
4
|
+
|
|
5
|
+
## Requirements
|
|
6
|
+
|
|
7
|
+
- Next.js (App Router)
|
|
8
|
+
- An `app` directory in your project
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install dynamic-zone
|
|
14
|
+
# or
|
|
15
|
+
pnpm add dynamic-zone
|
|
16
|
+
# or
|
|
17
|
+
yarn add dynamic-zone
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
Run from your Next.js project root:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
npx dynamic-zone
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Or if installed globally:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
dynamic-zone
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## What It Creates
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
app/
|
|
38
|
+
└── cms/
|
|
39
|
+
├── components/ # Add your section components here
|
|
40
|
+
└── dynamic-zone/
|
|
41
|
+
└── section-renderer.tsx
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Next Steps
|
|
45
|
+
|
|
46
|
+
1. **Add section components** to `app/cms/components/` (e.g. `hero.tsx`, `features.tsx`).
|
|
47
|
+
|
|
48
|
+
2. **Register them** in the `componentMap` in `section-renderer.tsx`:
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
const componentMap: { [key: string]: ComponentMapType } = {
|
|
52
|
+
hero: dynamicImport("hero"),
|
|
53
|
+
features: dynamicImport("features"),
|
|
54
|
+
};
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
3. **Use the renderer** in your pages:
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
import SectionRenderer from "@/app/cms/dynamic-zone/section-renderer";
|
|
61
|
+
|
|
62
|
+
export default function Page({ page }) {
|
|
63
|
+
return <SectionRenderer sections={page.sections} slug={page.slug} />;
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
The `__component` field from your CMS (e.g. `sections.hero`) is parsed to match the component key (`hero` → `hero`).
|
|
68
|
+
|
|
69
|
+
## CLI Options
|
|
70
|
+
|
|
71
|
+
| Flag | Description |
|
|
72
|
+
| --------------- | ------------ |
|
|
73
|
+
| `-h, --help` | Show help |
|
|
74
|
+
| `-v, --version` | Show version |
|
|
75
|
+
|
|
76
|
+
## License
|
|
77
|
+
|
|
78
|
+
ISC
|
package/index.js
CHANGED
|
@@ -3,105 +3,131 @@
|
|
|
3
3
|
const fs = require("fs");
|
|
4
4
|
const path = require("path");
|
|
5
5
|
|
|
6
|
+
// ─── ANSI escape codes (no external deps) ─────────────────────────────────────
|
|
7
|
+
const c = {
|
|
8
|
+
reset: "\x1b[0m",
|
|
9
|
+
dim: "\x1b[2m",
|
|
10
|
+
bold: "\x1b[1m",
|
|
11
|
+
green: "\x1b[32m",
|
|
12
|
+
red: "\x1b[31m",
|
|
13
|
+
yellow: "\x1b[33m",
|
|
14
|
+
cyan: "\x1b[36m",
|
|
15
|
+
blue: "\x1b[34m",
|
|
16
|
+
};
|
|
17
|
+
|
|
6
18
|
const basePath = process.cwd();
|
|
7
19
|
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
20
|
+
// ─── CLI flags ───────────────────────────────────────────────────────────────
|
|
21
|
+
const args = process.argv.slice(2);
|
|
22
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
23
|
+
console.log(`
|
|
24
|
+
${c.bold}dynamic-zone${c.reset} — Scaffold CMS dynamic zone for Next.js
|
|
25
|
+
|
|
26
|
+
${c.dim}Usage:${c.reset}
|
|
27
|
+
dynamic-zone
|
|
28
|
+
npx dynamic-zone
|
|
29
|
+
|
|
30
|
+
${c.dim}Options:${c.reset}
|
|
31
|
+
-h, --help Show this help
|
|
32
|
+
-v, --version Show version
|
|
33
|
+
|
|
34
|
+
${c.dim}Creates:${c.reset}
|
|
35
|
+
app/cms/
|
|
36
|
+
app/cms/components/
|
|
37
|
+
app/cms/dynamic-zone/section-renderer.tsx
|
|
38
|
+
`);
|
|
11
39
|
process.exit(0);
|
|
12
40
|
}
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
if (!fs.existsSync(srcPath)) {
|
|
17
|
-
console.log("⚠️ No app folder found. Please create a 'app' folder first.");
|
|
41
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
42
|
+
const pkg = require(path.join(__dirname, "package.json"));
|
|
43
|
+
console.log(pkg.version);
|
|
18
44
|
process.exit(0);
|
|
19
45
|
}
|
|
20
46
|
|
|
21
|
-
//
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
// Create folders
|
|
27
|
-
[cmsPath, componentsPath, dynamicZonePath].forEach((dir) => {
|
|
28
|
-
if (!fs.existsSync(dir)) {
|
|
29
|
-
fs.mkdirSync(dir);
|
|
30
|
-
console.log("✅ Created:", dir);
|
|
31
|
-
}
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
// Section Renderer file content
|
|
35
|
-
const fileContent = `import dynamic from "next/dynamic";
|
|
36
|
-
import { Suspense } from "react";
|
|
37
|
-
|
|
38
|
-
type SectionContentType = {
|
|
39
|
-
sections: { __component: string }[];
|
|
40
|
-
};
|
|
41
|
-
type SectionType = NonNullable<SectionContentType["sections"][number]>;
|
|
47
|
+
// ─── Guards ─────────────────────────────────────────────────────────────────
|
|
48
|
+
if (basePath.includes("node_modules")) {
|
|
49
|
+
console.log(`${c.red}✖${c.reset} Do not run inside node_modules`);
|
|
50
|
+
process.exit(1);
|
|
51
|
+
}
|
|
42
52
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
53
|
+
const packageJsonPath = path.join(basePath, "package.json");
|
|
54
|
+
if (!fs.existsSync(packageJsonPath)) {
|
|
55
|
+
console.log(
|
|
56
|
+
`${c.red}✖${c.reset} package.json not found. Run this from a Node.js project root.`
|
|
57
|
+
);
|
|
58
|
+
process.exit(1);
|
|
46
59
|
}
|
|
47
60
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
61
|
+
let packageJson;
|
|
62
|
+
try {
|
|
63
|
+
packageJson = JSON.parse(fs.readFileSync(packageJsonPath, "utf8"));
|
|
64
|
+
} catch {
|
|
65
|
+
console.log(`${c.red}✖${c.reset} Invalid package.json`);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
52
68
|
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
69
|
+
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
70
|
+
if (!deps.next) {
|
|
71
|
+
console.log(
|
|
72
|
+
`${c.red}✖${c.reset} Not a Next.js project. dynamic-zone requires Next.js.`
|
|
73
|
+
);
|
|
74
|
+
process.exit(1);
|
|
75
|
+
}
|
|
57
76
|
|
|
58
|
-
const
|
|
77
|
+
const appPath = path.join(basePath, "app");
|
|
78
|
+
if (!fs.existsSync(appPath)) {
|
|
79
|
+
console.log(
|
|
80
|
+
`${c.yellow}⚠${c.reset} No \`app\` directory found. Create it first (e.g. \`npx create-next-app@latest\`).`
|
|
81
|
+
);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
59
84
|
|
|
60
|
-
|
|
61
|
-
|
|
85
|
+
// ─── Paths ───────────────────────────────────────────────────────────────────
|
|
86
|
+
const cmsPath = path.join(appPath, "cms");
|
|
87
|
+
const componentsPath = path.join(cmsPath, "components");
|
|
88
|
+
const dynamicZonePath = path.join(cmsPath, "dynamic-zone");
|
|
62
89
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
90
|
+
// ─── Banner ──────────────────────────────────────────────────────────────────
|
|
91
|
+
console.log(`
|
|
92
|
+
${c.cyan}${c.bold} dynamic-zone${c.reset} ${c.dim}v${
|
|
93
|
+
require(path.join(__dirname, "package.json")).version
|
|
94
|
+
}${c.reset}
|
|
95
|
+
${c.dim} Scaffolding CMS dynamic zone for Next.js${c.reset}
|
|
96
|
+
`);
|
|
97
|
+
|
|
98
|
+
// ─── Create directories ─────────────────────────────────────────────────────
|
|
99
|
+
const dirs = [
|
|
100
|
+
[cmsPath, "app/cms"],
|
|
101
|
+
[componentsPath, "app/cms/components"],
|
|
102
|
+
[dynamicZonePath, "app/cms/dynamic-zone"],
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
for (const [fullPath, label] of dirs) {
|
|
106
|
+
if (!fs.existsSync(fullPath)) {
|
|
107
|
+
fs.mkdirSync(fullPath, { recursive: true });
|
|
108
|
+
console.log(` ${c.green}✓${c.reset} ${label}`);
|
|
109
|
+
} else {
|
|
110
|
+
console.log(` ${c.dim}○${c.reset} ${label} ${c.dim}(exists)${c.reset}`);
|
|
69
111
|
}
|
|
70
|
-
|
|
71
|
-
const renderSection = (section: SectionType, index: number) => {
|
|
72
|
-
const sectionType = section.__component?.split(".")[1]?.replace(/-/g, "_");
|
|
73
|
-
const Component = componentMap[sectionType];
|
|
74
|
-
|
|
75
|
-
if (!Component) {
|
|
76
|
-
return (
|
|
77
|
-
<div className="w-full" key={index}>
|
|
78
|
-
No Section Found {sectionType}
|
|
79
|
-
</div>
|
|
80
|
-
);
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return (
|
|
84
|
-
<div key={index}>
|
|
85
|
-
<Suspense fallback={<div>Loading...</div>}>
|
|
86
|
-
<Component index={index} content={section} />
|
|
87
|
-
</Suspense>
|
|
88
|
-
</div>
|
|
89
|
-
);
|
|
90
|
-
};
|
|
91
|
-
|
|
92
|
-
return (
|
|
93
|
-
<div className="flex flex-col">
|
|
94
|
-
{(sections || []).map(renderSection)}
|
|
95
|
-
</div>
|
|
96
|
-
);
|
|
97
112
|
}
|
|
98
|
-
`;
|
|
99
113
|
|
|
100
|
-
//
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
114
|
+
// ─── Section renderer template ───────────────────────────────────────────────
|
|
115
|
+
const templatePath = path.join(__dirname, "templates", "section-renderer.template");
|
|
116
|
+
const sectionRendererContent = fs.readFileSync(templatePath, "utf8");
|
|
117
|
+
|
|
118
|
+
// ─── Write section-renderer.tsx ──────────────────────────────────────────────
|
|
119
|
+
const sectionRendererPath = path.join(dynamicZonePath, "section-renderer.tsx");
|
|
120
|
+
if (!fs.existsSync(sectionRendererPath)) {
|
|
121
|
+
fs.writeFileSync(sectionRendererPath, sectionRendererContent);
|
|
122
|
+
console.log(
|
|
123
|
+
` ${c.green}✓${c.reset} app/cms/dynamic-zone/section-renderer.tsx`
|
|
124
|
+
);
|
|
105
125
|
} else {
|
|
106
|
-
console.log(
|
|
126
|
+
console.log(
|
|
127
|
+
` ${c.dim}○${c.reset} app/cms/dynamic-zone/section-renderer.tsx ${c.dim}(exists)${c.reset}`
|
|
128
|
+
);
|
|
107
129
|
}
|
|
130
|
+
|
|
131
|
+
console.log(`
|
|
132
|
+
${c.green}${c.bold}Done.${c.reset} ${c.dim}Add your section components to app/cms/components/ and register them in the componentMap.${c.reset}
|
|
133
|
+
`);
|
package/package.json
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import dynamic from "next/dynamic";
|
|
2
|
+
import { Suspense } from "react";
|
|
3
|
+
|
|
4
|
+
type SectionContentType = {
|
|
5
|
+
sections: { __component: string }[];
|
|
6
|
+
};
|
|
7
|
+
type SectionType = NonNullable<SectionContentType["sections"][number]>;
|
|
8
|
+
|
|
9
|
+
interface Props {
|
|
10
|
+
sections?: unknown;
|
|
11
|
+
slug?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
type ComponentMapType = React.ComponentType<{
|
|
15
|
+
index?: number;
|
|
16
|
+
content?: unknown;
|
|
17
|
+
}>;
|
|
18
|
+
|
|
19
|
+
const dynamicImport = (path: string) =>
|
|
20
|
+
dynamic(() => import(`../components/${path}`), {
|
|
21
|
+
loading: () => <div>Loading...</div>,
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const componentMap: { [key: string]: ComponentMapType } = {};
|
|
25
|
+
|
|
26
|
+
export default function SectionRenderer({ sections, slug }: Props) {
|
|
27
|
+
if (!sections) return null;
|
|
28
|
+
|
|
29
|
+
if (Array.isArray(sections) && sections.length === 0) {
|
|
30
|
+
return <div>No Sections Found for {slug}</div>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const renderSection = (section: SectionType, index: number) => {
|
|
34
|
+
const sectionType = section.__component?.split(".")[1]?.replace(/-/g, "_");
|
|
35
|
+
const Component = componentMap[sectionType];
|
|
36
|
+
|
|
37
|
+
if (!Component) {
|
|
38
|
+
return <div key={index}>No Section Found {sectionType}</div>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return (
|
|
42
|
+
<div key={index}>
|
|
43
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
44
|
+
<Component index={index} content={section} />
|
|
45
|
+
</Suspense>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const sectionsArray = Array.isArray(sections) ? sections : [];
|
|
51
|
+
return <div>{sectionsArray.map(renderSection)}</div>;
|
|
52
|
+
}
|