create-puck-app 0.12.0-canary.e38b98f → 0.12.0
Sign up to get free protection for your applications and to get access to all the features.
- package/index.js +16 -14
- package/package.json +8 -2
- package/templates/gitignore +8 -0
- package/templates/next/.eslintrc.js +4 -0
- package/templates/next/app/[...puckPath]/client.tsx +9 -0
- package/templates/next/app/[...puckPath]/page.tsx +47 -0
- package/templates/next/app/layout.tsx +14 -0
- package/templates/next/app/page.tsx +1 -0
- package/templates/next/app/puck/[...puckPath]/client.tsx +20 -0
- package/templates/next/app/puck/[...puckPath]/page.tsx +39 -0
- package/templates/next/app/puck/api/route.ts +25 -0
- package/templates/next/app/puck/page.tsx +1 -0
- package/templates/next/app/styles.css +5 -0
- package/templates/next/database.json +1 -0
- package/templates/next/gitignore +34 -0
- package/templates/next/lib/get-page.ts +11 -0
- package/templates/next/middleware.ts +27 -0
- package/templates/next/next-env.d.ts +5 -0
- package/templates/next/next.config.js +4 -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/templates/remix/.eslintrc.cjs +4 -0
- package/templates/remix/README.md +46 -0
- package/templates/remix/app/entry.client.tsx +18 -0
- package/templates/remix/app/entry.server.tsx +137 -0
- package/templates/remix/app/models/page.server.ts +27 -0
- package/templates/remix/app/root.tsx +33 -0
- package/templates/remix/app/routes/$puckPath.tsx +2 -0
- package/templates/remix/app/routes/$puckPath_.edit.tsx +5 -0
- package/templates/remix/app/routes/_index.tsx +34 -0
- package/templates/remix/app/routes/edit.tsx +64 -0
- package/templates/remix/database.json +1 -0
- package/templates/remix/gitignore +6 -0
- package/templates/remix/package.json.hbs +34 -0
- package/templates/remix/public/favicon.ico +0 -0
- package/templates/remix/puck.config.tsx +25 -0
- package/templates/remix/remix.config.js +12 -0
- package/templates/remix/remix.env.d.ts +2 -0
- package/templates/remix/tsconfig.json +22 -0
- package/scripts/generate.js +0 -72
package/index.js
CHANGED
@@ -103,7 +103,7 @@ program
|
|
103
103
|
return;
|
104
104
|
}
|
105
105
|
|
106
|
-
|
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 =
|
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 =
|
142
|
+
data = fs.readFileSync(filePath, "utf-8");
|
143
143
|
}
|
144
144
|
|
145
145
|
const dir = path.dirname(targetPath);
|
146
146
|
|
147
|
-
|
147
|
+
fs.mkdirSync(dir, { recursive: true });
|
148
148
|
|
149
|
-
|
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
|
-
|
169
|
+
try {
|
170
|
+
execSync("git init", { cwd: appPath, stdio: "inherit" });
|
172
171
|
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
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
|
3
|
+
"version": "0.12.0",
|
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,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 @@
|
|
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 @@
|
|
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,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,46 @@
|
|
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
|
+
## Disabling JavaScript
|
41
|
+
|
42
|
+
This recipe can be adapted to disable JavaScript. See the [Remix docs](https://remix.run/docs/en/main/guides/disabling-javascript) for steps on how to do this.
|
43
|
+
|
44
|
+
## License
|
45
|
+
|
46
|
+
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,33 @@
|
|
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
|
+
} from "@remix-run/react";
|
11
|
+
|
12
|
+
export const links: LinksFunction = () => [
|
13
|
+
...(cssBundleHref ? [{ rel: "stylesheet", href: cssBundleHref }] : []),
|
14
|
+
];
|
15
|
+
|
16
|
+
export default function App() {
|
17
|
+
return (
|
18
|
+
<html lang="en">
|
19
|
+
<head>
|
20
|
+
<meta charSet="utf-8" />
|
21
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
22
|
+
<Meta />
|
23
|
+
<Links />
|
24
|
+
</head>
|
25
|
+
<body>
|
26
|
+
<Outlet />
|
27
|
+
<ScrollRestoration />
|
28
|
+
<Scripts />
|
29
|
+
<LiveReload />
|
30
|
+
</body>
|
31
|
+
</html>
|
32
|
+
);
|
33
|
+
}
|
@@ -0,0 +1,34 @@
|
|
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
|
+
export const loader = async ({ params }: LoaderFunctionArgs) => {
|
10
|
+
// Get path, and default to slash for root path.
|
11
|
+
const puckPath = params.puckPath || "/";
|
12
|
+
// Get puckData for this path, this could be a database call.
|
13
|
+
const puckData = getPage(puckPath);
|
14
|
+
if (!puckData) {
|
15
|
+
throw new Response(null, {
|
16
|
+
status: 404,
|
17
|
+
statusText: "Not Found",
|
18
|
+
});
|
19
|
+
}
|
20
|
+
// Return the data.
|
21
|
+
return json({ puckData });
|
22
|
+
};
|
23
|
+
|
24
|
+
export const meta: MetaFunction<typeof loader> = ({ data }) => {
|
25
|
+
const title = data?.puckData?.root?.title || "Page";
|
26
|
+
|
27
|
+
return [{ title }];
|
28
|
+
};
|
29
|
+
|
30
|
+
export default function Page() {
|
31
|
+
const { puckData } = useLoaderData<typeof loader>();
|
32
|
+
|
33
|
+
return <Render config={puckConfig as Config} data={puckData} />;
|
34
|
+
}
|
@@ -0,0 +1,64 @@
|
|
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
|
+
return (
|
53
|
+
<Puck
|
54
|
+
config={puckConfig as Config}
|
55
|
+
data={initialData}
|
56
|
+
onPublish={async (data: Data) => {
|
57
|
+
// Use form data here because it's the usual remix way.
|
58
|
+
let formData = new FormData();
|
59
|
+
formData.append("puckData", JSON.stringify(data));
|
60
|
+
submit(formData, { method: "post" });
|
61
|
+
}}
|
62
|
+
/>
|
63
|
+
);
|
64
|
+
}
|
@@ -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
|
+
{
|
2
|
+
"name": "{{appName}}",
|
3
|
+
"version": "1.0.0",
|
4
|
+
"private": true,
|
5
|
+
"sideEffects": false,
|
6
|
+
"type": "module",
|
7
|
+
"scripts": {
|
8
|
+
"build": "remix build",
|
9
|
+
"dev": "remix dev --manual",
|
10
|
+
"start": "remix-serve ./build/index.js",
|
11
|
+
"typecheck": "tsc"
|
12
|
+
},
|
13
|
+
"dependencies": {
|
14
|
+
"@measured/puck": "{{puckVersion}}",
|
15
|
+
"@remix-run/css-bundle": "^2.2.0",
|
16
|
+
"@remix-run/node": "^2.2.0",
|
17
|
+
"@remix-run/react": "^2.2.0",
|
18
|
+
"@remix-run/serve": "^2.2.0",
|
19
|
+
"isbot": "^3.6.8",
|
20
|
+
"react": "^18.2.0",
|
21
|
+
"react-dom": "^18.2.0"
|
22
|
+
},
|
23
|
+
"devDependencies": {
|
24
|
+
"@remix-run/dev": "^2.2.0",
|
25
|
+
"@remix-run/eslint-config": "^2.2.0",
|
26
|
+
"@types/react": "^18.2.20",
|
27
|
+
"@types/react-dom": "^18.2.7",
|
28
|
+
"eslint": "^8.38.0",
|
29
|
+
"typescript": "^5.1.6"
|
30
|
+
},
|
31
|
+
"engines": {
|
32
|
+
"node": ">=18.0.0"
|
33
|
+
}
|
34
|
+
}
|
Binary file
|
@@ -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,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
|
+
}
|
package/scripts/generate.js
DELETED
@@ -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();
|