create-puck-app 0.12.0-canary.e38b98f → 0.12.0-canary.f882878

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. package/index.js +16 -14
  2. package/package.json +8 -2
  3. package/templates/gitignore +8 -0
  4. package/templates/next/.eslintrc.js +4 -0
  5. package/templates/next/app/[...puckPath]/client.tsx +9 -0
  6. package/templates/next/app/[...puckPath]/page.tsx +47 -0
  7. package/templates/next/app/layout.tsx +14 -0
  8. package/templates/next/app/page.tsx +1 -0
  9. package/templates/next/app/puck/[...puckPath]/client.tsx +20 -0
  10. package/templates/next/app/puck/[...puckPath]/page.tsx +39 -0
  11. package/templates/next/app/puck/api/route.ts +25 -0
  12. package/templates/next/app/puck/page.tsx +1 -0
  13. package/templates/next/app/styles.css +5 -0
  14. package/templates/next/database.json +1 -0
  15. package/templates/next/gitignore +34 -0
  16. package/templates/next/lib/get-page.ts +11 -0
  17. package/templates/next/middleware.ts +27 -0
  18. package/templates/next/next-env.d.ts +5 -0
  19. package/templates/next/next.config.js +4 -0
  20. package/templates/next/puck.config.tsx +25 -0
  21. package/templates/next/tsconfig/base.json +20 -0
  22. package/templates/next/tsconfig/nextjs.json +21 -0
  23. package/templates/next/tsconfig.json +8 -0
  24. package/templates/remix/.eslintrc.cjs +4 -0
  25. package/templates/remix/README.md +42 -0
  26. package/templates/remix/app/entry.client.tsx +18 -0
  27. package/templates/remix/app/entry.server.tsx +137 -0
  28. package/templates/remix/app/models/page.server.ts +27 -0
  29. package/templates/remix/app/root.tsx +43 -0
  30. package/templates/remix/app/routes/$puckPath.tsx +2 -0
  31. package/templates/remix/app/routes/$puckPath_.edit.tsx +5 -0
  32. package/templates/remix/app/routes/_index.tsx +49 -0
  33. package/templates/remix/app/routes/edit.tsx +70 -0
  34. package/templates/remix/database.json +1 -0
  35. package/templates/remix/gitignore +6 -0
  36. package/templates/remix/package.json.hbs +29 -0
  37. package/templates/remix/public/favicon.ico +0 -0
  38. package/templates/remix/puck.config.tsx +25 -0
  39. package/templates/remix/remix.config.js +12 -0
  40. package/templates/remix/remix.env.d.ts +2 -0
  41. package/templates/remix/tsconfig.json +22 -0
  42. package/scripts/generate.js +0 -72
package/index.js CHANGED
@@ -103,7 +103,7 @@ program
103
103
  return;
104
104
  }
105
105
 
106
- await fs.mkdirSync(appName);
106
+ fs.mkdirSync(appName);
107
107
 
108
108
  const packageManager = !!options.useNpm
109
109
  ? "npm"
@@ -130,7 +130,7 @@ program
130
130
  let data;
131
131
 
132
132
  if (path.extname(filePath) === ".hbs") {
133
- const templateString = await fs.readFileSync(filePath, "utf-8");
133
+ const templateString = fs.readFileSync(filePath, "utf-8");
134
134
 
135
135
  const template = Handlebars.compile(templateString);
136
136
  data = template({
@@ -139,14 +139,14 @@ program
139
139
  puckVersion: `^${packageJson.version}`,
140
140
  });
141
141
  } else {
142
- data = await fs.readFileSync(filePath, "utf-8");
142
+ data = fs.readFileSync(filePath, "utf-8");
143
143
  }
144
144
 
145
145
  const dir = path.dirname(targetPath);
146
146
 
147
- await fs.mkdirSync(dir, { recursive: true });
147
+ fs.mkdirSync(dir, { recursive: true });
148
148
 
149
- await fs.writeFileSync(targetPath, data);
149
+ fs.writeFileSync(targetPath, data);
150
150
  }
151
151
 
152
152
  if (packageManager === "yarn") {
@@ -159,22 +159,24 @@ program
159
159
 
160
160
  try {
161
161
  inGitRepo =
162
- execSync("git status", {
163
- cwd: appPath,
164
- })
162
+ execSync("git status", { cwd: appPath })
165
163
  .toString()
166
164
  .indexOf("fatal:") !== 0;
167
165
  } catch {}
168
166
 
169
167
  // Only commit if this is a new repo
170
168
  if (!inGitRepo) {
171
- execSync("git init", { cwd: appPath, stdio: "inherit" });
169
+ try {
170
+ execSync("git init", { cwd: appPath, stdio: "inherit" });
172
171
 
173
- execSync("git add .", { cwd: appPath, stdio: "inherit" });
174
- execSync("git commit -m 'build(puck): generate app'", {
175
- cwd: appPath,
176
- stdio: "inherit",
177
- });
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
+ } catch (error) {
178
+ console.log("Failed to commit git changes");
179
+ }
178
180
  }
179
181
  })
180
182
  .parse(process.argv);
package/package.json CHANGED
@@ -1,15 +1,21 @@
1
1
  {
2
2
  "name": "create-puck-app",
3
- "version": "0.12.0-canary.e38b98f",
3
+ "version": "0.12.0-canary.f882878",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "type": "module",
7
7
  "bin": {
8
8
  "create-puck-app": "./index.js"
9
9
  },
10
+ "files": [
11
+ "templates",
12
+ "index.js"
13
+ ],
10
14
  "scripts": {
11
15
  "generate": "node scripts/generate.js",
12
- "prepublishOnly": "yarn generate"
16
+ "prepublishOnly": "yarn generate",
17
+ "removeGitignore": "mv templates/.gitignore templates/gitignore",
18
+ "restoreGitignore": "mv templates/gitignore templates/.gitignore"
13
19
  },
14
20
  "dependencies": {
15
21
  "commander": "^10.0.1",
@@ -0,0 +1,8 @@
1
+ # # Ignore everything
2
+ *.*
3
+
4
+ # # Explicitly ignore gitignore without extension
5
+ gitignore
6
+
7
+ # # Except for handlebars files
8
+ !*.hbs
@@ -0,0 +1,4 @@
1
+ module.exports = {
2
+ root: true,
3
+ extends: ["custom"],
4
+ };
@@ -0,0 +1,9 @@
1
+ "use client";
2
+
3
+ import type { Data } from "@measured/puck";
4
+ import { Render } from "@measured/puck";
5
+ import config from "../../puck.config";
6
+
7
+ export function Client({ data }: { data: Data }) {
8
+ return <Render config={config} data={data} />;
9
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * This file implements a catch-all route that renders the user-facing pages
3
+ * generated by Puck. For any route visited (with exception of other hardcoded
4
+ * pages in /app), it will check your database (via `getPage`) for a Puck page
5
+ * and render it using <Render>.
6
+ *
7
+ * All routes produced by this page are statically rendered using incremental
8
+ * static site generation. After the first visit, the page will be cached as
9
+ * a static file. Subsequent visits will receive the cache. Publishing a page
10
+ * will invalidate the cache as the page is written in /api/puck/route.ts
11
+ */
12
+
13
+ import { Client } from "./client";
14
+ import { notFound } from "next/navigation";
15
+ import { Metadata } from "next";
16
+ import { getPage } from "../../lib/get-page";
17
+
18
+ export async function generateMetadata({
19
+ params: { puckPath = [] },
20
+ }: {
21
+ params: { puckPath: string[] };
22
+ }): Promise<Metadata> {
23
+ const path = `/${puckPath.join("/")}`;
24
+
25
+ return {
26
+ title: getPage(path)?.root.title,
27
+ };
28
+ }
29
+
30
+ export default async function Page({
31
+ params: { puckPath = [] },
32
+ }: {
33
+ params: { puckPath: string[] };
34
+ }) {
35
+ const path = `/${puckPath.join("/")}`;
36
+ const data = getPage(path);
37
+
38
+ if (!data) {
39
+ return notFound();
40
+ }
41
+
42
+ return <Client data={data} />;
43
+ }
44
+
45
+ // Force Next.js to produce static pages: https://nextjs.org/docs/app/api-reference/file-conventions/route-segment-config#dynamic
46
+ // Delete this if you need dynamic rendering, such as access to headers or cookies
47
+ export const dynamic = "force-static";
@@ -0,0 +1,14 @@
1
+ import "@measured/puck/puck.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,20 @@
1
+ "use client";
2
+
3
+ import type { Data } from "@measured/puck";
4
+ import { Puck } from "@measured/puck";
5
+ import config from "../../../puck.config";
6
+
7
+ export function Client({ path, data }: { path: string; data: Data }) {
8
+ return (
9
+ <Puck
10
+ config={config}
11
+ data={data}
12
+ onPublish={async (data: Data) => {
13
+ await fetch("/puck/api", {
14
+ method: "post",
15
+ body: JSON.stringify({ data, path }),
16
+ });
17
+ }}
18
+ />
19
+ );
20
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * This file implements a *magic* catch-all route that renders the Puck editor.
3
+ *
4
+ * This route exposes /puck/[...puckPath], but is disabled by middleware.ts. The middleware
5
+ * then rewrites all URL requests ending in `/edit` to this route, allowing you to visit any
6
+ * page in your application and add /edit to the end to spin up a Puck editor.
7
+ *
8
+ * This approach enables public pages to be statically rendered whilst the /puck route can
9
+ * remain dynamic.
10
+ *
11
+ * NB this route is public, and you will need to add authentication
12
+ */
13
+
14
+ import { Client } from "./client";
15
+ import { Metadata } from "next";
16
+ import { getPage } from "../../../lib/get-page";
17
+
18
+ export async function generateMetadata({
19
+ params: { puckPath = [] },
20
+ }: {
21
+ params: { puckPath: string[] };
22
+ }): Promise<Metadata> {
23
+ const path = `/${puckPath.join("/")}`;
24
+
25
+ return {
26
+ title: "Puck: " + path,
27
+ };
28
+ }
29
+
30
+ export default async function Page({
31
+ params: { puckPath = [] },
32
+ }: {
33
+ params: { puckPath: string[] };
34
+ }) {
35
+ const path = `/${puckPath.join("/")}`;
36
+ const data = getPage(path);
37
+
38
+ return <Client path={path} data={data} />;
39
+ }
@@ -0,0 +1,25 @@
1
+ import { revalidatePath } from "next/cache";
2
+ import { NextResponse } from "next/server";
3
+ import fs from "fs";
4
+
5
+ export async function POST(request: Request) {
6
+ const payload = await request.json();
7
+
8
+ const existingData = JSON.parse(
9
+ fs.existsSync("database.json")
10
+ ? fs.readFileSync("database.json", "utf-8")
11
+ : "{}"
12
+ );
13
+
14
+ const updatedData = {
15
+ ...existingData,
16
+ [payload.path]: payload.data,
17
+ };
18
+
19
+ fs.writeFileSync("database.json", JSON.stringify(updatedData));
20
+
21
+ // Purge Next.js cache
22
+ revalidatePath(payload.path);
23
+
24
+ return NextResponse.json({ status: "ok" });
25
+ }
@@ -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 @@
1
+ {"/":{"content":[{"type":"HeadingBlock","props":{"title":"Edit this page by adding /edit to the end of the URL","id":"HeadingBlock-1694032984497"}}],"root":{"props": {"title":""}}}}
@@ -0,0 +1,34 @@
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.js
7
+
8
+ # testing
9
+ /coverage
10
+
11
+ # next.js
12
+ /.next/
13
+ /out/
14
+
15
+ # production
16
+ /build
17
+
18
+ # misc
19
+ .DS_Store
20
+ *.pem
21
+
22
+ # debug
23
+ npm-debug.log*
24
+ yarn-debug.log*
25
+ yarn-error.log*
26
+
27
+ # local env files
28
+ .env.local
29
+ .env.development.local
30
+ .env.test.local
31
+ .env.production.local
32
+
33
+ # vercel
34
+ .vercel
@@ -0,0 +1,11 @@
1
+ import { Data } from "@measured/puck";
2
+ import fs from "fs";
3
+
4
+ // Replace with call to your database
5
+ export const getPage = (path: string) => {
6
+ const allData: Record<string, Data> | null = fs.existsSync("database.json")
7
+ ? JSON.parse(fs.readFileSync("database.json", "utf-8"))
8
+ : null;
9
+
10
+ return allData ? allData[path] : null;
11
+ };
@@ -0,0 +1,27 @@
1
+ import { NextResponse } from "next/server";
2
+
3
+ import type { NextRequest } from "next/server";
4
+
5
+ export async function middleware(req: NextRequest) {
6
+ const res = NextResponse.next();
7
+
8
+ if (req.method === "GET") {
9
+ // Rewrite routes that match "/[...puckPath]/edit" to "/puck/[...puckPath]"
10
+ if (req.nextUrl.pathname.endsWith("/edit")) {
11
+ const pathWithoutEdit = req.nextUrl.pathname.slice(
12
+ 0,
13
+ req.nextUrl.pathname.length - 5
14
+ );
15
+ const pathWithEditPrefix = `/puck${pathWithoutEdit}`;
16
+
17
+ return NextResponse.rewrite(new URL(pathWithEditPrefix, req.url));
18
+ }
19
+
20
+ // Disable "/puck/[...puckPath]"
21
+ if (req.nextUrl.pathname.startsWith("/puck")) {
22
+ return NextResponse.redirect(new URL("/", req.url));
23
+ }
24
+ }
25
+
26
+ return res;
27
+ }
@@ -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
+ 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
+ }
@@ -0,0 +1,4 @@
1
+ /** @type {import('eslint').Linter.Config} */
2
+ module.exports = {
3
+ extends: ["@remix-run/eslint-config", "@remix-run/eslint-config/node"],
4
+ };
@@ -0,0 +1,42 @@
1
+ # `remix` recipe
2
+
3
+ The `remix` recipe showcases a Remix Run app with Puck, using it to provide an authoring tool for any root-level route in your Remix app.
4
+
5
+ ## Demonstrates
6
+
7
+ - Remix Run V2 implementation
8
+ - JSON database implementation with HTTP API
9
+ - Dynamic routes to use puck for any root-level route on the platform
10
+ - Option to disable client-side JavaScript for Puck pages
11
+
12
+ ## Usage
13
+
14
+ Run the generator and enter `next` when prompted
15
+
16
+ ```
17
+ npx create-puck-app my-app
18
+ ```
19
+
20
+ Start the server
21
+
22
+ ```
23
+ yarn dev
24
+ ```
25
+
26
+ Navigate to the homepage at https://localhost:3000. To edit the homepage, access the Puck editor at https://localhost:3000/edit.
27
+
28
+ You can do this for any **base** 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.
29
+
30
+ ## Using this recipe
31
+
32
+ To adopt this recipe you will need to:
33
+
34
+ - **IMPORTANT** Add authentication to `/edit` routes. This can be done by modifying the example routes `/app/routes/_index.tsx` and `/app/routes/edit.tsx` or the example model in `/app/models/page.server.ts`. **If you don't do this, Puck will be completely public.**
35
+ - Integrate your database into the API calls in `/app/models/page.server.ts`
36
+ - Implement a custom puck configuration in `puck.config.tsx`
37
+
38
+ By default, this recipe will have JavaScript enable on all routes - like a usual react app. If you know that your Puck content doesn't need react, then you can disable JS uncommenting the relevant code in `/app/root.tsx` and the example route `/app/routes/_index.tsx`. Check the network tab for no JS downloads, and verify that the page still works.
39
+
40
+ ## License
41
+
42
+ MIT © [Measured Co.](https://github.com/measuredco)
@@ -0,0 +1,18 @@
1
+ /**
2
+ * By default, Remix will handle hydrating your app on the client for you.
3
+ * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
4
+ * For more information, see https://remix.run/file-conventions/entry.client
5
+ */
6
+
7
+ import { RemixBrowser } from "@remix-run/react";
8
+ import { startTransition, StrictMode } from "react";
9
+ import { hydrateRoot } from "react-dom/client";
10
+
11
+ startTransition(() => {
12
+ hydrateRoot(
13
+ document,
14
+ <StrictMode>
15
+ <RemixBrowser />
16
+ </StrictMode>
17
+ );
18
+ });
@@ -0,0 +1,137 @@
1
+ /**
2
+ * By default, Remix will handle generating the HTTP Response for you.
3
+ * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨
4
+ * For more information, see https://remix.run/file-conventions/entry.server
5
+ */
6
+
7
+ import { PassThrough } from "node:stream";
8
+
9
+ import type { AppLoadContext, EntryContext } from "@remix-run/node";
10
+ import { createReadableStreamFromReadable } from "@remix-run/node";
11
+ import { RemixServer } from "@remix-run/react";
12
+ import isbot from "isbot";
13
+ import { renderToPipeableStream } from "react-dom/server";
14
+
15
+ const ABORT_DELAY = 5_000;
16
+
17
+ export default function handleRequest(
18
+ request: Request,
19
+ responseStatusCode: number,
20
+ responseHeaders: Headers,
21
+ remixContext: EntryContext,
22
+ loadContext: AppLoadContext
23
+ ) {
24
+ return isbot(request.headers.get("user-agent"))
25
+ ? handleBotRequest(
26
+ request,
27
+ responseStatusCode,
28
+ responseHeaders,
29
+ remixContext
30
+ )
31
+ : handleBrowserRequest(
32
+ request,
33
+ responseStatusCode,
34
+ responseHeaders,
35
+ remixContext
36
+ );
37
+ }
38
+
39
+ function handleBotRequest(
40
+ request: Request,
41
+ responseStatusCode: number,
42
+ responseHeaders: Headers,
43
+ remixContext: EntryContext
44
+ ) {
45
+ return new Promise((resolve, reject) => {
46
+ let shellRendered = false;
47
+ const { pipe, abort } = renderToPipeableStream(
48
+ <RemixServer
49
+ context={remixContext}
50
+ url={request.url}
51
+ abortDelay={ABORT_DELAY}
52
+ />,
53
+ {
54
+ onAllReady() {
55
+ shellRendered = true;
56
+ const body = new PassThrough();
57
+ const stream = createReadableStreamFromReadable(body);
58
+
59
+ responseHeaders.set("Content-Type", "text/html");
60
+
61
+ resolve(
62
+ new Response(stream, {
63
+ headers: responseHeaders,
64
+ status: responseStatusCode,
65
+ })
66
+ );
67
+
68
+ pipe(body);
69
+ },
70
+ onShellError(error: unknown) {
71
+ reject(error);
72
+ },
73
+ onError(error: unknown) {
74
+ responseStatusCode = 500;
75
+ // Log streaming rendering errors from inside the shell. Don't log
76
+ // errors encountered during initial shell rendering since they'll
77
+ // reject and get logged in handleDocumentRequest.
78
+ if (shellRendered) {
79
+ console.error(error);
80
+ }
81
+ },
82
+ }
83
+ );
84
+
85
+ setTimeout(abort, ABORT_DELAY);
86
+ });
87
+ }
88
+
89
+ function handleBrowserRequest(
90
+ request: Request,
91
+ responseStatusCode: number,
92
+ responseHeaders: Headers,
93
+ remixContext: EntryContext
94
+ ) {
95
+ return new Promise((resolve, reject) => {
96
+ let shellRendered = false;
97
+ const { pipe, abort } = renderToPipeableStream(
98
+ <RemixServer
99
+ context={remixContext}
100
+ url={request.url}
101
+ abortDelay={ABORT_DELAY}
102
+ />,
103
+ {
104
+ onShellReady() {
105
+ shellRendered = true;
106
+ const body = new PassThrough();
107
+ const stream = createReadableStreamFromReadable(body);
108
+
109
+ responseHeaders.set("Content-Type", "text/html");
110
+
111
+ resolve(
112
+ new Response(stream, {
113
+ headers: responseHeaders,
114
+ status: responseStatusCode,
115
+ })
116
+ );
117
+
118
+ pipe(body);
119
+ },
120
+ onShellError(error: unknown) {
121
+ reject(error);
122
+ },
123
+ onError(error: unknown) {
124
+ responseStatusCode = 500;
125
+ // Log streaming rendering errors from inside the shell. Don't log
126
+ // errors encountered during initial shell rendering since they'll
127
+ // reject and get logged in handleDocumentRequest.
128
+ if (shellRendered) {
129
+ console.error(error);
130
+ }
131
+ },
132
+ }
133
+ );
134
+
135
+ setTimeout(abort, ABORT_DELAY);
136
+ });
137
+ }
@@ -0,0 +1,27 @@
1
+ import { Data } from "@measured/puck";
2
+ import fs from "fs";
3
+
4
+ // Replace with call to your database
5
+ export const getPage = (path: string) => {
6
+ const allData: Record<string, Data> | null = fs.existsSync("database.json")
7
+ ? JSON.parse(fs.readFileSync("database.json", "utf-8"))
8
+ : null;
9
+
10
+ return allData ? allData[path] : null;
11
+ };
12
+
13
+ // Replace with call to your database
14
+ export const setPage = (path: string, data: Data) => {
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
+ [path]: data,
24
+ };
25
+
26
+ fs.writeFileSync("database.json", JSON.stringify(updatedData));
27
+ };
@@ -0,0 +1,43 @@
1
+ import { cssBundleHref } from "@remix-run/css-bundle";
2
+ import type { LinksFunction } from "@remix-run/node";
3
+ import {
4
+ Links,
5
+ LiveReload,
6
+ Meta,
7
+ Outlet,
8
+ Scripts,
9
+ ScrollRestoration,
10
+ // useMatches,
11
+ } from "@remix-run/react";
12
+
13
+ export const links: LinksFunction = () => [
14
+ ...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []),
15
+ ];
16
+
17
+ export default function App() {
18
+ /**
19
+ * Disable client-side JS - Optional
20
+ * @see https://remix.run/docs/en/main/guides/disabling-javascript
21
+ */
22
+ // const matches = useMatches();
23
+ // const includeScripts = matches.some((match) => match.handle?.hydrate);
24
+
25
+ return (
26
+ <html lang="en">
27
+ <head>
28
+ <meta charSet="utf-8" />
29
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
30
+ <Meta />
31
+ <Links />
32
+ </head>
33
+ <body>
34
+ <Outlet />
35
+ <ScrollRestoration />
36
+ {/* Conditionally render scripts - Optional */}
37
+ {/* {includeScripts ? <Scripts /> : null} */}
38
+ <Scripts />
39
+ <LiveReload />
40
+ </body>
41
+ </html>
42
+ );
43
+ }
@@ -0,0 +1,2 @@
1
+ export { default } from "./_index";
2
+ export * from "./_index";
@@ -0,0 +1,5 @@
1
+ export { default } from "./edit";
2
+ // I think a bug in remix means loader needs to be explicitly exported here
3
+ export { action, loader } from "./edit";
4
+ // For meta and links etc.
5
+ export * from "./edit";
@@ -0,0 +1,49 @@
1
+ import { Render, type Config } from "@measured/puck";
2
+ import type { LoaderFunctionArgs, MetaFunction } from "@remix-run/node";
3
+ import { json } from "@remix-run/node";
4
+ import { useLoaderData } from "@remix-run/react";
5
+
6
+ import puckConfig from "../../puck.config";
7
+ import { getPage } from "~/models/page.server";
8
+
9
+ /**
10
+ * Disable client-side JS - Optional
11
+ * If you know that your Puck content doesn't need react.
12
+ * Then you can disable JS for this route.
13
+ * @see https://remix.run/docs/en/main/guides/disabling-javascript
14
+ */
15
+
16
+ // export const handle = { hydrate: false };
17
+
18
+ export const loader = async ({ params }: LoaderFunctionArgs) => {
19
+ // Get path, and default to slash for root path.
20
+ const puckPath = params.puckPath || "/";
21
+ // Get puckData for this path, this could be a database call.
22
+ const puckData = getPage(puckPath);
23
+ if (!puckData) {
24
+ throw new Response(null, {
25
+ status: 404,
26
+ statusText: "Not Found",
27
+ });
28
+ }
29
+ // Return the data.
30
+ return json({ puckData });
31
+ };
32
+
33
+ export const meta: MetaFunction<typeof loader> = ({ data }) => {
34
+ const title = data?.puckData?.root?.title || "Page";
35
+
36
+ return [{ title }];
37
+ };
38
+
39
+ export default function Page() {
40
+ const { puckData } = useLoaderData<typeof loader>();
41
+
42
+ /**
43
+ * TypeStript error
44
+ * Type 'Config<Props>' is not assignable to type 'Config'. Use 'as Config' for now.
45
+ * @see https://github.com/measuredco/puck/issues/185
46
+ */
47
+
48
+ return <Render config={puckConfig as Config} data={puckData} />;
49
+ }
@@ -0,0 +1,70 @@
1
+ import { Puck, type Data, type Config } from "@measured/puck";
2
+ import styles from "@measured/puck/puck.css";
3
+ import type {
4
+ ActionFunctionArgs,
5
+ LinksFunction,
6
+ LoaderFunctionArgs,
7
+ MetaFunction,
8
+ } from "@remix-run/node";
9
+ import { json } from "@remix-run/node";
10
+ import { useLoaderData, useSubmit } from "@remix-run/react";
11
+ import invariant from "tiny-invariant";
12
+
13
+ import puckConfig from "../../puck.config";
14
+ import { getPage, setPage } from "~/models/page.server";
15
+
16
+ export const action = async ({ params, request }: ActionFunctionArgs) => {
17
+ const puckPath = params.puckPath || "/";
18
+ const formData = await request.formData();
19
+ const puckData = formData.get("puckData");
20
+
21
+ invariant(puckData, "Missing data");
22
+ invariant(typeof puckData === "string", "Invalid data");
23
+
24
+ setPage(puckPath, JSON.parse(puckData));
25
+
26
+ return json({ ok: true });
27
+ };
28
+
29
+ export const links: LinksFunction = () => [
30
+ { rel: "stylesheet", href: styles, id: "puck-css" },
31
+ ];
32
+
33
+ export const loader = async ({ params }: LoaderFunctionArgs) => {
34
+ const puckPath = params.puckPath || "/";
35
+ const initialData = getPage(puckPath) || {
36
+ content: [],
37
+ root: {},
38
+ };
39
+ return json({ puckPath, initialData });
40
+ };
41
+
42
+ export const meta: MetaFunction<typeof loader> = ({ data }) => {
43
+ const title = data?.initialData?.root?.title || "Untitled page";
44
+
45
+ return [{ title: `Editing: ${title}` }];
46
+ };
47
+
48
+ export default function Edit() {
49
+ const { initialData } = useLoaderData<typeof loader>();
50
+ const submit = useSubmit();
51
+
52
+ /**
53
+ * TypeStript error
54
+ * Type 'Config<Props>' is not assignable to type 'Config'. Use 'as Config' for now.
55
+ * @see https://github.com/measuredco/puck/issues/185
56
+ */
57
+
58
+ return (
59
+ <Puck
60
+ config={puckConfig as Config}
61
+ data={initialData}
62
+ onPublish={async (data: Data) => {
63
+ // Use form data here because it's the usual remix way.
64
+ let formData = new FormData();
65
+ formData.append("puckData", JSON.stringify(data));
66
+ submit(formData, { method: "post" });
67
+ }}
68
+ />
69
+ );
70
+ }
@@ -0,0 +1 @@
1
+ {"/":{"content":[{"type":"HeadingBlock","props":{"title":"Edit this page by adding /edit to the end of the URL","id":"HeadingBlock-1694032984497"}}],"root":{"props": {"title":""}}}}
@@ -0,0 +1,6 @@
1
+ node_modules
2
+
3
+ /.cache
4
+ /build
5
+ /public/build
6
+ .env
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "{{appName}}",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "build": "remix build",
7
+ "dev": "remix dev --manual",
8
+ "start": "remix-serve ./build/index.js",
9
+ "typecheck": "tsc"
10
+ },
11
+ "dependencies": {
12
+ "@measured/puck": "{{puckVersion}}",
13
+ "@remix-run/css-bundle": "^2.2.0",
14
+ "@remix-run/node": "^2.2.0",
15
+ "@remix-run/react": "^2.2.0",
16
+ "@remix-run/serve": "^2.2.0",
17
+ "isbot": "^3.6.8",
18
+ "react": "^18.2.0",
19
+ "react-dom": "^18.2.0"
20
+ },
21
+ "devDependencies": {
22
+ "@remix-run/dev": "^2.2.0",
23
+ "@remix-run/eslint-config": "^2.2.0",
24
+ "@types/react": "^18.2.20",
25
+ "@types/react-dom": "^18.2.7",
26
+ "eslint": "^8.38.0",
27
+ "typescript": "^5.1.6"
28
+ }
29
+ }
@@ -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,12 @@
1
+ /** @type {import('@remix-run/dev').AppConfig} */
2
+ export default {
3
+ ignoredRouteFiles: ["**/.*"],
4
+ // appDirectory: "app",
5
+ // assetsBuildDirectory: "public/build",
6
+ // publicPath: "/build/",
7
+ // serverBuildPath: "build/index.js",
8
+ /**
9
+ * @see https://github.com/measuredco/puck/issues/112
10
+ */
11
+ browserNodeBuiltinsPolyfill: { modules: { crypto: true } },
12
+ };
@@ -0,0 +1,2 @@
1
+ /// <reference types="@remix-run/dev" />
2
+ /// <reference types="@remix-run/node" />
@@ -0,0 +1,22 @@
1
+ {
2
+ "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"],
3
+ "compilerOptions": {
4
+ "lib": ["DOM", "DOM.Iterable", "ES2022"],
5
+ "isolatedModules": true,
6
+ "esModuleInterop": true,
7
+ "jsx": "react-jsx",
8
+ "moduleResolution": "Bundler",
9
+ "resolveJsonModule": true,
10
+ "target": "ES2022",
11
+ "strict": true,
12
+ "allowJs": true,
13
+ "forceConsistentCasingInFileNames": true,
14
+ "baseUrl": ".",
15
+ "paths": {
16
+ "~/*": ["./app/*"]
17
+ },
18
+
19
+ // Remix takes care of building everything in `remix build`.
20
+ "noEmit": true
21
+ }
22
+ }
@@ -1,72 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import fs from "fs";
4
- import path from "path";
5
- import { glob } from "glob";
6
- import { dirname } from "path";
7
- import { fileURLToPath } from "url";
8
-
9
- const verbose = false;
10
-
11
- const run = async () => {
12
- const __filename = fileURLToPath(import.meta.url);
13
- const __dirname = dirname(__filename);
14
-
15
- // Copy template files to the new directory
16
- const recipePath = path.join(__dirname, "../../../recipes");
17
- const templatePath = path.join(__dirname, "../templates");
18
-
19
- if (!fs.existsSync(recipePath)) {
20
- console.error(`No recipe directory could be found at ${recipePath}.`);
21
- return;
22
- }
23
-
24
- if (!fs.existsSync(templatePath)) {
25
- console.error(`No template directory could be found at ${templatePath}.`);
26
- return;
27
- }
28
-
29
- // Copy recipe files
30
- const recipeFiles = glob.sync(`**/*`, {
31
- cwd: recipePath,
32
- nodir: true,
33
- dot: true,
34
- });
35
-
36
- console.warn(
37
- `⚠️ The following files use handlebars templates. Please manually update them:`
38
- );
39
-
40
- let counter = 0;
41
-
42
- for (const recipeFile of recipeFiles) {
43
- const filePath = path.join(recipePath, recipeFile);
44
-
45
- const targetPath = filePath
46
- .replace(recipePath, templatePath)
47
- .replace(".gitignore", "gitignore"); // rename .gitignore to gitignore so NPM publish doesn't ignore it
48
-
49
- // Don't copy file if it's templated by handlebars
50
- if (fs.existsSync(`${targetPath}.hbs`)) {
51
- console.warn(`- ${recipeFile}`);
52
- } else {
53
- if (verbose) {
54
- console.log(`Copying ${filePath} -> ${targetPath}`);
55
- }
56
-
57
- const data = await fs.readFileSync(filePath, "utf-8");
58
-
59
- const dir = path.dirname(targetPath);
60
-
61
- await fs.mkdirSync(dir, { recursive: true });
62
-
63
- await fs.writeFileSync(targetPath, data);
64
-
65
- counter += 1;
66
- }
67
- }
68
-
69
- console.log(`Copied ${counter} files into generator!`);
70
- };
71
-
72
- await run();