@pantheon-systems/create-p1-starter-kit 0.1.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.
Files changed (52) hide show
  1. package/index.js +5 -0
  2. package/lib/cli.js +149 -0
  3. package/lib/copy-template.js +67 -0
  4. package/lib/install-deps.js +68 -0
  5. package/lib/messages.js +28 -0
  6. package/package.json +38 -0
  7. package/template/.env.example +10 -0
  8. package/template/README.md +53 -0
  9. package/template/__tests__/editor-integration.test.ts +53 -0
  10. package/template/__tests__/remote-datasource-fetchers.test.ts +226 -0
  11. package/template/app/[...puckPath]/client.tsx +9 -0
  12. package/template/app/[...puckPath]/page.tsx +131 -0
  13. package/template/app/collection-nav.tsx +47 -0
  14. package/template/app/layout.tsx +13 -0
  15. package/template/app/p1/[[...p1]]/editor-client.tsx +116 -0
  16. package/template/app/p1/[[...p1]]/page.tsx +19 -0
  17. package/template/app/p1/[[...p1]]/render-client.tsx +9 -0
  18. package/template/app/p1/api/[...p1]/route.ts +16 -0
  19. package/template/app/p1/auth/[...action]/route.ts +9 -0
  20. package/template/app/p1/merge/merge-client.tsx +635 -0
  21. package/template/app/p1/merge/merge.css +257 -0
  22. package/template/app/p1/merge/page.tsx +12 -0
  23. package/template/app/page.tsx +130 -0
  24. package/template/app/styles.css +18 -0
  25. package/template/components/puck/block-padding.ts +2 -0
  26. package/template/components/puck/button-block.tsx +41 -0
  27. package/template/components/puck/divider-block.tsx +10 -0
  28. package/template/components/puck/grid-block.tsx +80 -0
  29. package/template/components/puck/heading-block.tsx +38 -0
  30. package/template/components/puck/image-block.tsx +33 -0
  31. package/template/components/puck/list-block.tsx +72 -0
  32. package/template/components/puck/paragraph-block.tsx +44 -0
  33. package/template/components/puck/quote-block.tsx +23 -0
  34. package/template/components/puck/root.tsx +20 -0
  35. package/template/components/puck/spacer-block.tsx +19 -0
  36. package/template/eslint.config.js +161 -0
  37. package/template/lib/content-publisher.ts +128 -0
  38. package/template/lib/fetcher-helpers.ts +17 -0
  39. package/template/lib/monsters-api.ts +125 -0
  40. package/template/lib/remote-datasource-fetchers.ts +10 -0
  41. package/template/lib/remote-datasources.ts +154 -0
  42. package/template/lib/swapi.ts +75 -0
  43. package/template/next-env.d.ts +6 -0
  44. package/template/next.config.mjs +13 -0
  45. package/template/package.json +42 -0
  46. package/template/postcss.config.mjs +8 -0
  47. package/template/public/sw.js +8 -0
  48. package/template/puck.config.tsx +51 -0
  49. package/template/tsconfig/base.json +20 -0
  50. package/template/tsconfig/nextjs.json +21 -0
  51. package/template/tsconfig.json +16 -0
  52. package/template/vitest.config.ts +7 -0
package/index.js ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { runCLI } from './lib/cli.js';
4
+
5
+ runCLI();
package/lib/cli.js ADDED
@@ -0,0 +1,149 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { execSync } from 'child_process';
4
+ import * as clack from '@clack/prompts';
5
+ import pc from 'picocolors';
6
+ import { copyTemplate } from './copy-template.js';
7
+ import { detectPackageManager, isPackageManagerAvailable, installDependencies } from './install-deps.js';
8
+ import { showWelcome, showSuccess, showInstallHelp, showError } from './messages.js';
9
+
10
+ export async function runCLI() {
11
+ showWelcome();
12
+
13
+ const args = process.argv.slice(2);
14
+ const targetDirArg = args[0];
15
+
16
+ clack.intro(pc.bgCyan(pc.black(' P1 Starter Kit Setup ')));
17
+
18
+ // Get project name
19
+ const projectName = await clack.text({
20
+ message: 'What is your project named?',
21
+ placeholder: 'my-p1-app',
22
+ initialValue: targetDirArg || 'my-p1-app',
23
+ validate: (value) => {
24
+ if (!value) return 'Please enter a project name';
25
+ if (!/^[a-z0-9-_]+$/.test(value)) {
26
+ return 'Project name must be lowercase and can only contain letters, numbers, hyphens, and underscores';
27
+ }
28
+ return undefined;
29
+ },
30
+ });
31
+
32
+ if (clack.isCancel(projectName)) {
33
+ clack.cancel('Operation cancelled');
34
+ process.exit(0);
35
+ }
36
+
37
+ const targetDir = path.resolve(process.cwd(), projectName);
38
+
39
+ // Check if directory already exists before prompting further
40
+ if (fs.existsSync(targetDir)) {
41
+ showError(`Directory "${projectName}" already exists. Please choose a different name or remove the existing directory.`);
42
+ process.exit(1);
43
+ }
44
+
45
+ // Detect package manager
46
+ const detectedPM = detectPackageManager();
47
+ const packageManager = await clack.select({
48
+ message: 'Which package manager do you want to use?',
49
+ options: [
50
+ { value: 'pnpm', label: 'pnpm', hint: detectedPM === 'pnpm' ? 'detected' : '' },
51
+ { value: 'npm', label: 'npm', hint: detectedPM === 'npm' ? 'detected' : '' },
52
+ { value: 'yarn', label: 'yarn', hint: detectedPM === 'yarn' ? 'detected' : '' },
53
+ ],
54
+ initialValue: detectedPM,
55
+ });
56
+
57
+ if (clack.isCancel(packageManager)) {
58
+ clack.cancel('Operation cancelled');
59
+ process.exit(0);
60
+ }
61
+
62
+ // Check if package manager is available
63
+ if (!isPackageManagerAvailable(packageManager)) {
64
+ showError(`${packageManager} is not installed. Please install it first or choose a different package manager.`);
65
+ process.exit(1);
66
+ }
67
+
68
+ // Git init?
69
+ const shouldInitGit = await clack.confirm({
70
+ message: 'Initialize a git repository?',
71
+ initialValue: true,
72
+ });
73
+
74
+ if (clack.isCancel(shouldInitGit)) {
75
+ clack.cancel('Operation cancelled');
76
+ process.exit(0);
77
+ }
78
+
79
+ // Install deps?
80
+ const shouldInstall = await clack.confirm({
81
+ message: 'Install dependencies now?',
82
+ initialValue: true,
83
+ });
84
+
85
+ if (clack.isCancel(shouldInstall)) {
86
+ clack.cancel('Operation cancelled');
87
+ process.exit(0);
88
+ }
89
+
90
+ const s = clack.spinner();
91
+
92
+ // Create directory and copy template
93
+ s.start('Copying template files...');
94
+ try {
95
+ fs.mkdirSync(targetDir, { recursive: false });
96
+ copyTemplate(targetDir, projectName);
97
+ s.stop('Template files copied');
98
+ } catch (error) {
99
+ s.stop('Failed to copy template');
100
+ if (error.code === 'EEXIST') {
101
+ showError(`Directory "${projectName}" already exists. Please choose a different name or remove the existing directory.`);
102
+ } else {
103
+ showError(error.message);
104
+ }
105
+ // Clean up on failure (skip if dir already existed)
106
+ if (error.code !== 'EEXIST') {
107
+ try {
108
+ fs.rmSync(targetDir, { recursive: true, force: true });
109
+ } catch (cleanupError) {
110
+ // Ignore cleanup errors
111
+ }
112
+ }
113
+ process.exit(1);
114
+ }
115
+
116
+ // Git init
117
+ if (shouldInitGit) {
118
+ s.start('Initializing git repository...');
119
+ try {
120
+ execSync('git init', { cwd: targetDir, stdio: 'ignore' });
121
+ execSync('git add -A', { cwd: targetDir, stdio: 'ignore' });
122
+ execSync('git commit -m "Initial commit from create-p1-starter-kit"', {
123
+ cwd: targetDir,
124
+ stdio: 'ignore',
125
+ });
126
+ s.stop('Git repository initialized');
127
+ } catch (error) {
128
+ s.stop('Failed to initialize git');
129
+ showError(`Git initialization failed: ${error.message}`);
130
+ }
131
+ }
132
+
133
+ // Install dependencies
134
+ if (shouldInstall) {
135
+ s.start('Installing dependencies (this may take a while)...');
136
+ const result = installDependencies(targetDir, packageManager);
137
+
138
+ if (result.success) {
139
+ s.stop('Dependencies installed');
140
+ } else {
141
+ s.stop('Dependency installation failed');
142
+ showInstallHelp(packageManager);
143
+ // Note: We don't clean up on install failure as user may want to retry manually
144
+ }
145
+ }
146
+
147
+ clack.outro(pc.green('All done!'));
148
+ showSuccess(projectName, targetDir, packageManager);
149
+ }
@@ -0,0 +1,67 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import { fileURLToPath } from 'url';
4
+
5
+ const __filename = fileURLToPath(import.meta.url);
6
+ const __dirname = path.dirname(__filename);
7
+
8
+ export function getTemplatePath() {
9
+ return path.join(__dirname, '..', 'template');
10
+ }
11
+
12
+ export function copyTemplate(targetDir, projectName) {
13
+ const templatePath = getTemplatePath();
14
+
15
+ if (!fs.existsSync(templatePath)) {
16
+ throw new Error(`Template directory not found at ${templatePath}`);
17
+ }
18
+
19
+ copyRecursive(templatePath, targetDir);
20
+
21
+ // Update package.json with project name
22
+ const packageJsonPath = path.join(targetDir, 'package.json');
23
+ let packageJson;
24
+ try {
25
+ packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
26
+ } catch (error) {
27
+ throw new Error(`Failed to parse package.json: ${error.message}`);
28
+ }
29
+ packageJson.name = projectName;
30
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
31
+ }
32
+
33
+ export function copyRecursive(src, dest, rootDest) {
34
+ // On first call, rootDest is the original destination directory
35
+ if (!rootDest) {
36
+ rootDest = path.resolve(dest);
37
+ }
38
+
39
+ // Validate that dest is within rootDest to prevent path traversal
40
+ const resolvedDest = path.resolve(dest);
41
+ if (!resolvedDest.startsWith(rootDest)) {
42
+ throw new Error(`Path traversal detected: ${dest} is outside target directory`);
43
+ }
44
+
45
+ if (!fs.existsSync(dest)) {
46
+ fs.mkdirSync(dest, { recursive: true });
47
+ }
48
+
49
+ const entries = fs.readdirSync(src, { withFileTypes: true });
50
+
51
+ for (const entry of entries) {
52
+ const srcPath = path.join(src, entry.name);
53
+ const destPath = path.join(dest, entry.name);
54
+
55
+ // Validate each destPath to prevent traversal attacks via entry.name
56
+ const resolvedDestPath = path.resolve(destPath);
57
+ if (!resolvedDestPath.startsWith(rootDest)) {
58
+ throw new Error(`Path traversal detected: ${entry.name} attempts to escape target directory`);
59
+ }
60
+
61
+ if (entry.isDirectory()) {
62
+ copyRecursive(srcPath, destPath, rootDest);
63
+ } else {
64
+ fs.copyFileSync(srcPath, destPath);
65
+ }
66
+ }
67
+ }
@@ -0,0 +1,68 @@
1
+ import { execSync } from 'child_process';
2
+ import fs from 'fs';
3
+ import path from 'path';
4
+
5
+ const ALLOWED_PACKAGE_MANAGERS = new Set(['pnpm', 'npm', 'yarn']);
6
+
7
+ function assertAllowedPM(pm) {
8
+ if (!ALLOWED_PACKAGE_MANAGERS.has(pm)) {
9
+ throw new Error(`Unknown package manager: ${pm}`);
10
+ }
11
+ }
12
+
13
+ export function detectPackageManager() {
14
+ // Check if pnpm-lock.yaml exists in parent directories
15
+ if (findFileInParents('pnpm-lock.yaml')) {
16
+ return 'pnpm';
17
+ }
18
+
19
+ // Check if yarn.lock exists
20
+ if (findFileInParents('yarn.lock')) {
21
+ return 'yarn';
22
+ }
23
+
24
+ // Default to npm
25
+ return 'npm';
26
+ }
27
+
28
+ function findFileInParents(filename) {
29
+ let currentDir = process.cwd();
30
+ const root = path.parse(currentDir).root;
31
+
32
+ while (currentDir !== root) {
33
+ if (fs.existsSync(path.join(currentDir, filename))) {
34
+ return true;
35
+ }
36
+ currentDir = path.dirname(currentDir);
37
+ }
38
+
39
+ return false;
40
+ }
41
+
42
+ export function isPackageManagerAvailable(pm) {
43
+ assertAllowedPM(pm);
44
+ try {
45
+ execSync(`${pm} --version`, { stdio: 'ignore' });
46
+ return true;
47
+ } catch {
48
+ return false;
49
+ }
50
+ }
51
+
52
+ export function installDependencies(targetDir, packageManager) {
53
+ assertAllowedPM(packageManager);
54
+ const installCmd = packageManager === 'yarn' ? 'yarn' : `${packageManager} install`;
55
+
56
+ try {
57
+ execSync(installCmd, {
58
+ cwd: targetDir,
59
+ stdio: 'inherit',
60
+ });
61
+ return { success: true };
62
+ } catch (error) {
63
+ return {
64
+ success: false,
65
+ error: error.message,
66
+ };
67
+ }
68
+ }
@@ -0,0 +1,28 @@
1
+ import pc from 'picocolors';
2
+
3
+ export function showWelcome() {
4
+ console.log(pc.bold(pc.cyan('\n┌─────────────────────────────────────────┐')));
5
+ console.log(pc.bold(pc.cyan('│ Create P1 Starter Kit │')));
6
+ console.log(pc.bold(pc.cyan('└─────────────────────────────────────────┘\n')));
7
+ }
8
+
9
+ export function showSuccess(projectName, projectPath, packageManager) {
10
+ console.log(pc.green('\n✔ Project created successfully!\n'));
11
+ console.log(`${pc.bold('Next steps:')}\n`);
12
+ console.log(` ${pc.cyan('cd')} ${projectName}`);
13
+ console.log(` ${pc.dim('# Copy .env.example to .env and fill in your credentials:')}`);
14
+ console.log(` ${pc.cyan('cp')} .env.example .env`);
15
+ console.log(` ${pc.dim('# Edit .env with your PCC_SITE_ID and PCC_TOKEN')}\n`);
16
+ console.log(` ${pc.dim('# Start the dev server:')}`);
17
+ console.log(` ${pc.cyan(`${packageManager} dev`)}\n`);
18
+ console.log(pc.bold('Happy building! 🚀\n'));
19
+ }
20
+
21
+ export function showInstallHelp(packageManager) {
22
+ console.log(pc.yellow('\n⚠️ Dependency installation failed.\n'));
23
+ console.log(`Try running ${pc.cyan(`${packageManager} install`)} manually.\n`);
24
+ }
25
+
26
+ export function showError(message) {
27
+ console.error(pc.red(`\n✖ Error: ${message}\n`));
28
+ }
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@pantheon-systems/create-p1-starter-kit",
3
+ "version": "0.1.0",
4
+ "description": "Scaffold a new P1 starter project",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-p1-starter-kit": "./index.js"
8
+ },
9
+ "files": [
10
+ "index.js",
11
+ "lib/",
12
+ "template/"
13
+ ],
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://github.com/pantheon-systems/puck-css-integration.git",
17
+ "directory": "packages/create-p1-starter-kit"
18
+ },
19
+ "keywords": [
20
+ "p1",
21
+ "puck",
22
+ "cms",
23
+ "content-publisher",
24
+ "starter",
25
+ "template"
26
+ ],
27
+ "engines": {
28
+ "node": ">=20.12.0"
29
+ },
30
+ "dependencies": {
31
+ "@clack/prompts": "^1.5.1",
32
+ "picocolors": "^1.1.1"
33
+ },
34
+ "license": "MIT",
35
+ "scripts": {
36
+ "build": "node scripts/build-template.js"
37
+ }
38
+ }
@@ -0,0 +1,10 @@
1
+ PCC_SITE_ID=your-site-id
2
+ PCC_TOKEN=your-token
3
+
4
+ # --- P1 backend ---
5
+ NEXT_PUBLIC_CSS_BASE_URL=https://css.example.com
6
+ NEXT_PUBLIC_CSS_SITE_ID=site-123
7
+ P1_CSS_API_KEY=your-api-key
8
+
9
+ # Branch is auto-detected (defaults to main) unless specified:
10
+ # NEXT_PUBLIC_CSS_BRANCH_ID=branch-456
@@ -0,0 +1,53 @@
1
+ # Content Publisher Site Sample
2
+
3
+ A sample Next.js site demonstrating the Content Publisher CMS — Pantheon's visual page-building and content management system. Content Publisher combines a Puck-based drag-and-drop editor with server-side datasource resolution, enabling authors to build dynamic, data-driven pages without writing code.
4
+
5
+ ## What's included
6
+
7
+ - **Visual page editor** — Puck-powered drag-and-drop editing at `/p1/<path>` with built-in blocks for typography, media, layout, and actions
8
+ - **Site structure management** — Dashboard at `/p1` for creating and organizing pages and templates
9
+ - **Datasource bindings** — Connect page blocks to live data from Content Publisher articles, external APIs (SWAPI, Pokemon GraphQL), and URL route parameters
10
+ - **Next.js App Router** — Server components, catch-all routing, and API route handlers via `@pantheon-systems/p1-client-sdk`
11
+
12
+ ## Getting started
13
+
14
+ 1. Copy the environment file and fill in your credentials:
15
+
16
+ ```
17
+ cp .env.example .env
18
+ ```
19
+
20
+ | Variable | Description |
21
+ | ------------- | -------------------------- |
22
+ | `PCC_SITE_ID` | Your Content Cloud site ID |
23
+ | `PCC_TOKEN` | API token for your site |
24
+
25
+ 2. Install dependencies and start the dev server:
26
+
27
+ ```
28
+ npm install
29
+ npm run dev
30
+ ```
31
+
32
+ 3. Open http://localhost:3000 to view the site, or http://localhost:3000/p1 for the page management dashboard.
33
+
34
+ ## Project structure
35
+
36
+ ```
37
+ app/
38
+ page.tsx # Site root (renders via Puck)
39
+ [...puckPath]/ # Catch-all route for published pages
40
+ p1/
41
+ page.tsx # P1 dashboard — lists pages, links to editor
42
+ [...p1]/ # Editor & renderer for any page path
43
+ api/[...p1]/route.ts # API handler (GET/POST/DELETE) for page data
44
+ components/puck/ # Block definitions (Heading, Paragraph, Image, etc.)
45
+ lib/builtin-datasources.ts # Datasource registry (SWAPI, Pokemon, articles, URL params)
46
+ puck.config.tsx # Puck editor configuration — block registry & categories
47
+ ```
48
+
49
+ ## Customization
50
+
51
+ - **Add blocks** — Create a new component in `components/puck/`, then register it in `puck.config.tsx`
52
+ - **Add datasources** — Define a new `DatasourceDefinition` in `lib/builtin-datasources.ts` to make external data available to block fields via `{{ datasource.field }}` expressions
53
+ - **Change styling** — The project uses Tailwind CSS v4; edit `app/styles.css` or individual block components
@@ -0,0 +1,53 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { readFileSync } from "fs";
3
+ import { resolve, dirname } from "path";
4
+ import { fileURLToPath } from "url";
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ const appDir = resolve(__dirname, "..");
8
+
9
+ describe("editor-client uses P1 plugins", () => {
10
+ const content = readFileSync(
11
+ resolve(appDir, "app/p1/[[...p1]]/editor-client.tsx"),
12
+ "utf-8",
13
+ );
14
+
15
+ it("imports useP1Plugins", () => {
16
+ expect(content).toContain("useP1Plugins");
17
+ });
18
+
19
+ it("imports wrapConfigForEditorPreview", () => {
20
+ expect(content).toContain("wrapConfigForEditorPreview");
21
+ });
22
+
23
+ it("passes additionalPlugins to useP1Editor", () => {
24
+ expect(content).toContain("additionalPlugins");
25
+ });
26
+
27
+ it("wraps with P1QueryProvider for TanStack React Query", () => {
28
+ expect(content).toContain("P1QueryProvider");
29
+ });
30
+ });
31
+
32
+ describe("API handler passes fetcher config", () => {
33
+ const content = readFileSync(
34
+ resolve(appDir, "app/p1/api/[...p1]/route.ts"),
35
+ "utf-8",
36
+ );
37
+
38
+ it("imports REMOTE_DATASOURCE_FETCHERS", () => {
39
+ expect(content).toContain("REMOTE_DATASOURCE_FETCHERS");
40
+ });
41
+
42
+ it("imports REMOTE_DATASOURCE_REGISTRY", () => {
43
+ expect(content).toContain("REMOTE_DATASOURCE_REGISTRY");
44
+ });
45
+
46
+ it("passes builtinFetchers to createP1Handler", () => {
47
+ expect(content).toContain("builtinFetchers");
48
+ });
49
+
50
+ it("passes builtinDatasourceRegistry to createP1Handler", () => {
51
+ expect(content).toContain("builtinDatasourceRegistry");
52
+ });
53
+ });