@angeloashmore/prismic-cli-poc 0.0.0-pr.7.1e5b475 → 0.0.0-pr.8.b80fefa

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@angeloashmore/prismic-cli-poc",
3
- "version": "0.0.0-pr.7.1e5b475",
3
+ "version": "0.0.0-pr.8.b80fefa",
4
4
  "description": "A proof-of-concept developer CLI for Prismic.",
5
5
  "keywords": [
6
6
  "prismic",
@@ -59,7 +59,14 @@ export async function customTypeCreate(): Promise<void> {
59
59
  status: true,
60
60
  format: "custom",
61
61
  json: {
62
- Main: {},
62
+ Main: single
63
+ ? {}
64
+ : {
65
+ uid: {
66
+ type: "UID",
67
+ config: { label: "UID", placeholder: "" },
68
+ },
69
+ },
63
70
  },
64
71
  } satisfies CustomType;
65
72
 
package/src/docs.ts ADDED
@@ -0,0 +1,149 @@
1
+ import { parseArgs } from "node:util";
2
+
3
+ const HELP = `
4
+ Fetch and display documentation from Prismic's docs site.
5
+
6
+ USAGE
7
+ prismic docs <path> [flags]
8
+
9
+ ARGUMENTS
10
+ path Documentation path with optional anchor (e.g., "nextjs" or "nextjs#set-up-a-prismic-client")
11
+
12
+ FLAGS
13
+ -h, --help Show help for command
14
+
15
+ EXAMPLES
16
+ prismic docs nextjs
17
+ prismic docs nextjs#set-up-a-prismic-client
18
+
19
+ LEARN MORE
20
+ Visit https://prismic.io/docs for the full documentation.
21
+ `.trim();
22
+
23
+ function parsePathAndAnchor(input: string): { path: string; anchor?: string } {
24
+ const hashIndex = input.indexOf("#");
25
+ if (hashIndex === -1) {
26
+ return { path: input };
27
+ }
28
+ return {
29
+ path: input.slice(0, hashIndex),
30
+ anchor: input.slice(hashIndex + 1),
31
+ };
32
+ }
33
+
34
+ async function fetchMarkdown(
35
+ url: string,
36
+ ): Promise<{ ok: true; content: string } | { ok: false; error: string }> {
37
+ try {
38
+ const response = await fetch(url);
39
+ if (response.status === 404) {
40
+ return { ok: false, error: `Documentation not found: ${url}` };
41
+ }
42
+ if (!response.ok) {
43
+ return {
44
+ ok: false,
45
+ error: `Failed to fetch documentation: ${response.status}`,
46
+ };
47
+ }
48
+ const content = await response.text();
49
+ return { ok: true, content };
50
+ } catch (error) {
51
+ const message = error instanceof Error ? error.message : String(error);
52
+ return { ok: false, error: `Network error: ${message}` };
53
+ }
54
+ }
55
+
56
+ function anchorToHeadingPattern(anchor: string): RegExp {
57
+ // Convert kebab-case anchor to a pattern that matches the heading text
58
+ // Each hyphen/space becomes a flexible match for hyphens or spaces
59
+ const pattern = anchor
60
+ .split("-")
61
+ .map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
62
+ .join("[\\s-]+");
63
+ return new RegExp(`^(#{1,6})\\s+${pattern}\\s*$`, "im");
64
+ }
65
+
66
+ function extractSection(
67
+ markdown: string,
68
+ anchor: string,
69
+ ): { ok: true; content: string } | { ok: false; error: string } {
70
+ const lines = markdown.split("\n");
71
+ const headingPattern = anchorToHeadingPattern(anchor);
72
+
73
+ let startIndex = -1;
74
+ let headingLevel = 0;
75
+
76
+ // Find the matching heading
77
+ for (let i = 0; i < lines.length; i++) {
78
+ const match = lines[i].match(headingPattern);
79
+ if (match) {
80
+ startIndex = i;
81
+ headingLevel = match[1].length;
82
+ break;
83
+ }
84
+ }
85
+
86
+ if (startIndex === -1) {
87
+ return { ok: false, error: `Anchor not found: #${anchor}` };
88
+ }
89
+
90
+ // Find the end of this section (next heading of equal or lower level number)
91
+ let endIndex = lines.length;
92
+ for (let i = startIndex + 1; i < lines.length; i++) {
93
+ const headingMatch = lines[i].match(/^(#{1,6})\s/);
94
+ if (headingMatch && headingMatch[1].length <= headingLevel) {
95
+ endIndex = i;
96
+ break;
97
+ }
98
+ }
99
+
100
+ const content = lines.slice(startIndex, endIndex).join("\n").trim();
101
+ return { ok: true, content };
102
+ }
103
+
104
+ export async function docs(): Promise<void> {
105
+ const {
106
+ positionals: [pathArg],
107
+ values: { help },
108
+ } = parseArgs({
109
+ args: process.argv.slice(3),
110
+ options: {
111
+ help: { type: "boolean", short: "h" },
112
+ },
113
+ allowPositionals: true,
114
+ });
115
+
116
+ if (help) {
117
+ console.info(HELP);
118
+ return;
119
+ }
120
+
121
+ if (!pathArg) {
122
+ console.info(HELP);
123
+ return;
124
+ }
125
+
126
+ const { path, anchor } = parsePathAndAnchor(pathArg);
127
+ const url = `https://prismic.io/docs/${path}.md`;
128
+
129
+ const fetchResult = await fetchMarkdown(url);
130
+ if (!fetchResult.ok) {
131
+ console.error(fetchResult.error);
132
+ process.exitCode = 1;
133
+ return;
134
+ }
135
+
136
+ let output = fetchResult.content;
137
+
138
+ if (anchor) {
139
+ const extractResult = extractSection(output, anchor);
140
+ if (!extractResult.ok) {
141
+ console.error(extractResult.error);
142
+ process.exitCode = 1;
143
+ return;
144
+ }
145
+ output = extractResult.content;
146
+ }
147
+
148
+ console.info(output);
149
+ }
package/src/index.ts CHANGED
@@ -5,6 +5,7 @@ import { parseArgs } from "node:util";
5
5
  import packageJson from "../package.json" with { type: "json" };
6
6
  import { codegen } from "./codegen";
7
7
  import { customType } from "./custom-type";
8
+ import { docs } from "./docs";
8
9
  import { init } from "./init";
9
10
  import { locale } from "./locale";
10
11
  import { login } from "./login";
@@ -40,6 +41,7 @@ COMMANDS
40
41
  pull Pull types and slices from Prismic
41
42
  push Push types and slices to Prismic
42
43
  codegen Generate code from Prismic models
44
+ docs Fetch documentation from Prismic
43
45
  preview Manage preview configurations
44
46
  token Manage API tokens in a repository
45
47
  webhook Manage webhooks in a repository
@@ -107,6 +109,9 @@ if (version) {
107
109
  case "codegen":
108
110
  await codegen();
109
111
  break;
112
+ case "docs":
113
+ await docs();
114
+ break;
110
115
  case "preview":
111
116
  await preview();
112
117
  break;
@@ -59,8 +59,38 @@ export async function pageTypeCreate(): Promise<void> {
59
59
  status: true,
60
60
  format: "page",
61
61
  json: {
62
- Main: {},
63
- "SEO & Metadata": {},
62
+ Main: single
63
+ ? {}
64
+ : {
65
+ uid: {
66
+ type: "UID",
67
+ config: { label: "UID", placeholder: "" },
68
+ },
69
+ },
70
+ "SEO & Metadata": {
71
+ meta_title: {
72
+ type: "Text",
73
+ config: {
74
+ label: "Meta Title",
75
+ placeholder: "A title of the page used for social media and search engines",
76
+ },
77
+ },
78
+ meta_description: {
79
+ type: "Text",
80
+ config: {
81
+ label: "Meta Description",
82
+ placeholder: "A brief summary of the page",
83
+ },
84
+ },
85
+ meta_image: {
86
+ type: "Image",
87
+ config: {
88
+ label: "Meta Image",
89
+ constraint: { width: 2400, height: 1260 },
90
+ thumbnails: [],
91
+ },
92
+ },
93
+ },
64
94
  },
65
95
  } satisfies CustomType;
66
96
 
package/src/status.ts CHANGED
@@ -66,16 +66,53 @@ type NextStep = {
66
66
  message: string;
67
67
  };
68
68
 
69
- function getDocsUrl(framework: Framework | undefined): string {
69
+ function getDocsPath(framework: Framework | undefined): string {
70
70
  switch (framework) {
71
71
  case "next":
72
- return "https://prismic.io/docs/nextjs/with-cli";
72
+ return "nextjs/with-cli";
73
73
  case "nuxt":
74
- return "https://prismic.io/docs/nuxt/with-cli";
74
+ return "nuxt/with-cli";
75
75
  case "sveltekit":
76
- return "https://prismic.io/docs/sveltekit/with-cli";
76
+ return "sveltekit/with-cli";
77
77
  default:
78
- return "https://prismic.io/docs";
78
+ return "";
79
+ }
80
+ }
81
+
82
+ function getDocsRef(docsPath: string, anchor?: string): string {
83
+ if (!docsPath) return "";
84
+ const fullPath = anchor ? `${docsPath}${anchor}` : docsPath;
85
+ return ` (run \`prismic docs ${fullPath}\`)`;
86
+ }
87
+
88
+ function getClientSetupAnchor(framework: Framework | undefined): string {
89
+ switch (framework) {
90
+ case "nuxt":
91
+ return "#configure-the-modules-prismic-client";
92
+ default:
93
+ return "#set-up-a-prismic-client";
94
+ }
95
+ }
96
+
97
+ function getPreviewSetupAnchor(framework: Framework | undefined): string {
98
+ switch (framework) {
99
+ case "next":
100
+ return "#set-up-previews-in-next-js";
101
+ case "sveltekit":
102
+ return "#set-up-previews-in-sveltekit";
103
+ default:
104
+ return "";
105
+ }
106
+ }
107
+
108
+ function getWriteComponentsAnchor(framework: Framework | undefined): string {
109
+ switch (framework) {
110
+ case "nuxt":
111
+ return "#write-vue-components";
112
+ case "sveltekit":
113
+ return "#write-svelte-components";
114
+ default:
115
+ return "#write-react-components";
79
116
  }
80
117
  }
81
118
 
@@ -85,8 +122,9 @@ function computeNextStep(
85
122
  typeStatuses: TypeWithStatus[],
86
123
  sliceStatuses: TypeWithStatus[],
87
124
  slicesWithMissingComponents: string[],
125
+ slicesReadyToConnect: string[],
88
126
  ): NextStep | undefined {
89
- const docsUrl = getDocsUrl(frameworkInfo.framework);
127
+ const docsPath = getDocsPath(frameworkInfo.framework);
90
128
 
91
129
  // 1. Setup - missing dependencies
92
130
  const setupSection = sections.find((s) => s.title === "Setup");
@@ -99,7 +137,7 @@ function computeNextStep(
99
137
  // 2. Setup - missing client file
100
138
  const missingClientFile = setupSection?.items.find((i) => !i.done && i.hint?.includes("client"));
101
139
  if (missingClientFile) {
102
- return { message: `Create a ${missingClientFile.label} file (see ${docsUrl})` };
140
+ return { message: `Create a ${missingClientFile.label} file${getDocsRef(docsPath, getClientSetupAnchor(frameworkInfo.framework))}` };
103
141
  }
104
142
 
105
143
  // 3-7. Preview section (in order: local files, then remote config)
@@ -110,21 +148,21 @@ function computeNextStep(
110
148
  (i) => i.label === "/slice-simulator route" && !i.done,
111
149
  );
112
150
  if (sliceSimRoute) {
113
- return { message: `Create the /slice-simulator route (see ${docsUrl})` };
151
+ return { message: `Create the /slice-simulator route${getDocsRef(docsPath, "#set-up-live-previewing")}` };
114
152
  }
115
153
 
116
154
  const apiPreview = previewSection.items.find(
117
155
  (i) => i.label === "/api/preview endpoint" && !i.done,
118
156
  );
119
157
  if (apiPreview) {
120
- return { message: `Create the /api/preview route (see ${docsUrl})` };
158
+ return { message: `Create the /api/preview route${getDocsRef(docsPath, getPreviewSetupAnchor(frameworkInfo.framework))}` };
121
159
  }
122
160
 
123
161
  const exitPreview = previewSection.items.find(
124
162
  (i) => i.label === "/api/exit-preview endpoint" && !i.done,
125
163
  );
126
164
  if (exitPreview) {
127
- return { message: `Create the /api/exit-preview route (see ${docsUrl})` };
165
+ return { message: `Create the /api/exit-preview route${getDocsRef(docsPath, getPreviewSetupAnchor(frameworkInfo.framework))}` };
128
166
  }
129
167
 
130
168
  // Remote config
@@ -151,6 +189,15 @@ function computeNextStep(
151
189
  return { message: `Pull remote models with 'prismic pull'` };
152
190
  }
153
191
 
192
+ // 8.5 Slices ready to connect to a page type (before pushing)
193
+ if (slicesReadyToConnect.length > 0) {
194
+ const sorted = [...slicesReadyToConnect].sort();
195
+ const sliceName = sorted[0];
196
+ return {
197
+ message: `Connect ${sliceName} to a page type with 'prismic page-type connect-slice <type-id> ${sliceName}'`,
198
+ };
199
+ }
200
+
154
201
  // 9. Models to push
155
202
  const hasToPush =
156
203
  typeStatuses.some((t) => t.status === "to_push") ||
@@ -166,7 +213,7 @@ function computeNextStep(
166
213
  const slicesDir = getSlicesDirectory(frameworkInfo);
167
214
  const ext = getSliceComponentExtensions(frameworkInfo.framework)[0];
168
215
  const path = `${slicesDir}${sliceName}/index${ext}`;
169
- return { message: `Implement the ${sliceName} slice component at ${path} (see ${docsUrl})` };
216
+ return { message: `Implement the ${sliceName} slice component at ${path}${getDocsRef(docsPath, getWriteComponentsAnchor(frameworkInfo.framework))}` };
170
217
  }
171
218
 
172
219
  // 11-12. Deployment (Next.js only)
@@ -176,7 +223,7 @@ function computeNextStep(
176
223
  (i) => i.label === "/api/revalidate endpoint" && !i.done,
177
224
  );
178
225
  if (revalidateEndpoint) {
179
- return { message: `Create the /api/revalidate route for ISR (see ${docsUrl})` };
226
+ return { message: `Create the /api/revalidate route for ISR${getDocsRef(docsPath, "#handle-content-changes")}` };
180
227
  }
181
228
 
182
229
  const webhook = deploymentSection.items.find(
@@ -261,6 +308,7 @@ export async function status(): Promise<void> {
261
308
  let typeStatuses: TypeWithStatus[] = [];
262
309
  let sliceStatuses: TypeWithStatus[] = [];
263
310
  let slicesWithMissingComponents: string[] = [];
311
+ let slicesReadyToConnect: string[] = [];
264
312
 
265
313
  // Setup section
266
314
  const setupSection = await buildSetupSection(frameworkInfo, installedDeps);
@@ -283,10 +331,17 @@ export async function status(): Promise<void> {
283
331
  section: slicesSection,
284
332
  statuses,
285
333
  missingComponents,
286
- } = await buildSlicesSection(localSlicesResult.value, remoteSlicesResult.value, frameworkInfo);
334
+ slicesReadyToConnect: readyToConnect,
335
+ } = await buildSlicesSection(
336
+ localSlicesResult.value,
337
+ remoteSlicesResult.value,
338
+ frameworkInfo,
339
+ localTypesResult.ok ? localTypesResult.value : [],
340
+ );
287
341
  sections.push(slicesSection);
288
342
  sliceStatuses = statuses;
289
343
  slicesWithMissingComponents = missingComponents;
344
+ slicesReadyToConnect = readyToConnect;
290
345
  }
291
346
 
292
347
  // Preview section
@@ -318,6 +373,7 @@ export async function status(): Promise<void> {
318
373
  typeStatuses,
319
374
  sliceStatuses,
320
375
  slicesWithMissingComponents,
376
+ slicesReadyToConnect,
321
377
  );
322
378
  if (nextStep) {
323
379
  console.info(`Next: ${nextStep.message}`);
@@ -554,23 +610,66 @@ function statusToHint(status: TypeStatus): string | undefined {
554
610
  }
555
611
 
556
612
  // Slices Section
613
+ function sliceHasFields(slice: SharedSlice): boolean {
614
+ for (const variation of slice.variations) {
615
+ const primaryFields = Object.keys(variation.primary ?? {});
616
+ const itemFields = Object.keys(variation.items ?? {});
617
+ if (primaryFields.length > 0 || itemFields.length > 0) {
618
+ return true;
619
+ }
620
+ }
621
+ return false;
622
+ }
623
+
624
+ function isSliceConnectedToAnyType(sliceId: string, localTypes: CustomType[]): boolean {
625
+ for (const type of localTypes) {
626
+ for (const tabFields of Object.values(type.json)) {
627
+ for (const field of Object.values(tabFields as Record<string, unknown>)) {
628
+ const typedField = field as {
629
+ type?: string;
630
+ config?: { choices?: Record<string, unknown> };
631
+ };
632
+ if (typedField.type === "Slices" && typedField.config?.choices?.[sliceId]) {
633
+ return true;
634
+ }
635
+ }
636
+ }
637
+ }
638
+ return false;
639
+ }
640
+
557
641
  async function buildSlicesSection(
558
642
  localSlices: SharedSlice[],
559
643
  remoteSlices: SharedSlice[],
560
644
  info: FrameworkInfo,
645
+ localTypes: CustomType[],
561
646
  ): Promise<{
562
647
  section: StatusSection;
563
648
  statuses: TypeWithStatus[];
564
649
  missingComponents: string[];
650
+ slicesReadyToConnect: string[];
565
651
  }> {
566
652
  const sliceStatuses = computeTypeStatus(localSlices, remoteSlices);
567
653
  const items: StatusItem[] = [];
568
654
  const missingComponents: string[] = [];
655
+ const slicesReadyToConnect: string[] = [];
569
656
 
570
657
  const slicesDir = getSlicesDirectory(info);
571
658
  const extensions = getSliceComponentExtensions(info.framework);
572
659
 
573
660
  for (const slice of sliceStatuses) {
661
+ const localSlice = localSlices.find((s) => s.id === slice.id);
662
+
663
+ // Track slices that have fields but aren't connected to any type
664
+ // These should be connected before pushing
665
+ if (localSlice) {
666
+ const hasFields = sliceHasFields(localSlice);
667
+ const isConnected = isSliceConnectedToAnyType(slice.id, localTypes);
668
+
669
+ if (hasFields && !isConnected) {
670
+ slicesReadyToConnect.push(slice.label);
671
+ }
672
+ }
574
673
  // Check if component is implemented
575
674
  const componentExists = await checkSliceComponent(info, slicesDir, slice.id, extensions);
576
675
 
@@ -600,6 +699,7 @@ async function buildSlicesSection(
600
699
  section: { title: "Slices", items },
601
700
  statuses: sliceStatuses,
602
701
  missingComponents,
702
+ slicesReadyToConnect,
603
703
  };
604
704
  }
605
705