Skip to content

Commit b92b20c

Browse files
authored
custom registry base (#59)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added a static component registry with JSON endpoints and a permanent /registry → /r redirect. - Added a Mode Toggle component template and a public Test component. - Enabled build-time registry validation and a lightweight Hono server entrypoint. - **Documentation** - Added FileMaker Data API examples guide and typed Customers layout docs. - Updated docs tooling to support richer inlined examples. - **Tests** - Added Vitest config, tests for registry API and registry utilities, and a test script. - **Chores** - Upgraded dependencies across apps/packages and added editor TypeScript SDK and path alias. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
2 parents b20028b + 0f572f3 commit b92b20c

34 files changed

+3732
-1255
lines changed

.changeset/bitter-socks-trade.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@proofkit/webviewer": patch
3+
---
4+
5+
Added method to support "executeScript" method required by the adapter

.cursor/plans/static-registry.md

Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
This plan will give you a clear, step-by-step guide to building the static component registry within the existing "apps/docs" project.
2+
3+
---
4+
5+
### **High-Level Plan: Phase 1 - Static Registry**
6+
7+
The goal is to create a robust API for static components that is fully compatible with the `shadcn-cli` and can be tested thoroughly.
8+
9+
### **1. The Data Layer: Defining the "Source of Truth"**
10+
11+
This is the most critical part. A well-defined data structure will make the rest of the implementation smooth.
12+
13+
#### **A. Directory Structure**
14+
15+
The directory structure remains the same, providing a clean organization for your templates.
16+
17+
```
18+
src/
19+
└── registry/
20+
├── lib/
21+
│ ├── types.ts // NEW: Centralized type definitions
22+
│ ├── validator.ts // Build-time validation script
23+
│ └── utils.ts // File system and data transformation logic
24+
└── templates/
25+
├── button/
26+
│ ├── _meta.ts
27+
│ └── button.tsx
28+
└── icon/
29+
├── _meta.ts
30+
└── index.ts
31+
```
32+
33+
#### **B. Type Definitions (`types.ts`)**
34+
35+
Create a central file for your internal data types. This ensures consistency and provides excellent developer experience with TypeScript.
36+
37+
```typescript
38+
// src/registry/lib/types.ts
39+
import { z } from "zod";
40+
41+
// Defines a single file within a template
42+
export const templateFileSchema = z.object({
43+
sourceFileName: z.string(),
44+
destinationPath: z.string(),
45+
});
46+
47+
// Defines the metadata for a single template (_meta.ts)
48+
export const templateMetadataSchema = z.object({
49+
name: z.string(),
50+
type: z.literal("static"), // For Phase 1, we only allow 'static'
51+
description: z.string(),
52+
categories: z.array(z.enum(["component", "page", "utility", "hook"])),
53+
files: z.array(templateFileSchema),
54+
});
55+
56+
export type TemplateFile = z.infer<typeof templateFileSchema>;
57+
export type TemplateMetadata = z.infer<typeof templateMetadataSchema>;
58+
```
59+
60+
#### **C. Example Metadata (`_meta.ts`)**
61+
62+
Here is how you would define a `button` component using the new types.
63+
64+
```typescript
65+
// src/registry/templates/button/_meta.ts
66+
import type { TemplateMetadata } from "@/registry/lib/types";
67+
68+
export const meta: TemplateMetadata = {
69+
name: "button",
70+
type: "static",
71+
description: "Displays a button or a link.",
72+
categories: ["component"],
73+
files: [
74+
{
75+
// The name of the file within this directory
76+
sourceFileName: "button.tsx",
77+
// The path where the file will be placed in the user's project
78+
destinationPath: "src/components/ui/button.tsx",
79+
},
80+
],
81+
};
82+
```
83+
84+
### **2. The API Layer: Building the Registry with Next.js & Hono**
85+
86+
This layer reads from your data source and exposes it in the Shadcn-compatible format.
87+
88+
#### **A. API Route Handler (`route.ts`)**
89+
90+
The Hono router remains the core of the API, providing flexibility for the future.
91+
92+
```typescript
93+
// src/app/api/registry/[...slug]/route.ts
94+
import { Hono } from "hono";
95+
import { handle } from "hono/vercel";
96+
import { getRegistryIndex, getStaticComponent } from "@/registry/lib/utils";
97+
98+
export const runtime = "edge";
99+
100+
const app = new Hono().basePath("/api/registry");
101+
102+
// Serves the index of all available components
103+
app.get("/index.json", async (c) => {
104+
try {
105+
const index = await getRegistryIndex();
106+
return c.json(index);
107+
} catch (error) {
108+
return c.json({ error: "Failed to fetch registry index." }, 500);
109+
}
110+
});
111+
112+
// Serves the data for a single component
113+
// The :style param is part of the shadcn spec, we'll include it for compatibility
114+
app.get("/:style/:name.json", async (c) => {
115+
const { name } = c.req.param();
116+
try {
117+
const component = await getStaticComponent(name);
118+
if (!component) {
119+
return c.json({ error: "Component not found." }, 404);
120+
}
121+
return c.json(component);
122+
} catch (error) {
123+
return c.json({ error: "Failed to fetch component." }, 500);
124+
}
125+
});
126+
127+
export const GET = handle(app);
128+
```
129+
130+
#### **B. Registry Utilities (`utils.ts`)**
131+
132+
These functions are updated to handle the new `sourceFileName` and `destinationPath` structure.
133+
134+
```typescript
135+
// src/registry/lib/utils.ts
136+
import fs from "fs/promises";
137+
import path from "path";
138+
import type { TemplateMetadata } from "./types";
139+
140+
const templatesPath = path.join(process.cwd(), "src/registry/templates");
141+
142+
// Builds the index.json file
143+
export async function getRegistryIndex() {
144+
const componentDirs = await fs.readdir(templatesPath, {
145+
withFileTypes: true,
146+
});
147+
const index = [];
148+
149+
for (const dir of componentDirs) {
150+
if (dir.isDirectory()) {
151+
const { meta }: { meta: TemplateMetadata } = await import(
152+
`@/registry/templates/${dir.name}/_meta`
153+
);
154+
index.push({
155+
name: meta.name,
156+
type: meta.type,
157+
categories: meta.categories,
158+
files: meta.files.map((f) => f.destinationPath), // shadcn index uses the destination paths
159+
});
160+
}
161+
}
162+
return index;
163+
}
164+
165+
// Builds the JSON for a single static component
166+
export async function getStaticComponent(name: string) {
167+
const { meta }: { meta: TemplateMetadata } = await import(
168+
`@/registry/templates/${name}/_meta`
169+
);
170+
171+
const componentFiles = await Promise.all(
172+
meta.files.map(async (file) => {
173+
const contentPath = path.join(templatesPath, name, file.sourceFileName);
174+
const content = await fs.readFile(contentPath, "utf-8");
175+
return {
176+
// The `name` key in the output should be the filename part of the destination
177+
name: path.basename(file.destinationPath),
178+
path: file.destinationPath,
179+
content: content, // The critical content key
180+
};
181+
}),
182+
);
183+
184+
return {
185+
name: meta.name,
186+
type: meta.type,
187+
files: componentFiles,
188+
};
189+
}
190+
```
191+
192+
#### **C. Build-Time Validation (`validator.ts`)**
193+
194+
This script is crucial for preventing regressions. It should be run as part of your CI/CD pipeline or build process.
195+
196+
```typescript
197+
// src/registry/lib/validator.ts
198+
import fs from "fs/promises";
199+
import path from "path";
200+
import { templateMetadataSchema } from "./types";
201+
202+
const templatesPath = path.join(process.cwd(), "src/registry/templates");
203+
204+
async function validateRegistry() {
205+
console.log("🔍 Validating registry templates...");
206+
const componentDirs = await fs.readdir(templatesPath, {
207+
withFileTypes: true,
208+
});
209+
let errorCount = 0;
210+
211+
for (const dir of componentDirs) {
212+
if (dir.isDirectory()) {
213+
const metaPath = path.join(templatesPath, dir.name, "_meta.ts");
214+
const { meta } = await import(metaPath);
215+
216+
// 1. Validate metadata against Zod schema
217+
const validationResult = templateMetadataSchema.safeParse(meta);
218+
if (!validationResult.success) {
219+
console.error(`❌ Invalid metadata in ${dir.name}/_meta.ts:`);
220+
console.error(validationResult.error.flatten());
221+
errorCount++;
222+
}
223+
224+
// 2. Validate that all source files exist
225+
for (const file of meta.files) {
226+
const sourcePath = path.join(
227+
templatesPath,
228+
dir.name,
229+
file.sourceFileName,
230+
);
231+
try {
232+
await fs.access(sourcePath);
233+
} catch {
234+
console.error(
235+
`❌ Missing source file: ${file.sourceFileName} referenced in ${dir.name}/_meta.ts`,
236+
);
237+
errorCount++;
238+
}
239+
}
240+
}
241+
}
242+
243+
if (errorCount > 0) {
244+
console.error(`\nValidation failed with ${errorCount} error(s).`);
245+
process.exit(1); // Fail the build
246+
} else {
247+
console.log("✅ Registry validation successful!");
248+
}
249+
}
250+
251+
validateRegistry();
252+
```
253+
254+
To run this, add a script to your `package.json`:
255+
256+
```json
257+
{
258+
"scripts": {
259+
"build": "npm run registry:validate && next build",
260+
"registry:validate": "node src/registry/lib/validator.ts"
261+
}
262+
}
263+
```
264+
265+
### **3. Testing with Vitest**
266+
267+
Your tests should confirm that the API output adheres to the Shadcn spec.
268+
269+
```typescript
270+
// src/app/api/registry/route.test.ts
271+
import { describe, it, expect, vi } from "vitest";
272+
// You will need to mock the `utils.ts` functions to test the API routes in isolation.
273+
274+
vi.mock("@/registry/lib/utils", () => ({
275+
getRegistryIndex: vi.fn(),
276+
getStaticComponent: vi.fn(),
277+
}));
278+
279+
describe("Registry API - Phase 1", () => {
280+
it("GET /api/registry/index.json should return a valid index", async () => {
281+
// Mock the return value of getRegistryIndex
282+
// Make a request to the endpoint
283+
// Assert that the response contains `name`, `type`, `categories`, and `files` (as an array of strings).
284+
});
285+
286+
it("GET /api/registry/default/button.json should return a valid component", async () => {
287+
// Mock the return value of getStaticComponent
288+
// Make a request to the endpoint
289+
// Assert that the top-level response has `name`, `type`, and `files`.
290+
// Assert that each object in the `files` array has `name`, `path`, and `content`.
291+
});
292+
});
293+
```
294+
295+
This detailed plan for Phase 1 provides a robust, testable, and scalable foundation. By focusing on data integrity and API compatibility first, you set yourself up for success when implementing dynamic components and authentication later.

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,6 @@
33
{
44
"mode": "auto"
55
}
6-
]
6+
],
7+
"typescript.tsdk": "node_modules/typescript/lib"
78
}

apps/demo/package.json

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,26 +16,26 @@
1616
"@proofkit/fmdapi": "workspace:*",
1717
"@proofkit/typegen": "workspace:*",
1818
"better-auth": "^1.2.10",
19-
"dotenv": "^16.4.7",
19+
"dotenv": "^16.5.0",
2020
"fm-odata-client": "^3.0.1",
2121
"fs-extra": "^11.3.0",
22-
"next": "^15.3.4",
23-
"react": "^19.1.0",
24-
"react-dom": "^19.1.0",
22+
"next": "^15.4.6",
23+
"react": "^19.1.1",
24+
"react-dom": "^19.1.1",
2525
"zod": "3.25.64"
2626
},
2727
"devDependencies": {
2828
"@eslint/eslintrc": "^3",
29-
"@tailwindcss/postcss": "^4.1.10",
29+
"@tailwindcss/postcss": "^4.1.11",
3030
"@types/fs-extra": "^11.0.4",
31-
"@types/node": "^22.15.32",
32-
"@types/react": "^19.1.8",
33-
"@types/react-dom": "^19.1.6",
31+
"@types/node": "^22.17.1",
32+
"@types/react": "^19.1.10",
33+
"@types/react-dom": "^19.1.7",
3434
"dotenv-cli": "^8.0.0",
3535
"eslint": "^9.23.0",
3636
"eslint-config-next": "^15.3.3",
37-
"tailwindcss": "^4.1.10",
38-
"typescript": "^5.8.3",
39-
"vitest": "^3.2.3"
37+
"tailwindcss": "^4.1.11",
38+
"typescript": "^5.9.2",
39+
"vitest": "^3.2.4"
4040
}
4141
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import { DataApi, OttoAdapter } from "@proofkit/fmdapi";
2+
import { z } from "zod";
3+
4+
const fieldData = z.object({
5+
firstName: z.string(),
6+
lastName: z.string(),
7+
email: z.string(),
8+
phone: z.string(),
9+
city: z.string(),
10+
status: z.enum(["Active", "Inactive"]),
11+
created_date: z.string().datetime(),
12+
});
13+
14+
export const CustomersLayout = DataApi({
15+
adapter: new OttoAdapter({
16+
auth: { apiKey: "dk_not_a_real_key" },
17+
db: "Customers.fmp12",
18+
server: "https://filemaker.example.com",
19+
}),
20+
layout: "serverConnection",
21+
schema: {
22+
fieldData,
23+
},
24+
});
25+
26+
export type TCustomer = z.infer<typeof fieldData>;

0 commit comments

Comments
 (0)