@designtools/next-plugin 0.1.6 → 0.1.8

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.
@@ -1,51 +1,52 @@
1
1
  /**
2
- * Preview route generator for component isolation.
2
+ * Component registry generator for component isolation.
3
3
  *
4
- * Writes a catch-all preview page into the target app's app directory
5
- * that can render any component with arbitrary props via postMessage.
6
- * Uses Next.js file-system routing — no custom server needed.
4
+ * Writes a static import map file into the target app's app directory.
5
+ * The <CodeSurface /> component uses this registry to dynamically load
6
+ * components for the isolation overlay — no route change needed.
7
7
  */
8
8
 
9
9
  import fs from "fs";
10
10
  import path from "path";
11
11
 
12
- const PREVIEW_DIR = "designtools-preview";
12
+ const REGISTRY_FILE = "designtools-registry.ts";
13
13
 
14
- /** Generate the preview route files in the target app's app directory. */
15
- export function generatePreviewRoute(appDir: string): void {
14
+ /** Generate the component registry file in the target app's app directory. */
15
+ export function generateComponentRegistry(appDir: string): void {
16
16
  const projectRoot = path.dirname(appDir);
17
- const previewDir = path.join(appDir, PREVIEW_DIR);
18
17
 
19
18
  // Discover component files so we can generate static imports
20
19
  const componentPaths = discoverComponentFiles(projectRoot);
21
20
 
22
- // Create directory
23
- fs.mkdirSync(previewDir, { recursive: true });
21
+ const registryEntries = componentPaths
22
+ .map((p) => ` "${p}": () => import("@/${p}"),`)
23
+ .join("\n");
24
+
25
+ const content = `"use client";
26
+ // Auto-generated by @designtools/next-plugin — do not edit
27
+
28
+ const COMPONENT_REGISTRY: Record<string, () => Promise<any>> = {
29
+ ${registryEntries}
30
+ };
24
31
 
25
- // Write layout minimal shell, no app chrome
26
- fs.writeFileSync(
27
- path.join(previewDir, "layout.tsx"),
28
- getLayoutTemplate(),
29
- "utf-8"
30
- );
32
+ // Self-register on window so CodeSurface can access it without
33
+ // passing functions through the RSC serialization boundary.
34
+ if (typeof window !== "undefined") {
35
+ (window as any).__DESIGNTOOLS_REGISTRY__ = COMPONENT_REGISTRY;
36
+ }
37
+ `;
31
38
 
32
- // Write page — client component that renders previews via postMessage
33
- fs.writeFileSync(
34
- path.join(previewDir, "page.tsx"),
35
- getPageTemplate(componentPaths),
36
- "utf-8"
37
- );
39
+ fs.writeFileSync(path.join(appDir, REGISTRY_FILE), content, "utf-8");
38
40
 
39
- // Ensure designtools-preview is gitignored
41
+ // Ensure registry file is gitignored
40
42
  ensureGitignore(projectRoot);
41
43
  }
42
44
 
43
- /** Clean up generated preview files. */
44
- export function cleanupPreviewRoute(appDir: string): void {
45
- const previewDir = path.join(appDir, PREVIEW_DIR);
45
+ /** Clean up generated registry file. */
46
+ export function cleanupComponentRegistry(appDir: string): void {
47
+ const registryPath = path.join(appDir, REGISTRY_FILE);
46
48
  try {
47
- fs.rmSync(previewDir, { recursive: true, force: true });
48
- // Directory itself is removed by rmSync above
49
+ fs.unlinkSync(registryPath);
49
50
  } catch {
50
51
  // ignore
51
52
  }
@@ -53,7 +54,7 @@ export function cleanupPreviewRoute(appDir: string): void {
53
54
 
54
55
  function ensureGitignore(projectRoot: string): void {
55
56
  const gitignorePath = path.join(projectRoot, ".gitignore");
56
- const entry = "app/designtools-preview";
57
+ const entry = "app/designtools-registry.ts";
57
58
 
58
59
  try {
59
60
  const existing = fs.existsSync(gitignorePath)
@@ -70,9 +71,10 @@ function ensureGitignore(projectRoot: string): void {
70
71
  /**
71
72
  * Scan the project for component .tsx files in known directories.
72
73
  * Returns paths like "components/ui/button" (no extension).
74
+ * Accepts an optional override directory to skip hardcoded candidates.
73
75
  */
74
- function discoverComponentFiles(projectRoot: string): string[] {
75
- const dirs = ["components/ui", "src/components/ui"];
76
+ function discoverComponentFiles(projectRoot: string, overrideDir?: string): string[] {
77
+ const dirs = overrideDir ? [overrideDir] : ["components/ui", "src/components/ui"];
76
78
  for (const dir of dirs) {
77
79
  const fullDir = path.join(projectRoot, dir);
78
80
  if (fs.existsSync(fullDir)) {
@@ -84,148 +86,3 @@ function discoverComponentFiles(projectRoot: string): string[] {
84
86
  }
85
87
  return [];
86
88
  }
87
-
88
- function getLayoutTemplate(): string {
89
- return `// Auto-generated by @designtools/next-plugin — do not edit
90
- export default function PreviewLayout({ children }: { children: React.ReactNode }) {
91
- return (
92
- <div style={{ padding: 32, background: "var(--background, #fff)", minHeight: "100vh" }}>
93
- {children}
94
- </div>
95
- );
96
- }
97
- `;
98
- }
99
-
100
- function getPageTemplate(componentPaths: string[]): string {
101
- // Generate static import map entries — webpack can analyze these
102
- const registryEntries = componentPaths
103
- .map((p) => ` "${p}": () => import("@/${p}"),`)
104
- .join("\n");
105
-
106
- return `// Auto-generated by @designtools/next-plugin — do not edit
107
- "use client";
108
-
109
- import { useState, useEffect, useCallback, createElement } from "react";
110
-
111
- /* Static import registry — webpack can analyze these imports */
112
- const COMPONENT_REGISTRY: Record<string, () => Promise<any>> = {
113
- ${registryEntries}
114
- };
115
-
116
- interface Combination {
117
- label: string;
118
- props: Record<string, string>;
119
- }
120
-
121
- interface RenderMsg {
122
- type: "tool:renderPreview";
123
- componentPath: string;
124
- exportName: string;
125
- combinations: Combination[];
126
- defaultChildren: string;
127
- }
128
-
129
- export default function PreviewPage() {
130
- const [Component, setComponent] = useState<React.ComponentType<any> | null>(null);
131
- const [combinations, setCombinations] = useState<Combination[]>([]);
132
- const [defaultChildren, setDefaultChildren] = useState("");
133
- const [error, setError] = useState<string | null>(null);
134
-
135
- const handleMessage = useCallback(async (e: MessageEvent) => {
136
- const msg = e.data;
137
- if (msg?.type !== "tool:renderPreview") return;
138
-
139
- const { componentPath, exportName, combinations: combos, defaultChildren: children } = msg as RenderMsg;
140
-
141
- try {
142
- setError(null);
143
- setCombinations(combos);
144
- setDefaultChildren(children || exportName);
145
-
146
- const loader = COMPONENT_REGISTRY[componentPath];
147
- if (!loader) {
148
- setError(\`Component "\${componentPath}" not found in registry. Available: \${Object.keys(COMPONENT_REGISTRY).join(", ")}\`);
149
- return;
150
- }
151
-
152
- const mod = await loader();
153
- const Comp = mod[exportName] || mod.default;
154
- if (!Comp) {
155
- setError(\`Export "\${exportName}" not found in \${componentPath}\`);
156
- return;
157
- }
158
-
159
- setComponent(() => Comp);
160
-
161
- // Notify editor that preview is ready
162
- window.parent.postMessage(
163
- { type: "tool:previewReady", cellCount: combos.length },
164
- "*"
165
- );
166
- } catch (err: any) {
167
- setError(\`Failed to load component: \${err.message}\`);
168
- }
169
- }, []);
170
-
171
- useEffect(() => {
172
- window.addEventListener("message", handleMessage);
173
- // Signal readiness to the editor
174
- window.parent.postMessage({ type: "tool:injectedReady" }, "*");
175
- return () => window.removeEventListener("message", handleMessage);
176
- }, [handleMessage]);
177
-
178
- if (error) {
179
- return (
180
- <div style={{ padding: 32, color: "#ef4444", fontFamily: "monospace", fontSize: 14 }}>
181
- {error}
182
- </div>
183
- );
184
- }
185
-
186
- if (!Component) {
187
- return (
188
- <div style={{ padding: 32, color: "#888", fontFamily: "system-ui", fontSize: 14 }}>
189
- Waiting for component…
190
- </div>
191
- );
192
- }
193
-
194
- return (
195
- <div style={{ fontFamily: "system-ui" }}>
196
- <div style={{
197
- display: "grid",
198
- gridTemplateColumns: "repeat(auto-fill, minmax(240px, 1fr))",
199
- gap: 24,
200
- }}>
201
- {combinations.map((combo, i) => (
202
- <div key={i} style={{ display: "flex", flexDirection: "column", gap: 8 }}>
203
- <div style={{
204
- fontSize: 11,
205
- fontWeight: 600,
206
- color: "#888",
207
- textTransform: "uppercase",
208
- letterSpacing: "0.05em",
209
- }}>
210
- {combo.label}
211
- </div>
212
- <div style={{
213
- padding: 16,
214
- border: "1px solid #e5e7eb",
215
- borderRadius: 8,
216
- display: "flex",
217
- alignItems: "center",
218
- justifyContent: "center",
219
- minHeight: 64,
220
- background: "#fff",
221
- }}>
222
- {createElement(Component, combo.props, defaultChildren)}
223
- </div>
224
- </div>
225
- ))}
226
- </div>
227
- </div>
228
- );
229
- }
230
- `;
231
- }