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 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,4 @@
1
+ module.exports = {
2
+ root: true,
3
+ extends: ["custom"],
4
+ };
@@ -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,14 @@
1
+ import "@measured/puck/dist/index.css";
2
+ import "./styles.css";
3
+
4
+ export default function RootLayout({
5
+ children,
6
+ }: {
7
+ children: React.ReactNode;
8
+ }) {
9
+ return (
10
+ <html lang="en">
11
+ <body>{children}</body>
12
+ </html>
13
+ );
14
+ }
@@ -0,0 +1 @@
1
+ export { default, generateMetadata } from "./[...puckPath]/page";
@@ -0,0 +1,5 @@
1
+ html,
2
+ body {
3
+ margin: 0;
4
+ padding: 0;
5
+ }
@@ -0,0 +1,5 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+
4
+ // NOTE: This file should not be edited
5
+ // see https://nextjs.org/docs/basic-features/typescript for more information.
@@ -0,0 +1,4 @@
1
+ module.exports = {
2
+ reactStrictMode: true,
3
+ transpilePackages: ["ui"],
4
+ };
@@ -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
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "extends": "./tsconfig/nextjs.json",
3
+ "compilerOptions": {
4
+ "plugins": [{ "name": "next" }]
5
+ },
6
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
7
+ "exclude": ["node_modules"]
8
+ }