@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.
- package/index.js +5 -0
- package/lib/cli.js +149 -0
- package/lib/copy-template.js +67 -0
- package/lib/install-deps.js +68 -0
- package/lib/messages.js +28 -0
- package/package.json +38 -0
- package/template/.env.example +10 -0
- package/template/README.md +53 -0
- package/template/__tests__/editor-integration.test.ts +53 -0
- package/template/__tests__/remote-datasource-fetchers.test.ts +226 -0
- package/template/app/[...puckPath]/client.tsx +9 -0
- package/template/app/[...puckPath]/page.tsx +131 -0
- package/template/app/collection-nav.tsx +47 -0
- package/template/app/layout.tsx +13 -0
- package/template/app/p1/[[...p1]]/editor-client.tsx +116 -0
- package/template/app/p1/[[...p1]]/page.tsx +19 -0
- package/template/app/p1/[[...p1]]/render-client.tsx +9 -0
- package/template/app/p1/api/[...p1]/route.ts +16 -0
- package/template/app/p1/auth/[...action]/route.ts +9 -0
- package/template/app/p1/merge/merge-client.tsx +635 -0
- package/template/app/p1/merge/merge.css +257 -0
- package/template/app/p1/merge/page.tsx +12 -0
- package/template/app/page.tsx +130 -0
- package/template/app/styles.css +18 -0
- package/template/components/puck/block-padding.ts +2 -0
- package/template/components/puck/button-block.tsx +41 -0
- package/template/components/puck/divider-block.tsx +10 -0
- package/template/components/puck/grid-block.tsx +80 -0
- package/template/components/puck/heading-block.tsx +38 -0
- package/template/components/puck/image-block.tsx +33 -0
- package/template/components/puck/list-block.tsx +72 -0
- package/template/components/puck/paragraph-block.tsx +44 -0
- package/template/components/puck/quote-block.tsx +23 -0
- package/template/components/puck/root.tsx +20 -0
- package/template/components/puck/spacer-block.tsx +19 -0
- package/template/eslint.config.js +161 -0
- package/template/lib/content-publisher.ts +128 -0
- package/template/lib/fetcher-helpers.ts +17 -0
- package/template/lib/monsters-api.ts +125 -0
- package/template/lib/remote-datasource-fetchers.ts +10 -0
- package/template/lib/remote-datasources.ts +154 -0
- package/template/lib/swapi.ts +75 -0
- package/template/next-env.d.ts +6 -0
- package/template/next.config.mjs +13 -0
- package/template/package.json +42 -0
- package/template/postcss.config.mjs +8 -0
- package/template/public/sw.js +8 -0
- package/template/puck.config.tsx +51 -0
- package/template/tsconfig/base.json +20 -0
- package/template/tsconfig/nextjs.json +21 -0
- package/template/tsconfig.json +16 -0
- package/template/vitest.config.ts +7 -0
package/index.js
ADDED
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
|
+
}
|
package/lib/messages.js
ADDED
|
@@ -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
|
+
});
|