create-puck-app 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- package/README.md +21 -0
- package/index.js +179 -0
- package/package.json +19 -0
- package/templates/next/.eslintrc.js +4 -0
- package/templates/next/README.md +39 -0
- package/templates/next/app/[...puckPath]/client.tsx +32 -0
- package/templates/next/app/[...puckPath]/page.tsx +49 -0
- package/templates/next/app/[...puckPath]/resolve-puck-path.ts +15 -0
- package/templates/next/app/api/puck/route.ts +31 -0
- package/templates/next/app/layout.tsx +14 -0
- package/templates/next/app/page.tsx +1 -0
- package/templates/next/app/styles.css +5 -0
- package/templates/next/next-env.d.ts +5 -0
- package/templates/next/next.config.js +4 -0
- package/templates/next/package.json.hbs +25 -0
- package/templates/next/puck.config.tsx +25 -0
- package/templates/next/tsconfig/base.json +20 -0
- package/templates/next/tsconfig/nextjs.json +21 -0
- package/templates/next/tsconfig.json +8 -0
package/README.md
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
# create-puck-app
|
2
|
+
|
3
|
+
`create-puck-app` generates recipes. For a full list of recipes, please see the monorepo README.
|
4
|
+
|
5
|
+
## Usage
|
6
|
+
|
7
|
+
npx
|
8
|
+
|
9
|
+
```sh
|
10
|
+
npx create-puck-app my-app
|
11
|
+
```
|
12
|
+
|
13
|
+
yarn
|
14
|
+
|
15
|
+
```sh
|
16
|
+
yarn create puck-app my-app
|
17
|
+
```
|
18
|
+
|
19
|
+
## License
|
20
|
+
|
21
|
+
MIT © [Measured Co.](https://github.com/measuredco)
|
package/index.js
ADDED
@@ -0,0 +1,179 @@
|
|
1
|
+
#!/usr/bin/env node
|
2
|
+
|
3
|
+
import fs from "fs";
|
4
|
+
import path from "path";
|
5
|
+
import { program } from "commander";
|
6
|
+
import inquirer from "inquirer";
|
7
|
+
import Handlebars from "handlebars";
|
8
|
+
import glob from "glob";
|
9
|
+
import { execSync } from "child_process";
|
10
|
+
import { dirname } from "path";
|
11
|
+
import { fileURLToPath } from "url";
|
12
|
+
|
13
|
+
const __filename = fileURLToPath(import.meta.url);
|
14
|
+
const __dirname = dirname(__filename);
|
15
|
+
|
16
|
+
const packageJson = JSON.parse(
|
17
|
+
fs.readFileSync(path.join(__dirname, "./package.json"))
|
18
|
+
);
|
19
|
+
|
20
|
+
// Lifted from https://github.com/vercel/next.js/blob/c2d7bbd1b82c71808b99e9a7944fb16717a581db/packages/create-next-app/helpers/get-pkg-manager.ts
|
21
|
+
function getPkgManager() {
|
22
|
+
// eslint-disable-next-line turbo/no-undeclared-env-vars
|
23
|
+
const userAgent = process.env.npm_config_user_agent || "";
|
24
|
+
|
25
|
+
if (userAgent.startsWith("yarn")) {
|
26
|
+
return "yarn";
|
27
|
+
}
|
28
|
+
|
29
|
+
if (userAgent.startsWith("pnpm")) {
|
30
|
+
return "pnpm";
|
31
|
+
}
|
32
|
+
|
33
|
+
return "npm";
|
34
|
+
}
|
35
|
+
|
36
|
+
program
|
37
|
+
.command("create [app-name]")
|
38
|
+
.option(
|
39
|
+
"--use-npm",
|
40
|
+
`
|
41
|
+
|
42
|
+
Explicitly tell the CLI to bootstrap the application using npm
|
43
|
+
`
|
44
|
+
)
|
45
|
+
.option(
|
46
|
+
"--use-pnpm",
|
47
|
+
`
|
48
|
+
|
49
|
+
Explicitly tell the CLI to bootstrap the application using pnpm
|
50
|
+
`
|
51
|
+
)
|
52
|
+
.option(
|
53
|
+
"--use-yarn",
|
54
|
+
`
|
55
|
+
|
56
|
+
Explicitly tell the CLI to bootstrap the application using Yarn
|
57
|
+
`
|
58
|
+
)
|
59
|
+
.action(async (_appName, options) => {
|
60
|
+
const beforeQuestions = [];
|
61
|
+
|
62
|
+
if (!_appName) {
|
63
|
+
beforeQuestions.push({
|
64
|
+
type: "input",
|
65
|
+
name: "appName",
|
66
|
+
message: "What is the name of your app?",
|
67
|
+
required: true,
|
68
|
+
});
|
69
|
+
}
|
70
|
+
|
71
|
+
const questions = [
|
72
|
+
...beforeQuestions,
|
73
|
+
{
|
74
|
+
type: "input",
|
75
|
+
name: "recipe",
|
76
|
+
message: "Which recipe would you like to use?",
|
77
|
+
required: true,
|
78
|
+
default: "next",
|
79
|
+
},
|
80
|
+
];
|
81
|
+
const answers = await inquirer.prompt(questions);
|
82
|
+
const appName = answers.appName || _appName;
|
83
|
+
const recipe = answers.recipe;
|
84
|
+
|
85
|
+
// Copy template files to the new directory
|
86
|
+
const templatePath = path.join(__dirname, "./templates", recipe);
|
87
|
+
const appPath = path.join(process.cwd(), appName);
|
88
|
+
|
89
|
+
if (!recipe) {
|
90
|
+
console.error(`Please specify a recipe.`);
|
91
|
+
return;
|
92
|
+
}
|
93
|
+
|
94
|
+
if (!fs.existsSync(templatePath)) {
|
95
|
+
console.error(`No recipe named ${recipe} exists.`);
|
96
|
+
return;
|
97
|
+
}
|
98
|
+
|
99
|
+
if (fs.existsSync(appPath)) {
|
100
|
+
console.error(
|
101
|
+
`A directory called ${appName} already exists. Please use a different name or delete this directory.`
|
102
|
+
);
|
103
|
+
return;
|
104
|
+
}
|
105
|
+
|
106
|
+
await fs.mkdirSync(appName);
|
107
|
+
|
108
|
+
const packageManager = !!options.useNpm
|
109
|
+
? "npm"
|
110
|
+
: !!options.usePnpm
|
111
|
+
? "pnpm"
|
112
|
+
: !!options.useYarn
|
113
|
+
? "yarn"
|
114
|
+
: getPkgManager();
|
115
|
+
|
116
|
+
// Compile handlebars templates
|
117
|
+
const templateFiles = glob.sync(`**/*`, {
|
118
|
+
cwd: templatePath,
|
119
|
+
nodir: true,
|
120
|
+
dot: true,
|
121
|
+
});
|
122
|
+
|
123
|
+
for (const templateFile of templateFiles) {
|
124
|
+
const filePath = path.join(templatePath, templateFile);
|
125
|
+
const targetPath = filePath
|
126
|
+
.replace(templatePath, appPath)
|
127
|
+
.replace(".hbs", "");
|
128
|
+
|
129
|
+
let data;
|
130
|
+
|
131
|
+
if (path.extname(filePath) === ".hbs") {
|
132
|
+
const templateString = await fs.readFileSync(filePath, "utf-8");
|
133
|
+
|
134
|
+
const template = Handlebars.compile(templateString);
|
135
|
+
data = template({
|
136
|
+
...answers,
|
137
|
+
appName,
|
138
|
+
puckVersion: `^${packageJson.version}`,
|
139
|
+
});
|
140
|
+
} else {
|
141
|
+
data = await fs.readFileSync(filePath, "utf-8");
|
142
|
+
}
|
143
|
+
|
144
|
+
const dir = path.dirname(targetPath);
|
145
|
+
|
146
|
+
await fs.mkdirSync(dir, { recursive: true });
|
147
|
+
|
148
|
+
await fs.writeFileSync(targetPath, data);
|
149
|
+
}
|
150
|
+
|
151
|
+
if (packageManager === "yarn") {
|
152
|
+
execSync("yarn install", { cwd: appPath, stdio: "inherit" });
|
153
|
+
} else {
|
154
|
+
execSync(`${packageManager} i`, { cwd: appPath, stdio: "inherit" });
|
155
|
+
}
|
156
|
+
|
157
|
+
let inGitRepo = false;
|
158
|
+
|
159
|
+
try {
|
160
|
+
inGitRepo =
|
161
|
+
execSync("git status", {
|
162
|
+
cwd: appPath,
|
163
|
+
})
|
164
|
+
.toString()
|
165
|
+
.indexOf("fatal:") !== 0;
|
166
|
+
} catch {}
|
167
|
+
|
168
|
+
// Only commit if this is a new repo
|
169
|
+
if (!inGitRepo) {
|
170
|
+
execSync("git init", { cwd: appPath, stdio: "inherit" });
|
171
|
+
|
172
|
+
execSync("git add .", { cwd: appPath, stdio: "inherit" });
|
173
|
+
execSync("git commit -m 'build(puck): generate app'", {
|
174
|
+
cwd: appPath,
|
175
|
+
stdio: "inherit",
|
176
|
+
});
|
177
|
+
}
|
178
|
+
})
|
179
|
+
.parse(process.argv);
|
package/package.json
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
{
|
2
|
+
"name": "create-puck-app",
|
3
|
+
"version": "0.3.0",
|
4
|
+
"private": false,
|
5
|
+
"license": "MIT",
|
6
|
+
"type": "module",
|
7
|
+
"bin": {
|
8
|
+
"create-puck-app": "./index.js"
|
9
|
+
},
|
10
|
+
"files": [
|
11
|
+
"templates"
|
12
|
+
],
|
13
|
+
"dependencies": {
|
14
|
+
"commander": "^10.0.1",
|
15
|
+
"handlebars": "^4.7.7",
|
16
|
+
"inquirer": "^9.2.7",
|
17
|
+
"prettier": "^2.8.8"
|
18
|
+
}
|
19
|
+
}
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# `next` recipe
|
2
|
+
|
3
|
+
The `next` recipe showcases one of the most powerful ways to implement Puck using to provide an authoring tool for any route in your Next app.
|
4
|
+
|
5
|
+
## Demonstrates
|
6
|
+
|
7
|
+
- Next.js 13 App Router implementation
|
8
|
+
- JSON database implementation with HTTP API
|
9
|
+
- Catch-all routes to use puck for any route on the platform
|
10
|
+
|
11
|
+
## Usage
|
12
|
+
|
13
|
+
Run the generator and enter `next` when prompted
|
14
|
+
|
15
|
+
```
|
16
|
+
npx create-puck-app my-app
|
17
|
+
```
|
18
|
+
|
19
|
+
Start the server
|
20
|
+
|
21
|
+
```
|
22
|
+
yarn dev
|
23
|
+
```
|
24
|
+
|
25
|
+
Navigate to the homepage at https://localhost:3000. To edit the homepage, access the Puck editor at https://localhost:3000/edit.
|
26
|
+
|
27
|
+
You can do this for any route on the application, **even if the page doesn't exist**. For example, visit https://localhost:3000/hello/world and you'll receive a 404. You can author and publish a page by visiting https://localhost:3000/hello/world/edit. After publishing, go back to the original URL to see your page.
|
28
|
+
|
29
|
+
## Using this recipe
|
30
|
+
|
31
|
+
To adopt this recipe you will need to:
|
32
|
+
|
33
|
+
- **IMPORTANT** Add authentication to `/edit` routes. This can be done by modifying the example API routes in `/app/api/puck/route.ts` and server component in `/app/[...puckPath]/page.tsx`. **If you don't do this, Puck will be completely public.**
|
34
|
+
- Integrate your database into the API calls in `/app/api/puck/route.ts`
|
35
|
+
- Implement a custom puck configuration in `puck.config.tsx`
|
36
|
+
|
37
|
+
## License
|
38
|
+
|
39
|
+
MIT © [Measured Co.](https://github.com/measuredco)
|
@@ -0,0 +1,32 @@
|
|
1
|
+
"use client";
|
2
|
+
|
3
|
+
import type { Data } from "@measured/puck";
|
4
|
+
import { Puck, Render } from "@measured/puck";
|
5
|
+
import config from "../../puck.config";
|
6
|
+
|
7
|
+
export function Client({
|
8
|
+
path,
|
9
|
+
data,
|
10
|
+
isEdit,
|
11
|
+
}: {
|
12
|
+
path: string;
|
13
|
+
data: Data;
|
14
|
+
isEdit: boolean;
|
15
|
+
}) {
|
16
|
+
if (isEdit) {
|
17
|
+
return (
|
18
|
+
<Puck
|
19
|
+
config={config}
|
20
|
+
data={data}
|
21
|
+
onPublish={async (data: Data) => {
|
22
|
+
await fetch("/api/puck", {
|
23
|
+
method: "post",
|
24
|
+
body: JSON.stringify({ [path]: data }),
|
25
|
+
});
|
26
|
+
}}
|
27
|
+
/>
|
28
|
+
);
|
29
|
+
}
|
30
|
+
|
31
|
+
return <Render config={config} data={data} />;
|
32
|
+
}
|
@@ -0,0 +1,49 @@
|
|
1
|
+
import { Client } from "./client";
|
2
|
+
import { notFound } from "next/navigation";
|
3
|
+
import resolvePuckPath from "./resolve-puck-path";
|
4
|
+
import { Metadata } from "next";
|
5
|
+
import { Data } from "@measured/puck/types/Config";
|
6
|
+
|
7
|
+
export async function generateMetadata({
|
8
|
+
params,
|
9
|
+
}: {
|
10
|
+
params: { puckPath: string[] };
|
11
|
+
}): Promise<Metadata> {
|
12
|
+
const { isEdit, path } = resolvePuckPath(params.puckPath);
|
13
|
+
|
14
|
+
if (isEdit) {
|
15
|
+
return {
|
16
|
+
title: "Puck: " + path,
|
17
|
+
};
|
18
|
+
}
|
19
|
+
|
20
|
+
const data: Data = (
|
21
|
+
await fetch("http://localhost:3000/api/puck", {
|
22
|
+
next: { revalidate: 0 },
|
23
|
+
}).then((d) => d.json())
|
24
|
+
)[path];
|
25
|
+
|
26
|
+
return {
|
27
|
+
title: data?.page?.title,
|
28
|
+
};
|
29
|
+
}
|
30
|
+
|
31
|
+
export default async function Page({
|
32
|
+
params,
|
33
|
+
}: {
|
34
|
+
params: { puckPath: string[] };
|
35
|
+
}) {
|
36
|
+
const { isEdit, path } = resolvePuckPath(params.puckPath);
|
37
|
+
|
38
|
+
const data = (
|
39
|
+
await fetch("http://localhost:3000/api/puck", {
|
40
|
+
next: { revalidate: 0 },
|
41
|
+
}).then((d) => d.json())
|
42
|
+
)[path];
|
43
|
+
|
44
|
+
if (!data && !isEdit) {
|
45
|
+
return notFound();
|
46
|
+
}
|
47
|
+
|
48
|
+
return <Client isEdit={isEdit} path={path} data={data} />;
|
49
|
+
}
|
@@ -0,0 +1,15 @@
|
|
1
|
+
const resolvePuckPath = (puckPath: string[] = []) => {
|
2
|
+
const hasPath = puckPath.length > 0;
|
3
|
+
|
4
|
+
const isEdit = hasPath ? puckPath[puckPath.length - 1] === "edit" : false;
|
5
|
+
|
6
|
+
return {
|
7
|
+
isEdit,
|
8
|
+
path: `/${(isEdit
|
9
|
+
? [...puckPath].slice(0, puckPath.length - 1)
|
10
|
+
: [...puckPath]
|
11
|
+
).join("/")}`,
|
12
|
+
};
|
13
|
+
};
|
14
|
+
|
15
|
+
export default resolvePuckPath;
|
@@ -0,0 +1,31 @@
|
|
1
|
+
import { NextResponse } from "next/server";
|
2
|
+
import fs from "fs";
|
3
|
+
|
4
|
+
export async function GET() {
|
5
|
+
const data = fs.existsSync("database.json")
|
6
|
+
? fs.readFileSync("database.json", "utf-8")
|
7
|
+
: null;
|
8
|
+
|
9
|
+
return NextResponse.json(JSON.parse(data || "{}"));
|
10
|
+
}
|
11
|
+
|
12
|
+
export async function POST(request: Request) {
|
13
|
+
const data = await request.json();
|
14
|
+
|
15
|
+
const existingData = JSON.parse(
|
16
|
+
fs.existsSync("database.json")
|
17
|
+
? fs.readFileSync("database.json", "utf-8")
|
18
|
+
: "{}"
|
19
|
+
);
|
20
|
+
|
21
|
+
const updatedData = {
|
22
|
+
...existingData,
|
23
|
+
...data,
|
24
|
+
};
|
25
|
+
|
26
|
+
fs.writeFileSync("database.json", JSON.stringify(updatedData));
|
27
|
+
|
28
|
+
return NextResponse.json({ status: "ok" });
|
29
|
+
}
|
30
|
+
|
31
|
+
export const revalidate = 0;
|
@@ -0,0 +1 @@
|
|
1
|
+
export { default, generateMetadata } from "./[...puckPath]/page";
|
@@ -0,0 +1,25 @@
|
|
1
|
+
{
|
2
|
+
"name": "{{appName}}",
|
3
|
+
"version": "1.0.0",
|
4
|
+
"private": true,
|
5
|
+
"scripts": {
|
6
|
+
"dev": "next dev",
|
7
|
+
"build": "next build",
|
8
|
+
"start": "next start",
|
9
|
+
"lint": "next lint"
|
10
|
+
},
|
11
|
+
"dependencies": {
|
12
|
+
"@measured/puck": "{{puckVersion}}",
|
13
|
+
"classnames": "^2.3.2",
|
14
|
+
"next": "^13.4.6",
|
15
|
+
"react": "^18.2.0",
|
16
|
+
"react-dom": "^18.2.0"
|
17
|
+
},
|
18
|
+
"devDependencies": {
|
19
|
+
"@types/node": "^17.0.12",
|
20
|
+
"@types/react": "^18.0.22",
|
21
|
+
"@types/react-dom": "^18.0.7",
|
22
|
+
"eslint-config-custom": "*",
|
23
|
+
"typescript": "^4.5.3"
|
24
|
+
}
|
25
|
+
}
|
@@ -0,0 +1,25 @@
|
|
1
|
+
import type { Config } from "@measured/puck";
|
2
|
+
|
3
|
+
type Props = {
|
4
|
+
HeadingBlock: { title: string };
|
5
|
+
};
|
6
|
+
|
7
|
+
export const config: Config<Props> = {
|
8
|
+
components: {
|
9
|
+
HeadingBlock: {
|
10
|
+
fields: {
|
11
|
+
title: { type: "text" },
|
12
|
+
},
|
13
|
+
defaultProps: {
|
14
|
+
title: "Heading",
|
15
|
+
},
|
16
|
+
render: ({ title }) => (
|
17
|
+
<div style={{ padding: 64 }}>
|
18
|
+
<h1>{title}</h1>
|
19
|
+
</div>
|
20
|
+
),
|
21
|
+
},
|
22
|
+
},
|
23
|
+
};
|
24
|
+
|
25
|
+
export default config;
|
@@ -0,0 +1,20 @@
|
|
1
|
+
{
|
2
|
+
"$schema": "https://json.schemastore.org/tsconfig",
|
3
|
+
"display": "Default",
|
4
|
+
"compilerOptions": {
|
5
|
+
"composite": false,
|
6
|
+
"declaration": true,
|
7
|
+
"declarationMap": true,
|
8
|
+
"esModuleInterop": true,
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
10
|
+
"inlineSources": false,
|
11
|
+
"isolatedModules": true,
|
12
|
+
"moduleResolution": "node",
|
13
|
+
"noUnusedLocals": false,
|
14
|
+
"noUnusedParameters": false,
|
15
|
+
"preserveWatchOutput": true,
|
16
|
+
"skipLibCheck": true,
|
17
|
+
"strict": true
|
18
|
+
},
|
19
|
+
"exclude": ["node_modules"]
|
20
|
+
}
|
@@ -0,0 +1,21 @@
|
|
1
|
+
{
|
2
|
+
"$schema": "https://json.schemastore.org/tsconfig",
|
3
|
+
"display": "Next.js",
|
4
|
+
"extends": "./base.json",
|
5
|
+
"compilerOptions": {
|
6
|
+
"plugins": [{ "name": "next" }],
|
7
|
+
"allowJs": true,
|
8
|
+
"declaration": false,
|
9
|
+
"declarationMap": false,
|
10
|
+
"incremental": true,
|
11
|
+
"jsx": "preserve",
|
12
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
13
|
+
"module": "esnext",
|
14
|
+
"noEmit": true,
|
15
|
+
"resolveJsonModule": true,
|
16
|
+
"strict": false,
|
17
|
+
"target": "es5"
|
18
|
+
},
|
19
|
+
"include": ["src", "next-env.d.ts"],
|
20
|
+
"exclude": ["node_modules"]
|
21
|
+
}
|