create-puck-app 0.3.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/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
|
+
}
|