dynamic-zone 1.0.6 → 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 +95 -86
- 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,122 +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
|
+
`);
|
|
39
|
+
process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
if (args.includes("--version") || args.includes("-v")) {
|
|
42
|
+
const pkg = require(path.join(__dirname, "package.json"));
|
|
43
|
+
console.log(pkg.version);
|
|
11
44
|
process.exit(0);
|
|
12
45
|
}
|
|
13
46
|
|
|
14
|
-
//
|
|
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
|
+
}
|
|
52
|
+
|
|
15
53
|
const packageJsonPath = path.join(basePath, "package.json");
|
|
16
54
|
if (!fs.existsSync(packageJsonPath)) {
|
|
17
|
-
console.log(
|
|
55
|
+
console.log(
|
|
56
|
+
`${c.red}✖${c.reset} package.json not found. Run this from a Node.js project root.`
|
|
57
|
+
);
|
|
18
58
|
process.exit(1);
|
|
19
59
|
}
|
|
20
60
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
+
}
|
|
24
68
|
|
|
69
|
+
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
25
70
|
if (!deps.next) {
|
|
26
71
|
console.log(
|
|
27
|
-
|
|
72
|
+
`${c.red}✖${c.reset} Not a Next.js project. dynamic-zone requires Next.js.`
|
|
28
73
|
);
|
|
29
74
|
process.exit(1);
|
|
30
75
|
}
|
|
31
76
|
|
|
32
|
-
// --- 3️⃣ Check if app folder exists ---
|
|
33
77
|
const appPath = path.join(basePath, "app");
|
|
34
78
|
if (!fs.existsSync(appPath)) {
|
|
35
|
-
console.log(
|
|
79
|
+
console.log(
|
|
80
|
+
`${c.yellow}⚠${c.reset} No \`app\` directory found. Create it first (e.g. \`npx create-next-app@latest\`).`
|
|
81
|
+
);
|
|
36
82
|
process.exit(1);
|
|
37
83
|
}
|
|
38
84
|
|
|
39
|
-
// Paths
|
|
85
|
+
// ─── Paths ───────────────────────────────────────────────────────────────────
|
|
40
86
|
const cmsPath = path.join(appPath, "cms");
|
|
41
87
|
const componentsPath = path.join(cmsPath, "components");
|
|
42
88
|
const dynamicZonePath = path.join(cmsPath, "dynamic-zone");
|
|
43
89
|
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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}`);
|
|
49
111
|
}
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
// Section Renderer file content
|
|
53
|
-
const fileContent = `import dynamic from "next/dynamic";
|
|
54
|
-
import { Suspense } from "react";
|
|
55
|
-
|
|
56
|
-
type SectionContentType = {
|
|
57
|
-
sections: { __component: string }[];
|
|
58
|
-
};
|
|
59
|
-
type SectionType = NonNullable<SectionContentType["sections"][number]>;
|
|
60
|
-
|
|
61
|
-
interface Props {
|
|
62
|
-
sections?: unknown;
|
|
63
|
-
slug?: string;
|
|
64
112
|
}
|
|
65
113
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
}>;
|
|
70
|
-
|
|
71
|
-
const dynamicImport = (path: string) =>
|
|
72
|
-
dynamic(() => import(\`../components/\${path}\`), {
|
|
73
|
-
loading: () => <div>Loading...</div>,
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
const componentMap: { [key: string]: ComponentMapType } = {};
|
|
77
|
-
|
|
78
|
-
export default function SectionRenderer({ sections, slug }: Props) {
|
|
79
|
-
if (!sections) return null;
|
|
80
|
-
|
|
81
|
-
if (Array.isArray(sections) && sections.length === 0) {
|
|
82
|
-
return (
|
|
83
|
-
// This is the div for the sections that are not found DO W-FULL PX
|
|
84
|
-
<div>No Sections Found for {slug}</div>
|
|
85
|
-
);
|
|
86
|
-
}
|
|
114
|
+
// ─── Section renderer template ───────────────────────────────────────────────
|
|
115
|
+
const templatePath = path.join(__dirname, "templates", "section-renderer.template");
|
|
116
|
+
const sectionRendererContent = fs.readFileSync(templatePath, "utf8");
|
|
87
117
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
// This is the div for the section that is not found DO W-FULL
|
|
95
|
-
<div key={index}>No Section Found {sectionType}</div>
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
return (
|
|
100
|
-
<div key={index}>
|
|
101
|
-
<Suspense fallback={<div>Loading...</div>}>
|
|
102
|
-
<Component index={index} content={section} />
|
|
103
|
-
</Suspense>
|
|
104
|
-
</div>
|
|
105
|
-
);
|
|
106
|
-
};
|
|
107
|
-
|
|
108
|
-
const sectionsArray = Array.isArray(sections) ? sections : [];
|
|
109
|
-
return (
|
|
110
|
-
// This is the root div for the dynamic zone DO FLEX FLEX-COL
|
|
111
|
-
<div>{sectionsArray.map(renderSection)}</div>
|
|
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`
|
|
112
124
|
);
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
`;
|
|
116
|
-
|
|
117
|
-
// Write file
|
|
118
|
-
const filePath = path.join(dynamicZonePath, "section-renderer.tsx");
|
|
119
|
-
if (!fs.existsSync(filePath)) {
|
|
120
|
-
fs.writeFileSync(filePath, fileContent);
|
|
121
|
-
console.log("📄 Created file:", filePath);
|
|
122
125
|
} else {
|
|
123
|
-
console.log(
|
|
126
|
+
console.log(
|
|
127
|
+
` ${c.dim}○${c.reset} app/cms/dynamic-zone/section-renderer.tsx ${c.dim}(exists)${c.reset}`
|
|
128
|
+
);
|
|
124
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
|
+
}
|