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 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
+ }