@designtools/next-plugin 0.1.2 → 0.1.3
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/dist/codesurface.js +357 -0
- package/dist/codesurface.mjs +357 -0
- package/dist/index.js +197 -4
- package/dist/index.mjs +197 -4
- package/package.json +1 -1
- package/src/codesurface.tsx +476 -0
- package/src/index.ts +11 -0
- package/src/preview-route.ts +231 -0
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preview route generator for component isolation.
|
|
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.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import fs from "fs";
|
|
10
|
+
import path from "path";
|
|
11
|
+
|
|
12
|
+
const PREVIEW_DIR = "designtools-preview";
|
|
13
|
+
|
|
14
|
+
/** Generate the preview route files in the target app's app directory. */
|
|
15
|
+
export function generatePreviewRoute(appDir: string): void {
|
|
16
|
+
const projectRoot = path.dirname(appDir);
|
|
17
|
+
const previewDir = path.join(appDir, PREVIEW_DIR);
|
|
18
|
+
|
|
19
|
+
// Discover component files so we can generate static imports
|
|
20
|
+
const componentPaths = discoverComponentFiles(projectRoot);
|
|
21
|
+
|
|
22
|
+
// Create directory
|
|
23
|
+
fs.mkdirSync(previewDir, { recursive: true });
|
|
24
|
+
|
|
25
|
+
// Write layout — minimal shell, no app chrome
|
|
26
|
+
fs.writeFileSync(
|
|
27
|
+
path.join(previewDir, "layout.tsx"),
|
|
28
|
+
getLayoutTemplate(),
|
|
29
|
+
"utf-8"
|
|
30
|
+
);
|
|
31
|
+
|
|
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
|
+
);
|
|
38
|
+
|
|
39
|
+
// Ensure designtools-preview is gitignored
|
|
40
|
+
ensureGitignore(projectRoot);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Clean up generated preview files. */
|
|
44
|
+
export function cleanupPreviewRoute(appDir: string): void {
|
|
45
|
+
const previewDir = path.join(appDir, PREVIEW_DIR);
|
|
46
|
+
try {
|
|
47
|
+
fs.rmSync(previewDir, { recursive: true, force: true });
|
|
48
|
+
// Directory itself is removed by rmSync above
|
|
49
|
+
} catch {
|
|
50
|
+
// ignore
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function ensureGitignore(projectRoot: string): void {
|
|
55
|
+
const gitignorePath = path.join(projectRoot, ".gitignore");
|
|
56
|
+
const entry = "app/designtools-preview";
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const existing = fs.existsSync(gitignorePath)
|
|
60
|
+
? fs.readFileSync(gitignorePath, "utf-8")
|
|
61
|
+
: "";
|
|
62
|
+
if (!existing.includes(entry)) {
|
|
63
|
+
fs.appendFileSync(gitignorePath, `\n# Generated by @designtools/next-plugin\n${entry}\n`);
|
|
64
|
+
}
|
|
65
|
+
} catch {
|
|
66
|
+
// ignore if we can't write gitignore
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Scan the project for component .tsx files in known directories.
|
|
72
|
+
* Returns paths like "components/ui/button" (no extension).
|
|
73
|
+
*/
|
|
74
|
+
function discoverComponentFiles(projectRoot: string): string[] {
|
|
75
|
+
const dirs = ["components/ui", "src/components/ui"];
|
|
76
|
+
for (const dir of dirs) {
|
|
77
|
+
const fullDir = path.join(projectRoot, dir);
|
|
78
|
+
if (fs.existsSync(fullDir)) {
|
|
79
|
+
const files = fs.readdirSync(fullDir);
|
|
80
|
+
return files
|
|
81
|
+
.filter((f) => f.endsWith(".tsx") || f.endsWith(".ts") || f.endsWith(".jsx"))
|
|
82
|
+
.map((f) => `${dir}/${f.replace(/\.(tsx|ts|jsx|js)$/, "")}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return [];
|
|
86
|
+
}
|
|
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
|
+
}
|