code-to-design 0.1.5 → 0.2.1
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/canvas-dist/assets/index-DZ4iZlMc.js +40 -0
- package/canvas-dist/index.html +1 -1
- package/dist/{chunk-WX4KLWOS.js → chunk-AL6L2SWU.js} +782 -146
- package/dist/chunk-AL6L2SWU.js.map +1 -0
- package/dist/commands/scan.js +1 -1
- package/dist/index.d.ts +15 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/canvas-dist/assets/index-BRaZpda-.js +0 -40
- package/dist/chunk-WX4KLWOS.js.map +0 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// src/commands/scan.ts
|
|
2
|
-
import { join as
|
|
3
|
-
import { rm, mkdir as
|
|
2
|
+
import { join as join10 } from "path";
|
|
3
|
+
import { rm, mkdir as mkdir4 } from "fs/promises";
|
|
4
4
|
import { existsSync as existsSync7, watch as fsWatch } from "fs";
|
|
5
5
|
|
|
6
6
|
// ../core/src/discovery/route-scanner.ts
|
|
7
|
-
import { readdir, stat } from "fs/promises";
|
|
7
|
+
import { readdir, readFile, stat } from "fs/promises";
|
|
8
8
|
import { join, extname } from "path";
|
|
9
9
|
var PAGE_EXTENSIONS = /* @__PURE__ */ new Set([".js", ".jsx", ".ts", ".tsx"]);
|
|
10
10
|
var PAGE_BASENAMES = /* @__PURE__ */ new Set(["page"]);
|
|
@@ -13,6 +13,13 @@ function isPageFile(filename) {
|
|
|
13
13
|
const basename = filename.slice(0, -ext.length);
|
|
14
14
|
return PAGE_EXTENSIONS.has(ext) && PAGE_BASENAMES.has(basename);
|
|
15
15
|
}
|
|
16
|
+
function isPagesRouterFile(filename) {
|
|
17
|
+
const ext = extname(filename);
|
|
18
|
+
if (!PAGE_EXTENSIONS.has(ext)) return false;
|
|
19
|
+
const basename = filename.slice(0, -ext.length);
|
|
20
|
+
if (basename.startsWith("_")) return false;
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
16
23
|
function shouldSkipDir(name) {
|
|
17
24
|
if (name.startsWith("_")) return true;
|
|
18
25
|
if (name.startsWith("@")) return true;
|
|
@@ -82,24 +89,182 @@ async function scanDir(dirPath, urlSegments, params) {
|
|
|
82
89
|
}
|
|
83
90
|
return routes;
|
|
84
91
|
}
|
|
92
|
+
async function scanPagesDir(dirPath, urlSegments, params) {
|
|
93
|
+
const routes = [];
|
|
94
|
+
let entries;
|
|
95
|
+
try {
|
|
96
|
+
entries = await readdir(dirPath);
|
|
97
|
+
} catch {
|
|
98
|
+
return routes;
|
|
99
|
+
}
|
|
100
|
+
for (const entry of entries) {
|
|
101
|
+
const entryPath = join(dirPath, entry);
|
|
102
|
+
const entryStat = await stat(entryPath).catch(() => null);
|
|
103
|
+
if (!entryStat) continue;
|
|
104
|
+
if (entryStat.isFile() && isPagesRouterFile(entry)) {
|
|
105
|
+
const ext = extname(entry);
|
|
106
|
+
const basename = entry.slice(0, -ext.length);
|
|
107
|
+
let fileUrlSegments;
|
|
108
|
+
let fileParams;
|
|
109
|
+
if (basename === "index") {
|
|
110
|
+
fileUrlSegments = urlSegments;
|
|
111
|
+
fileParams = params;
|
|
112
|
+
} else {
|
|
113
|
+
const param = parseDynamicSegment(basename);
|
|
114
|
+
const urlPart = segmentToUrlPart(basename);
|
|
115
|
+
fileUrlSegments = [...urlSegments, urlPart];
|
|
116
|
+
fileParams = param ? [...params, param] : params;
|
|
117
|
+
}
|
|
118
|
+
const urlPath = "/" + fileUrlSegments.join("/");
|
|
119
|
+
routes.push({
|
|
120
|
+
urlPath: urlPath || "/",
|
|
121
|
+
filePath: entryPath,
|
|
122
|
+
params: [...fileParams],
|
|
123
|
+
isDynamic: fileParams.length > 0
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
if (entryStat.isDirectory()) {
|
|
127
|
+
if (entry === "api") continue;
|
|
128
|
+
if (entry.startsWith("_")) continue;
|
|
129
|
+
if (entry === "node_modules" || entry === ".next") continue;
|
|
130
|
+
const param = parseDynamicSegment(entry);
|
|
131
|
+
const urlPart = segmentToUrlPart(entry);
|
|
132
|
+
const newParams = param ? [...params, param] : params;
|
|
133
|
+
const nested = await scanPagesDir(entryPath, [...urlSegments, urlPart], newParams);
|
|
134
|
+
routes.push(...nested);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return routes;
|
|
138
|
+
}
|
|
139
|
+
function parseReactRouterParam(segment) {
|
|
140
|
+
if (!segment.startsWith(":")) return null;
|
|
141
|
+
return { name: segment.slice(1), isCatchAll: false, isOptional: false };
|
|
142
|
+
}
|
|
143
|
+
async function findFiles(dir, extensions) {
|
|
144
|
+
const results = [];
|
|
145
|
+
let entries;
|
|
146
|
+
try {
|
|
147
|
+
entries = await readdir(dir);
|
|
148
|
+
} catch {
|
|
149
|
+
return results;
|
|
150
|
+
}
|
|
151
|
+
for (const entry of entries) {
|
|
152
|
+
const full = join(dir, entry);
|
|
153
|
+
const s = await stat(full).catch(() => null);
|
|
154
|
+
if (!s) continue;
|
|
155
|
+
if (s.isDirectory()) {
|
|
156
|
+
if (entry === "node_modules" || entry === ".git" || entry === "dist" || entry === "build") continue;
|
|
157
|
+
results.push(...await findFiles(full, extensions));
|
|
158
|
+
} else if (extensions.has(extname(entry))) {
|
|
159
|
+
results.push(full);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return results;
|
|
163
|
+
}
|
|
164
|
+
async function hasReactRouter(projectRoot) {
|
|
165
|
+
try {
|
|
166
|
+
const pkg = JSON.parse(await readFile(join(projectRoot, "package.json"), "utf-8"));
|
|
167
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
168
|
+
return "react-router-dom" in deps || "react-router" in deps;
|
|
169
|
+
} catch {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function extractRoutePathsFromSource(source) {
|
|
174
|
+
const paths = [];
|
|
175
|
+
const seen = /* @__PURE__ */ new Set();
|
|
176
|
+
const jsxPattern = /<Route\s[^>]*?path\s*=\s*["']([^"']+)["']/g;
|
|
177
|
+
let match;
|
|
178
|
+
while ((match = jsxPattern.exec(source)) !== null) {
|
|
179
|
+
const p = match[1];
|
|
180
|
+
if (p !== "*" && !seen.has(p)) {
|
|
181
|
+
seen.add(p);
|
|
182
|
+
paths.push(p);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
const objPattern = /path\s*:\s*["']([^"']+)["']/g;
|
|
186
|
+
while ((match = objPattern.exec(source)) !== null) {
|
|
187
|
+
const p = match[1];
|
|
188
|
+
if (p !== "*" && !seen.has(p)) {
|
|
189
|
+
seen.add(p);
|
|
190
|
+
paths.push(p);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
return paths;
|
|
194
|
+
}
|
|
195
|
+
async function scanReactRouter(projectRoot) {
|
|
196
|
+
const routes = [];
|
|
197
|
+
const seen = /* @__PURE__ */ new Set();
|
|
198
|
+
const srcDir = join(projectRoot, "src");
|
|
199
|
+
const scanRoot = await stat(srcDir).then(() => srcDir).catch(() => projectRoot);
|
|
200
|
+
const files = await findFiles(scanRoot, PAGE_EXTENSIONS);
|
|
201
|
+
for (const filePath of files) {
|
|
202
|
+
let source;
|
|
203
|
+
try {
|
|
204
|
+
source = await readFile(filePath, "utf-8");
|
|
205
|
+
} catch {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
if (!source.includes("react-router-dom") && !source.includes("react-router")) {
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (!source.includes("Route") && !source.includes("createBrowserRouter") && !source.includes("createRoutesFromElements") && !source.match(/path\s*:\s*["']/)) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
const extractedPaths = extractRoutePathsFromSource(source);
|
|
215
|
+
for (const routePath of extractedPaths) {
|
|
216
|
+
if (seen.has(routePath)) continue;
|
|
217
|
+
seen.add(routePath);
|
|
218
|
+
const segments = routePath.split("/").filter(Boolean);
|
|
219
|
+
const params = [];
|
|
220
|
+
for (const seg of segments) {
|
|
221
|
+
const param = parseReactRouterParam(seg);
|
|
222
|
+
if (param) params.push(param);
|
|
223
|
+
}
|
|
224
|
+
const urlPath = routePath.startsWith("/") ? routePath : "/" + routePath;
|
|
225
|
+
routes.push({
|
|
226
|
+
urlPath,
|
|
227
|
+
filePath,
|
|
228
|
+
params,
|
|
229
|
+
isDynamic: params.length > 0
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return routes;
|
|
234
|
+
}
|
|
85
235
|
async function scanRoutes(options) {
|
|
86
|
-
const { appDir } = options;
|
|
236
|
+
const { appDir, routerType = "auto" } = options;
|
|
237
|
+
if (routerType === "react-router") {
|
|
238
|
+
const routes2 = await scanReactRouter(appDir);
|
|
239
|
+
routes2.sort((a, b) => a.urlPath.localeCompare(b.urlPath));
|
|
240
|
+
return routes2;
|
|
241
|
+
}
|
|
242
|
+
if (routerType === "auto") {
|
|
243
|
+
const isReactRouter = await hasReactRouter(appDir);
|
|
244
|
+
if (isReactRouter) {
|
|
245
|
+
const routes2 = await scanReactRouter(appDir);
|
|
246
|
+
routes2.sort((a, b) => a.urlPath.localeCompare(b.urlPath));
|
|
247
|
+
return routes2;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
87
250
|
const dirStat = await stat(appDir).catch(() => null);
|
|
88
251
|
if (!dirStat || !dirStat.isDirectory()) {
|
|
89
252
|
throw new Error(`App directory not found: ${appDir}`);
|
|
90
253
|
}
|
|
91
|
-
const
|
|
254
|
+
const dirName = appDir.replace(/\/$/, "").split("/").pop();
|
|
255
|
+
const isPagesRouter = routerType === "pages-router" || routerType === "auto" && dirName === "pages";
|
|
256
|
+
const routes = isPagesRouter ? await scanPagesDir(appDir, [], []) : await scanDir(appDir, [], []);
|
|
92
257
|
routes.sort((a, b) => a.urlPath.localeCompare(b.urlPath));
|
|
93
258
|
return routes;
|
|
94
259
|
}
|
|
95
260
|
|
|
96
261
|
// ../core/src/analysis/code-analyzer.ts
|
|
97
|
-
import { readFile as
|
|
262
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
98
263
|
import { join as join3, dirname, resolve } from "path";
|
|
99
264
|
import { existsSync as existsSync2 } from "fs";
|
|
100
265
|
|
|
101
266
|
// ../core/src/analysis/auth-detector.ts
|
|
102
|
-
import { readFile } from "fs/promises";
|
|
267
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
103
268
|
import { join as join2 } from "path";
|
|
104
269
|
import { existsSync } from "fs";
|
|
105
270
|
var MIDDLEWARE_FILES = ["middleware.ts", "middleware.js", "middleware.tsx", "middleware.jsx"];
|
|
@@ -147,7 +312,7 @@ async function detectAuth(projectRoot, allSources) {
|
|
|
147
312
|
const middlewarePath = join2(projectRoot, filename);
|
|
148
313
|
if (existsSync(middlewarePath)) {
|
|
149
314
|
try {
|
|
150
|
-
const source = await
|
|
315
|
+
const source = await readFile2(middlewarePath, "utf-8");
|
|
151
316
|
const cookies = extractCookieNames(source);
|
|
152
317
|
if (cookies.length > 0) {
|
|
153
318
|
config.hasAuth = true;
|
|
@@ -170,6 +335,84 @@ async function detectAuth(projectRoot, allSources) {
|
|
|
170
335
|
return config;
|
|
171
336
|
}
|
|
172
337
|
|
|
338
|
+
// ../core/src/analysis/endpoint-extractor.ts
|
|
339
|
+
function extractBaseUrl(source) {
|
|
340
|
+
const envFallback = source.match(
|
|
341
|
+
/process\.env\.\w+\s*\|\|\s*['"]([^'"]+)['"]/
|
|
342
|
+
);
|
|
343
|
+
if (envFallback) return envFallback[1];
|
|
344
|
+
const baseUrlProp = source.match(/baseURL\s*:\s*['"]([^'"]+)['"]/);
|
|
345
|
+
if (baseUrlProp) return baseUrlProp[1];
|
|
346
|
+
const constAssign = source.match(
|
|
347
|
+
/(?:API_BASE_URL|API_URL|BASE_URL)\s*=\s*['"]([^'"]+)['"]/
|
|
348
|
+
);
|
|
349
|
+
if (constAssign) return constAssign[1];
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
function extractEndpoints(apiClientSource) {
|
|
353
|
+
const endpoints = [];
|
|
354
|
+
const seen = /* @__PURE__ */ new Set();
|
|
355
|
+
const lines = apiClientSource.split("\n");
|
|
356
|
+
for (let i = 0; i < lines.length; i++) {
|
|
357
|
+
const line = lines[i];
|
|
358
|
+
const methodCallRegex = /\.\s*(get|post|put|delete|patch)\s*\(\s*(?:['"`])([^'"`$]*)/gi;
|
|
359
|
+
let match;
|
|
360
|
+
while ((match = methodCallRegex.exec(line)) !== null) {
|
|
361
|
+
const method = match[1].toUpperCase();
|
|
362
|
+
let path = match[2];
|
|
363
|
+
path = path.replace(/['"`)]+$/, "").trim();
|
|
364
|
+
if (!path || path.length === 0) continue;
|
|
365
|
+
const key = `${method} ${path}`;
|
|
366
|
+
if (seen.has(key)) continue;
|
|
367
|
+
seen.add(key);
|
|
368
|
+
const endpoint = { method, path };
|
|
369
|
+
const fnName = findFunctionName(lines, i);
|
|
370
|
+
if (fnName) endpoint.functionName = fnName;
|
|
371
|
+
endpoints.push(endpoint);
|
|
372
|
+
}
|
|
373
|
+
const fetchRegex = /\bfetch\s*\(\s*['"`]([^'"`$]+)/g;
|
|
374
|
+
while ((match = fetchRegex.exec(line)) !== null) {
|
|
375
|
+
const path = match[1].replace(/['"`)]+$/, "").trim();
|
|
376
|
+
if (!path || path.length === 0) continue;
|
|
377
|
+
const key = `GET ${path}`;
|
|
378
|
+
if (seen.has(key)) continue;
|
|
379
|
+
seen.add(key);
|
|
380
|
+
const endpoint = { method: "GET", path };
|
|
381
|
+
const fnName = findFunctionName(lines, i);
|
|
382
|
+
if (fnName) endpoint.functionName = fnName;
|
|
383
|
+
endpoints.push(endpoint);
|
|
384
|
+
}
|
|
385
|
+
const templateRegex = /\.\s*(get|post|put|delete|patch)\s*\(\s*`([^`]*?)\$\{/gi;
|
|
386
|
+
while ((match = templateRegex.exec(line)) !== null) {
|
|
387
|
+
const method = match[1].toUpperCase();
|
|
388
|
+
const path = match[2].trim();
|
|
389
|
+
if (!path || path.length === 0) continue;
|
|
390
|
+
const key = `${method} ${path}`;
|
|
391
|
+
if (seen.has(key)) continue;
|
|
392
|
+
seen.add(key);
|
|
393
|
+
const endpoint = { method, path };
|
|
394
|
+
const fnName = findFunctionName(lines, i);
|
|
395
|
+
if (fnName) endpoint.functionName = fnName;
|
|
396
|
+
endpoints.push(endpoint);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return endpoints;
|
|
400
|
+
}
|
|
401
|
+
function findFunctionName(lines, lineIndex) {
|
|
402
|
+
for (let i = lineIndex; i >= Math.max(0, lineIndex - 3); i--) {
|
|
403
|
+
const line = lines[i];
|
|
404
|
+
const constArrow = line.match(/(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:\([^)]*\)\s*=>|\([^)]*\)\s*:\s*\w[^=]*=>)/);
|
|
405
|
+
if (constArrow) return constArrow[1];
|
|
406
|
+
const propMatch = line.match(/(\w+)\s*:\s*(?:\([^)]*\)\s*=>|function\b)/);
|
|
407
|
+
if (propMatch) return propMatch[1];
|
|
408
|
+
const fnMatch = line.match(/(?:async\s+)?function\s+(\w+)/);
|
|
409
|
+
if (fnMatch) return fnMatch[1];
|
|
410
|
+
const methodMatch = line.match(/^\s*(\w+)\s*\([^)]*\)\s*\{/);
|
|
411
|
+
if (methodMatch) return methodMatch[1];
|
|
412
|
+
}
|
|
413
|
+
return void 0;
|
|
414
|
+
}
|
|
415
|
+
|
|
173
416
|
// ../core/src/analysis/code-analyzer.ts
|
|
174
417
|
var DEFAULT_MAX_TOKENS = 8e3;
|
|
175
418
|
var CHARS_PER_TOKEN = 4;
|
|
@@ -198,7 +441,7 @@ async function readPathAliases(projectRoot) {
|
|
|
198
441
|
const configPath = join3(projectRoot, filename);
|
|
199
442
|
if (!existsSync2(configPath)) continue;
|
|
200
443
|
try {
|
|
201
|
-
const content = await
|
|
444
|
+
const content = await readFile3(configPath, "utf-8");
|
|
202
445
|
const stripped = content.replace(
|
|
203
446
|
/"(?:[^"\\]|\\.)*"|\/\/.*$|\/\*[\s\S]*?\*\//gm,
|
|
204
447
|
(match) => match.startsWith('"') ? match : ""
|
|
@@ -242,7 +485,7 @@ async function traceImports(filePath, projectRoot, aliases, maxDepth, visited =
|
|
|
242
485
|
visited.add(filePath);
|
|
243
486
|
let source;
|
|
244
487
|
try {
|
|
245
|
-
source = await
|
|
488
|
+
source = await readFile3(filePath, "utf-8");
|
|
246
489
|
} catch {
|
|
247
490
|
return results;
|
|
248
491
|
}
|
|
@@ -289,6 +532,58 @@ function estimateTokens(text) {
|
|
|
289
532
|
function hasApiCalls(source) {
|
|
290
533
|
return /\bfetch\s*\(/.test(source) || /\baxios\b/.test(source) || /\buseQuery\b/.test(source) || /\buseMutation\b/.test(source) || /\bapiRequest\b/.test(source) || /\bgetServerSideProps\b/.test(source) || /\bgetStaticProps\b/.test(source) || /API_BASE_URL/.test(source) || /['"]\/api\//.test(source) || /from\s+['"].*api[-_]?client.*['"]/.test(source) || /from\s+['"].*\/api['"]/.test(source) || /from\s+['"].*\/services\//.test(source);
|
|
291
534
|
}
|
|
535
|
+
var API_CLIENT_CANDIDATES = [
|
|
536
|
+
"lib/api.ts",
|
|
537
|
+
"lib/api.tsx",
|
|
538
|
+
"lib/api.js",
|
|
539
|
+
"lib/api.jsx",
|
|
540
|
+
"lib/api-client.ts",
|
|
541
|
+
"lib/api-client.tsx",
|
|
542
|
+
"lib/api-client.js",
|
|
543
|
+
"lib/api-client.jsx",
|
|
544
|
+
"src/lib/api.ts",
|
|
545
|
+
"src/lib/api.tsx",
|
|
546
|
+
"src/lib/api.js",
|
|
547
|
+
"src/lib/api.jsx",
|
|
548
|
+
"src/lib/api-client.ts",
|
|
549
|
+
"src/lib/api-client.tsx",
|
|
550
|
+
"src/lib/api-client.js",
|
|
551
|
+
"src/lib/api-client.jsx",
|
|
552
|
+
"services/api.ts",
|
|
553
|
+
"services/api.js",
|
|
554
|
+
"services/api.tsx",
|
|
555
|
+
"services/api.jsx",
|
|
556
|
+
"src/services/api.ts",
|
|
557
|
+
"src/services/api.js",
|
|
558
|
+
"src/services/api.tsx",
|
|
559
|
+
"src/services/api.jsx",
|
|
560
|
+
"utils/api.ts",
|
|
561
|
+
"utils/api.js",
|
|
562
|
+
"utils/api.tsx",
|
|
563
|
+
"utils/api.jsx",
|
|
564
|
+
"src/utils/api.ts",
|
|
565
|
+
"src/utils/api.js",
|
|
566
|
+
"src/utils/api.tsx",
|
|
567
|
+
"src/utils/api.jsx"
|
|
568
|
+
];
|
|
569
|
+
function hasApiClientPatterns(source) {
|
|
570
|
+
return /\.(get|post|put|delete|patch)\s*\(/.test(source) || /\bfetch\s*\(/.test(source) || /\baxios\b/.test(source) || /\bbaseURL\b/.test(source) || /\bAPI_URL\b/.test(source);
|
|
571
|
+
}
|
|
572
|
+
async function findApiClientEagerly(projectRoot, alreadyIncluded) {
|
|
573
|
+
for (const candidate of API_CLIENT_CANDIDATES) {
|
|
574
|
+
const fullPath = join3(projectRoot, candidate);
|
|
575
|
+
if (alreadyIncluded.has(fullPath)) continue;
|
|
576
|
+
if (!existsSync2(fullPath)) continue;
|
|
577
|
+
try {
|
|
578
|
+
const content = await readFile3(fullPath, "utf-8");
|
|
579
|
+
if (hasApiClientPatterns(content)) {
|
|
580
|
+
return { path: fullPath, content };
|
|
581
|
+
}
|
|
582
|
+
} catch {
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
292
587
|
async function analyzePage(route, options) {
|
|
293
588
|
const { projectRoot, maxTokensPerPage = DEFAULT_MAX_TOKENS } = options;
|
|
294
589
|
const maxChars = maxTokensPerPage * CHARS_PER_TOKEN;
|
|
@@ -308,6 +603,39 @@ ${content}
|
|
|
308
603
|
}
|
|
309
604
|
sourceContext += section;
|
|
310
605
|
}
|
|
606
|
+
const alreadyIncluded = new Set(tracedFiles.keys());
|
|
607
|
+
let apiClientPath = null;
|
|
608
|
+
let apiClientSource = null;
|
|
609
|
+
const eagerResult = await findApiClientEagerly(projectRoot, alreadyIncluded);
|
|
610
|
+
if (eagerResult) {
|
|
611
|
+
apiClientPath = eagerResult.path;
|
|
612
|
+
apiClientSource = eagerResult.content;
|
|
613
|
+
const relativePath = eagerResult.path.startsWith(projectRoot) ? eagerResult.path.slice(projectRoot.length + 1) : eagerResult.path;
|
|
614
|
+
const section = `
|
|
615
|
+
// === ${relativePath} ===
|
|
616
|
+
${eagerResult.content}
|
|
617
|
+
`;
|
|
618
|
+
if (sourceContext.length + section.length <= maxChars) {
|
|
619
|
+
sourceContext += section;
|
|
620
|
+
}
|
|
621
|
+
resolvedImports.push(eagerResult.path);
|
|
622
|
+
allSources.push(eagerResult.content);
|
|
623
|
+
} else {
|
|
624
|
+
for (const [filePath, content] of tracedFiles) {
|
|
625
|
+
if (filePath === route.filePath) continue;
|
|
626
|
+
if (hasApiClientPatterns(content)) {
|
|
627
|
+
apiClientPath = filePath;
|
|
628
|
+
apiClientSource = content;
|
|
629
|
+
break;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
let extractedEndpointsList = [];
|
|
634
|
+
let apiBaseUrl = null;
|
|
635
|
+
if (apiClientSource) {
|
|
636
|
+
extractedEndpointsList = extractEndpoints(apiClientSource);
|
|
637
|
+
apiBaseUrl = extractBaseUrl(apiClientSource);
|
|
638
|
+
}
|
|
311
639
|
const authConfig = await detectAuth(projectRoot, allSources);
|
|
312
640
|
const combinedSource = allSources.join("\n");
|
|
313
641
|
const hasApi = hasApiCalls(sourceContext) || hasApiCalls(combinedSource);
|
|
@@ -325,13 +653,13 @@ ${content}
|
|
|
325
653
|
unresolvedImports: [...new Set(unresolvedImports)],
|
|
326
654
|
authConfig,
|
|
327
655
|
hasApiDependencies: hasApi,
|
|
328
|
-
estimatedTokens: estimateTokens(sourceContext)
|
|
656
|
+
estimatedTokens: estimateTokens(sourceContext),
|
|
657
|
+
apiClientPath,
|
|
658
|
+
extractedEndpoints: extractedEndpointsList,
|
|
659
|
+
apiBaseUrl
|
|
329
660
|
};
|
|
330
661
|
}
|
|
331
662
|
|
|
332
|
-
// ../core/src/mock/types.ts
|
|
333
|
-
var ALL_STATE_VARIANTS = ["success", "empty", "error", "loading"];
|
|
334
|
-
|
|
335
663
|
// ../core/src/mock/llm-client.ts
|
|
336
664
|
import Anthropic from "@anthropic-ai/sdk";
|
|
337
665
|
var LlmClient = class {
|
|
@@ -359,20 +687,18 @@ var LlmClient = class {
|
|
|
359
687
|
};
|
|
360
688
|
|
|
361
689
|
// ../core/src/mock/prompt-templates.ts
|
|
362
|
-
var SYSTEM_PROMPT = `You are a mock data generator for a UI pre-rendering tool called
|
|
363
|
-
Your job is to analyze a Next.js page's source code and generate realistic mock API responses that will make the page render correctly.
|
|
690
|
+
var SYSTEM_PROMPT = `You are a mock data generator for a UI pre-rendering tool called Code to Design.
|
|
691
|
+
Your job is to analyze a Next.js page's source code and generate realistic mock API responses that will make the page render correctly in different visual states.
|
|
364
692
|
|
|
365
693
|
Rules:
|
|
366
694
|
1. Output ONLY valid JSON \u2014 no markdown, no code fences, no explanation.
|
|
367
|
-
2. Mock data must match the TypeScript interfaces found in the source code.
|
|
695
|
+
2. Mock data must match the TypeScript interfaces and API response shapes found in the source code.
|
|
368
696
|
3. Generate contextually appropriate data (e.g., real company names for a finance app, realistic usernames for a social app).
|
|
369
|
-
4.
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
5. Include auth mock data if the page requires authentication.
|
|
375
|
-
6. For dynamic route parameters, generate a realistic sample value.`;
|
|
697
|
+
4. Generate MULTIPLE items in success data (at least 3-5 items for list endpoints).
|
|
698
|
+
5. If actual API endpoint URLs are provided, use those EXACT paths in your response.
|
|
699
|
+
6. Analyze the page's conditional rendering (if/else, ternary, switch, state variables) to identify meaningful visual states \u2014 not just generic success/error.
|
|
700
|
+
7. Include auth mock data if the page requires authentication.
|
|
701
|
+
8. For dynamic route parameters, generate a realistic sample value.`;
|
|
376
702
|
function buildUserPrompt(analysis) {
|
|
377
703
|
const parts = [];
|
|
378
704
|
parts.push(`## Page Route: ${analysis.route.urlPath}`);
|
|
@@ -380,6 +706,16 @@ function buildUserPrompt(analysis) {
|
|
|
380
706
|
if (analysis.route.isDynamic) {
|
|
381
707
|
parts.push(`## Dynamic Parameters: ${analysis.route.params.map((p) => p.name).join(", ")}`);
|
|
382
708
|
}
|
|
709
|
+
if (analysis.extractedEndpoints.length > 0) {
|
|
710
|
+
parts.push("## Actual API Endpoints (use these EXACT paths)");
|
|
711
|
+
for (const ep of analysis.extractedEndpoints) {
|
|
712
|
+
const name = ep.functionName ? ` (${ep.functionName})` : "";
|
|
713
|
+
parts.push(`- ${ep.method} ${ep.path}${name}`);
|
|
714
|
+
}
|
|
715
|
+
if (analysis.apiBaseUrl) {
|
|
716
|
+
parts.push(`- Base URL: ${analysis.apiBaseUrl}`);
|
|
717
|
+
}
|
|
718
|
+
}
|
|
383
719
|
parts.push("## Source Code Context");
|
|
384
720
|
parts.push(analysis.sourceContext);
|
|
385
721
|
if (analysis.authConfig.hasAuth) {
|
|
@@ -398,29 +734,51 @@ function buildUserPrompt(analysis) {
|
|
|
398
734
|
parts.push(`
|
|
399
735
|
## Required Output Format
|
|
400
736
|
|
|
737
|
+
Analyze the page code carefully. Look at:
|
|
738
|
+
- Conditional rendering (if/else, ternary operators, switch statements)
|
|
739
|
+
- State variables that control what is displayed
|
|
740
|
+
- URL parameters or search params that affect the view
|
|
741
|
+
- Authentication state checks
|
|
742
|
+
- Data loading patterns
|
|
743
|
+
|
|
744
|
+
Then generate mock data for **page-specific visual states** \u2014 not just generic success/error. For example:
|
|
745
|
+
- A dashboard with tabs might have states: "overview_tab", "readings_tab", "settings_tab"
|
|
746
|
+
- A page checking auth might have: "authenticated_with_data", "authenticated_empty", "unauthenticated"
|
|
747
|
+
- A results page might have: "results_found", "no_results", "invalid_id"
|
|
748
|
+
|
|
749
|
+
Always include at least these base states:
|
|
750
|
+
- One state with realistic populated data (the "happy path")
|
|
751
|
+
- One state with empty/no data
|
|
752
|
+
- One error state (API returns 500)
|
|
753
|
+
|
|
401
754
|
Return a JSON object with this exact structure:
|
|
402
755
|
{
|
|
403
|
-
"routeParams": { "paramName": "sampleValue" },
|
|
404
|
-
"
|
|
756
|
+
"routeParams": { "paramName": "sampleValue" },
|
|
757
|
+
"stateVariants": [
|
|
405
758
|
{
|
|
406
|
-
"
|
|
407
|
-
"
|
|
408
|
-
"
|
|
409
|
-
"
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
759
|
+
"name": "descriptive_state_name",
|
|
760
|
+
"description": "What this state represents visually",
|
|
761
|
+
"apiMocks": {
|
|
762
|
+
"METHOD /endpoint/path": {
|
|
763
|
+
"status": 200,
|
|
764
|
+
"body": { ... },
|
|
765
|
+
"delay": 0
|
|
766
|
+
}
|
|
413
767
|
}
|
|
414
768
|
}
|
|
415
769
|
],
|
|
416
|
-
"authMock": {
|
|
770
|
+
"authMock": {
|
|
417
771
|
"cookies": { "cookie_name": "mock_value" },
|
|
418
772
|
"authCheckEndpoint": "/auth/me",
|
|
419
773
|
"authCheckResponse": { "status": 200, "body": { ... } }
|
|
420
774
|
}
|
|
421
775
|
}
|
|
422
776
|
|
|
423
|
-
|
|
777
|
+
IMPORTANT:
|
|
778
|
+
- Use the ACTUAL endpoint paths from the "Actual API Endpoints" section above if provided.
|
|
779
|
+
- Each state variant should produce a VISUALLY DIFFERENT page render.
|
|
780
|
+
- Generate at least 3-5 items in list/array responses for the happy path state.
|
|
781
|
+
- Generate ONLY the JSON. No explanation, no markdown fences.`);
|
|
424
782
|
return parts.join("\n\n");
|
|
425
783
|
}
|
|
426
784
|
|
|
@@ -444,7 +802,41 @@ function parseLlmJson(content) {
|
|
|
444
802
|
return null;
|
|
445
803
|
}
|
|
446
804
|
}
|
|
447
|
-
function
|
|
805
|
+
function isV2Output(output) {
|
|
806
|
+
return "stateVariants" in output && Array.isArray(output.stateVariants);
|
|
807
|
+
}
|
|
808
|
+
function convertV2ToMockConfigs(output, analysis) {
|
|
809
|
+
if (!output.stateVariants || output.stateVariants.length === 0) {
|
|
810
|
+
return [];
|
|
811
|
+
}
|
|
812
|
+
return output.stateVariants.map((variant) => {
|
|
813
|
+
const apiMocks = {};
|
|
814
|
+
for (const [pattern, mock] of Object.entries(variant.apiMocks)) {
|
|
815
|
+
apiMocks[pattern] = {
|
|
816
|
+
status: mock.status,
|
|
817
|
+
body: mock.body,
|
|
818
|
+
delay: mock.delay
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
let authMock = null;
|
|
822
|
+
if (analysis.authConfig.hasAuth && output.authMock) {
|
|
823
|
+
authMock = {
|
|
824
|
+
cookies: output.authMock.cookies,
|
|
825
|
+
authCheckResponse: {
|
|
826
|
+
status: output.authMock.authCheckResponse.status,
|
|
827
|
+
body: output.authMock.authCheckResponse.body
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
return {
|
|
832
|
+
stateName: variant.name,
|
|
833
|
+
apiMocks,
|
|
834
|
+
authMock,
|
|
835
|
+
routeParams: output.routeParams
|
|
836
|
+
};
|
|
837
|
+
});
|
|
838
|
+
}
|
|
839
|
+
function convertV1ToMockConfigs(output, analysis, variants) {
|
|
448
840
|
const configs = [];
|
|
449
841
|
for (const variant of variants) {
|
|
450
842
|
const apiMocks = {};
|
|
@@ -494,7 +886,7 @@ function generateFallbackConfigs(analysis, variants) {
|
|
|
494
886
|
}));
|
|
495
887
|
}
|
|
496
888
|
async function generateMocks(analysis, options) {
|
|
497
|
-
const variants = options.variants ??
|
|
889
|
+
const variants = options.variants ?? ["success", "empty", "error", "loading"];
|
|
498
890
|
if (!analysis.hasApiDependencies) {
|
|
499
891
|
return {
|
|
500
892
|
configs: generateFallbackConfigs(analysis, variants),
|
|
@@ -513,7 +905,15 @@ async function generateMocks(analysis, options) {
|
|
|
513
905
|
tokenUsage: { input: response.inputTokens, output: response.outputTokens }
|
|
514
906
|
};
|
|
515
907
|
}
|
|
516
|
-
|
|
908
|
+
let configs;
|
|
909
|
+
if (isV2Output(parsed)) {
|
|
910
|
+
configs = convertV2ToMockConfigs(parsed, analysis);
|
|
911
|
+
} else {
|
|
912
|
+
configs = convertV1ToMockConfigs(parsed, analysis, variants);
|
|
913
|
+
}
|
|
914
|
+
if (configs.length === 0) {
|
|
915
|
+
configs = generateFallbackConfigs(analysis, variants);
|
|
916
|
+
}
|
|
517
917
|
return {
|
|
518
918
|
configs,
|
|
519
919
|
tokenUsage: { input: response.inputTokens, output: response.outputTokens }
|
|
@@ -529,8 +929,8 @@ async function generateMocks(analysis, options) {
|
|
|
529
929
|
|
|
530
930
|
// ../core/src/render/pre-renderer.ts
|
|
531
931
|
import { chromium } from "playwright";
|
|
532
|
-
import { mkdir, writeFile } from "fs/promises";
|
|
533
|
-
import { join as
|
|
932
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
933
|
+
import { join as join6 } from "path";
|
|
534
934
|
|
|
535
935
|
// ../core/src/render/dev-server.ts
|
|
536
936
|
import { spawn } from "child_process";
|
|
@@ -633,6 +1033,199 @@ ${stderr.slice(-500)}`));
|
|
|
633
1033
|
};
|
|
634
1034
|
}
|
|
635
1035
|
|
|
1036
|
+
// ../core/src/render/style-inliner.ts
|
|
1037
|
+
async function inlineStylesAndCleanup(page) {
|
|
1038
|
+
await page.evaluate(`(() => {
|
|
1039
|
+
const links = document.querySelectorAll('link[rel="stylesheet"]');
|
|
1040
|
+
links.forEach(link => {
|
|
1041
|
+
try {
|
|
1042
|
+
const href = link.getAttribute('href');
|
|
1043
|
+
if (!href) return;
|
|
1044
|
+
for (const sheet of document.styleSheets) {
|
|
1045
|
+
if (sheet.href && sheet.href.includes(href.replace(/^\\//, ''))) {
|
|
1046
|
+
const rules = Array.from(sheet.cssRules).map(r => r.cssText).join('\\n');
|
|
1047
|
+
const style = document.createElement('style');
|
|
1048
|
+
style.textContent = rules;
|
|
1049
|
+
link.parentNode.replaceChild(style, link);
|
|
1050
|
+
break;
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
} catch (e) {}
|
|
1054
|
+
});
|
|
1055
|
+
document.querySelectorAll('script').forEach(s => s.remove());
|
|
1056
|
+
})()`);
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
// ../core/src/render/interaction-capturer.ts
|
|
1060
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
1061
|
+
import { join as join5 } from "path";
|
|
1062
|
+
function slugifyRoute(urlPath) {
|
|
1063
|
+
if (urlPath === "/") return "index";
|
|
1064
|
+
return urlPath.replace(/^\//, "").replace(/\//g, "-").replace(/:/g, "_").replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
1065
|
+
}
|
|
1066
|
+
async function findClickableElements(page, maxElements) {
|
|
1067
|
+
return page.evaluate(`((max) => {
|
|
1068
|
+
const selectors = [
|
|
1069
|
+
'[role="tab"]:not([aria-selected="true"]):not([aria-disabled="true"])',
|
|
1070
|
+
'[role="button"]:not([aria-disabled="true"]):not([disabled])',
|
|
1071
|
+
'button:not([disabled]):not([type="submit"])',
|
|
1072
|
+
'[data-tab]:not(.active):not(.selected)',
|
|
1073
|
+
'.tab:not(.active):not(.selected)',
|
|
1074
|
+
'a[href="#"]:not(.active)',
|
|
1075
|
+
'a[href^="#"]:not([href="#"]):not(.active)',
|
|
1076
|
+
];
|
|
1077
|
+
|
|
1078
|
+
const seen = new Set();
|
|
1079
|
+
const results = [];
|
|
1080
|
+
|
|
1081
|
+
for (const sel of selectors) {
|
|
1082
|
+
if (results.length >= max) break;
|
|
1083
|
+
const elements = document.querySelectorAll(sel);
|
|
1084
|
+
|
|
1085
|
+
for (const el of elements) {
|
|
1086
|
+
if (results.length >= max) break;
|
|
1087
|
+
if (seen.has(el)) continue;
|
|
1088
|
+
seen.add(el);
|
|
1089
|
+
|
|
1090
|
+
const rect = el.getBoundingClientRect();
|
|
1091
|
+
if (rect.width === 0 || rect.height === 0) continue;
|
|
1092
|
+
const style = window.getComputedStyle(el);
|
|
1093
|
+
if (style.display === 'none' || style.visibility === 'hidden') continue;
|
|
1094
|
+
|
|
1095
|
+
if (el.tagName === 'A') {
|
|
1096
|
+
const href = el.getAttribute('href') || '';
|
|
1097
|
+
if (href.startsWith('http') || href.startsWith('//')) continue;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
if (el.tagName === 'BUTTON' && el.type === 'submit') continue;
|
|
1101
|
+
|
|
1102
|
+
const text = (el.textContent || '').trim().slice(0, 50);
|
|
1103
|
+
const ariaLabel = el.getAttribute('aria-label');
|
|
1104
|
+
const role = el.getAttribute('role');
|
|
1105
|
+
const tag = el.tagName.toLowerCase();
|
|
1106
|
+
|
|
1107
|
+
let desc = '';
|
|
1108
|
+
if (role === 'tab') desc = 'Tab: ' + (ariaLabel || text || 'unnamed');
|
|
1109
|
+
else if (role === 'button') desc = 'Button: ' + (ariaLabel || text || 'unnamed');
|
|
1110
|
+
else if (tag === 'button') desc = 'Button: ' + (ariaLabel || text || 'unnamed');
|
|
1111
|
+
else desc = 'Clickable: ' + (ariaLabel || text || 'unnamed');
|
|
1112
|
+
|
|
1113
|
+
let uniqueSelector = '';
|
|
1114
|
+
const id = el.getAttribute('id');
|
|
1115
|
+
if (id) {
|
|
1116
|
+
uniqueSelector = '#' + CSS.escape(id);
|
|
1117
|
+
} else {
|
|
1118
|
+
const dataTestId = el.getAttribute('data-testid');
|
|
1119
|
+
if (dataTestId) {
|
|
1120
|
+
uniqueSelector = '[data-testid="' + CSS.escape(dataTestId) + '"]';
|
|
1121
|
+
} else {
|
|
1122
|
+
const allMatching = document.querySelectorAll(sel);
|
|
1123
|
+
const idx = Array.from(allMatching).indexOf(el);
|
|
1124
|
+
uniqueSelector = '__INDEX__' + sel + '__' + idx;
|
|
1125
|
+
}
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
results.push({ selector: uniqueSelector, description: desc });
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
return results;
|
|
1133
|
+
})(${maxElements})`);
|
|
1134
|
+
}
|
|
1135
|
+
async function captureInteractions(page, pageUrl, route, stateName, outputDir, options) {
|
|
1136
|
+
const maxInteractions = options?.maxInteractions ?? 5;
|
|
1137
|
+
const settleTime = options?.settleTime ?? 500;
|
|
1138
|
+
const routeSlug = slugifyRoute(route.urlPath);
|
|
1139
|
+
const stateDir = join5(outputDir, "renders", routeSlug);
|
|
1140
|
+
await mkdir(stateDir, { recursive: true });
|
|
1141
|
+
const clickables = await findClickableElements(page, maxInteractions);
|
|
1142
|
+
if (clickables.length === 0) return [];
|
|
1143
|
+
const results = [];
|
|
1144
|
+
for (let i = 0; i < clickables.length; i++) {
|
|
1145
|
+
const clickable = clickables[i];
|
|
1146
|
+
const htmlRelPath = join5("renders", routeSlug, `${stateName}_interaction_${i}.html`);
|
|
1147
|
+
const htmlAbsPath = join5(outputDir, htmlRelPath);
|
|
1148
|
+
try {
|
|
1149
|
+
let clicked = false;
|
|
1150
|
+
if (clickable.selector.startsWith("__INDEX__")) {
|
|
1151
|
+
const parts = clickable.selector.slice("__INDEX__".length);
|
|
1152
|
+
const lastUnderscoreIdx = parts.lastIndexOf("__");
|
|
1153
|
+
const sel = parts.slice(0, lastUnderscoreIdx);
|
|
1154
|
+
const idx = parseInt(parts.slice(lastUnderscoreIdx + 2), 10);
|
|
1155
|
+
const elements = await page.$$(sel);
|
|
1156
|
+
if (elements[idx]) {
|
|
1157
|
+
await elements[idx].click();
|
|
1158
|
+
clicked = true;
|
|
1159
|
+
}
|
|
1160
|
+
} else {
|
|
1161
|
+
const element = await page.$(clickable.selector);
|
|
1162
|
+
if (element) {
|
|
1163
|
+
await element.click();
|
|
1164
|
+
clicked = true;
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
if (!clicked) {
|
|
1168
|
+
results.push({
|
|
1169
|
+
elementDescription: clickable.description,
|
|
1170
|
+
htmlPath: htmlRelPath,
|
|
1171
|
+
success: false,
|
|
1172
|
+
error: "Element not found on re-query"
|
|
1173
|
+
});
|
|
1174
|
+
continue;
|
|
1175
|
+
}
|
|
1176
|
+
await page.waitForTimeout(settleTime);
|
|
1177
|
+
await inlineStylesAndCleanup(page);
|
|
1178
|
+
const html = await page.content();
|
|
1179
|
+
await writeFile(htmlAbsPath, html, "utf-8");
|
|
1180
|
+
results.push({
|
|
1181
|
+
elementDescription: clickable.description,
|
|
1182
|
+
htmlPath: htmlRelPath,
|
|
1183
|
+
success: true
|
|
1184
|
+
});
|
|
1185
|
+
} catch (err) {
|
|
1186
|
+
results.push({
|
|
1187
|
+
elementDescription: clickable.description,
|
|
1188
|
+
htmlPath: htmlRelPath,
|
|
1189
|
+
success: false,
|
|
1190
|
+
error: err instanceof Error ? err.message : String(err)
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
try {
|
|
1194
|
+
await page.goto(pageUrl, { waitUntil: "networkidle", timeout: 1e4 });
|
|
1195
|
+
await page.waitForTimeout(settleTime);
|
|
1196
|
+
} catch {
|
|
1197
|
+
break;
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
return results;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// ../core/src/render/url-matcher.ts
|
|
1204
|
+
function matchMockUrl(requestUrl, requestMethod, mockPattern) {
|
|
1205
|
+
const [mockMethod, ...pathParts] = mockPattern.split(" ");
|
|
1206
|
+
if (requestMethod !== mockMethod) return false;
|
|
1207
|
+
let mockPath = pathParts.join(" ");
|
|
1208
|
+
mockPath = mockPath.replace(/\/+$/, "");
|
|
1209
|
+
const urlPath = new URL(requestUrl).pathname.replace(/\/+$/, "");
|
|
1210
|
+
const regexStr = mockPath.replace(/\{[^}]+\}/g, "[^/]+").replace(/\*/g, "[^/]+").replace(/:[a-zA-Z_]+/g, "[^/]+");
|
|
1211
|
+
const fullRegex = new RegExp("^" + regexStr.replace(/\//g, "\\/") + "$");
|
|
1212
|
+
if (fullRegex.test(urlPath)) return true;
|
|
1213
|
+
const mockSegments = mockPath.split("/").filter(Boolean);
|
|
1214
|
+
const urlSegments = urlPath.split("/").filter(Boolean);
|
|
1215
|
+
if (mockSegments.length >= 1 && urlSegments.length >= mockSegments.length) {
|
|
1216
|
+
const urlSuffix = urlSegments.slice(-mockSegments.length);
|
|
1217
|
+
const match = mockSegments.every((seg, i) => {
|
|
1218
|
+
if (seg.startsWith("{") || seg.startsWith(":") || seg === "*") return true;
|
|
1219
|
+
return seg === urlSuffix[i];
|
|
1220
|
+
});
|
|
1221
|
+
if (match) return true;
|
|
1222
|
+
}
|
|
1223
|
+
return false;
|
|
1224
|
+
}
|
|
1225
|
+
function isApiUrl(url) {
|
|
1226
|
+
return url.includes("/api/") || url.includes(":8000") || url.includes(":3000/api");
|
|
1227
|
+
}
|
|
1228
|
+
|
|
636
1229
|
// ../core/src/render/pre-renderer.ts
|
|
637
1230
|
var DEFAULT_CONCURRENCY = 3;
|
|
638
1231
|
var DEFAULT_PAGE_TIMEOUT = 15e3;
|
|
@@ -650,7 +1243,7 @@ function buildUrlPath(route, mockConfig) {
|
|
|
650
1243
|
}
|
|
651
1244
|
return path;
|
|
652
1245
|
}
|
|
653
|
-
function
|
|
1246
|
+
function slugifyRoute2(urlPath) {
|
|
654
1247
|
if (urlPath === "/") return "index";
|
|
655
1248
|
return urlPath.replace(/^\//, "").replace(/\//g, "-").replace(/:/g, "_").replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
656
1249
|
}
|
|
@@ -688,12 +1281,7 @@ async function setupMockInterception(page, devServerUrl, mockConfig, authConfig)
|
|
|
688
1281
|
});
|
|
689
1282
|
}
|
|
690
1283
|
for (const [pattern, mock] of Object.entries(mockConfig.apiMocks)) {
|
|
691
|
-
|
|
692
|
-
let mockPath = pathParts.join(" ");
|
|
693
|
-
mockPath = mockPath.replace(/\{[^}]+\}/g, "[^/]+");
|
|
694
|
-
mockPath = mockPath.replace(/\*/g, "[^/]+");
|
|
695
|
-
const pathRegex = new RegExp(mockPath.replace(/\//g, "\\/"));
|
|
696
|
-
if (method === mockMethod && pathRegex.test(url)) {
|
|
1284
|
+
if (matchMockUrl(url, method, pattern)) {
|
|
697
1285
|
if (mock.delay) await new Promise((r) => setTimeout(r, mock.delay));
|
|
698
1286
|
return route.fulfill({
|
|
699
1287
|
status: mock.status,
|
|
@@ -702,19 +1290,26 @@ async function setupMockInterception(page, devServerUrl, mockConfig, authConfig)
|
|
|
702
1290
|
});
|
|
703
1291
|
}
|
|
704
1292
|
}
|
|
1293
|
+
if (isApiUrl(url)) {
|
|
1294
|
+
return route.fulfill({
|
|
1295
|
+
status: 200,
|
|
1296
|
+
contentType: "application/json",
|
|
1297
|
+
body: JSON.stringify({ data: [], message: "ok" })
|
|
1298
|
+
});
|
|
1299
|
+
}
|
|
705
1300
|
return route.continue();
|
|
706
1301
|
});
|
|
707
1302
|
}
|
|
708
1303
|
async function renderPage(context, devServerUrl, task, outputDir, options, viewport) {
|
|
709
1304
|
const { route, mockConfig, authConfig } = task;
|
|
710
|
-
const routeSlug =
|
|
711
|
-
const stateDir =
|
|
712
|
-
await
|
|
1305
|
+
const routeSlug = slugifyRoute2(route.urlPath);
|
|
1306
|
+
const stateDir = join6(outputDir, "renders", routeSlug);
|
|
1307
|
+
await mkdir2(stateDir, { recursive: true });
|
|
713
1308
|
const filePrefix = viewport ? `${mockConfig.stateName}_${viewport.name}` : mockConfig.stateName;
|
|
714
|
-
const htmlRelPath =
|
|
715
|
-
const pngRelPath =
|
|
716
|
-
const htmlAbsPath =
|
|
717
|
-
const pngAbsPath =
|
|
1309
|
+
const htmlRelPath = join6("renders", routeSlug, `${filePrefix}.html`);
|
|
1310
|
+
const pngRelPath = join6("renders", routeSlug, `${filePrefix}.png`);
|
|
1311
|
+
const htmlAbsPath = join6(outputDir, htmlRelPath);
|
|
1312
|
+
const pngAbsPath = join6(outputDir, pngRelPath);
|
|
718
1313
|
const page = await context.newPage();
|
|
719
1314
|
if (viewport) {
|
|
720
1315
|
await page.setViewportSize({ width: viewport.width, height: viewport.height });
|
|
@@ -728,42 +1323,47 @@ async function renderPage(context, devServerUrl, task, outputDir, options, viewp
|
|
|
728
1323
|
timeout: options.pageTimeout
|
|
729
1324
|
});
|
|
730
1325
|
await page.waitForTimeout(options.settleTime);
|
|
731
|
-
await page
|
|
732
|
-
const links = document.querySelectorAll('link[rel="stylesheet"]');
|
|
733
|
-
links.forEach(link => {
|
|
734
|
-
try {
|
|
735
|
-
const href = link.getAttribute('href');
|
|
736
|
-
if (!href) return;
|
|
737
|
-
for (const sheet of document.styleSheets) {
|
|
738
|
-
if (sheet.href && sheet.href.includes(href.replace(/^\\//, ''))) {
|
|
739
|
-
const rules = Array.from(sheet.cssRules).map(r => r.cssText).join('\\n');
|
|
740
|
-
const style = document.createElement('style');
|
|
741
|
-
style.textContent = rules;
|
|
742
|
-
link.parentNode.replaceChild(style, link);
|
|
743
|
-
break;
|
|
744
|
-
}
|
|
745
|
-
}
|
|
746
|
-
} catch (e) {}
|
|
747
|
-
});
|
|
748
|
-
document.querySelectorAll('script').forEach(s => s.remove());
|
|
749
|
-
})()`);
|
|
1326
|
+
await inlineStylesAndCleanup(page);
|
|
750
1327
|
const html = await page.content();
|
|
751
|
-
await
|
|
1328
|
+
await writeFile2(htmlAbsPath, html, "utf-8");
|
|
752
1329
|
await page.screenshot({ path: pngAbsPath, fullPage: true });
|
|
1330
|
+
const interactionStates = ["success", "empty"];
|
|
1331
|
+
const shouldCapture = options.captureInteractionStates !== false && interactionStates.includes(mockConfig.stateName);
|
|
1332
|
+
let interactions;
|
|
1333
|
+
if (shouldCapture) {
|
|
1334
|
+
await page.goto(fullUrl, { waitUntil: "networkidle", timeout: options.pageTimeout });
|
|
1335
|
+
await page.waitForTimeout(options.settleTime);
|
|
1336
|
+
const interactionResults = await captureInteractions(
|
|
1337
|
+
page,
|
|
1338
|
+
fullUrl,
|
|
1339
|
+
route,
|
|
1340
|
+
mockConfig.stateName,
|
|
1341
|
+
outputDir,
|
|
1342
|
+
{ maxInteractions: options.maxInteractions, settleTime: 500 }
|
|
1343
|
+
);
|
|
1344
|
+
const successful = interactionResults.filter((r) => r.success);
|
|
1345
|
+
if (successful.length > 0) {
|
|
1346
|
+
interactions = successful.map((r) => ({
|
|
1347
|
+
description: r.elementDescription,
|
|
1348
|
+
htmlPath: r.htmlPath
|
|
1349
|
+
}));
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
753
1352
|
return {
|
|
754
1353
|
route,
|
|
755
1354
|
stateName: mockConfig.stateName,
|
|
756
1355
|
htmlPath: htmlRelPath,
|
|
757
1356
|
screenshotPath: pngRelPath,
|
|
758
1357
|
success: true,
|
|
759
|
-
viewportName: viewport?.name
|
|
1358
|
+
viewportName: viewport?.name,
|
|
1359
|
+
interactions
|
|
760
1360
|
};
|
|
761
1361
|
} catch (err) {
|
|
762
1362
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
763
1363
|
const errorHtml = `<!DOCTYPE html><html><body style="display:flex;align-items:center;justify-content:center;height:100vh;font-family:sans-serif;color:#666;">
|
|
764
1364
|
<div style="text-align:center"><h2>Render Failed</h2><p>${route.urlPath} [${mockConfig.stateName}]</p><pre style="color:#c00">${errorMsg}</pre></div>
|
|
765
1365
|
</body></html>`;
|
|
766
|
-
await
|
|
1366
|
+
await writeFile2(htmlAbsPath, errorHtml, "utf-8").catch(() => {
|
|
767
1367
|
});
|
|
768
1368
|
return {
|
|
769
1369
|
route,
|
|
@@ -820,7 +1420,8 @@ function buildManifest(results, projectName, viewports) {
|
|
|
820
1420
|
status: result.success ? "ok" : "error",
|
|
821
1421
|
error: result.error,
|
|
822
1422
|
viewport: result.viewportName,
|
|
823
|
-
viewportWidth: result.viewportName ? viewportWidthMap.get(result.viewportName) : void 0
|
|
1423
|
+
viewportWidth: result.viewportName ? viewportWidthMap.get(result.viewportName) : void 0,
|
|
1424
|
+
interactions: result.interactions
|
|
824
1425
|
});
|
|
825
1426
|
}
|
|
826
1427
|
return {
|
|
@@ -832,16 +1433,18 @@ function buildManifest(results, projectName, viewports) {
|
|
|
832
1433
|
async function preRenderPages(tasks, options) {
|
|
833
1434
|
const {
|
|
834
1435
|
projectRoot,
|
|
835
|
-
outputDir =
|
|
1436
|
+
outputDir = join6(projectRoot, ".c2d"),
|
|
836
1437
|
concurrency = DEFAULT_CONCURRENCY,
|
|
837
1438
|
pageTimeout = DEFAULT_PAGE_TIMEOUT,
|
|
838
1439
|
settleTime = DEFAULT_SETTLE_TIME,
|
|
839
1440
|
viewportWidth = DEFAULT_VIEWPORT.width,
|
|
840
|
-
viewportHeight = DEFAULT_VIEWPORT.height
|
|
1441
|
+
viewportHeight = DEFAULT_VIEWPORT.height,
|
|
1442
|
+
captureInteractions: captureInteractionStates = true,
|
|
1443
|
+
maxInteractions
|
|
841
1444
|
} = options;
|
|
842
1445
|
const viewports = options.viewports;
|
|
843
1446
|
const useMultiViewport = viewports && viewports.length > 0;
|
|
844
|
-
await
|
|
1447
|
+
await mkdir2(outputDir, { recursive: true });
|
|
845
1448
|
let devServer = null;
|
|
846
1449
|
try {
|
|
847
1450
|
devServer = await startDevServer(projectRoot, {
|
|
@@ -864,20 +1467,20 @@ async function preRenderPages(tasks, options) {
|
|
|
864
1467
|
results = await processWithConcurrency(
|
|
865
1468
|
expandedItems,
|
|
866
1469
|
concurrency,
|
|
867
|
-
({ task, viewport: vp }) => renderPage(context, devServer.url, task, outputDir, { pageTimeout, settleTime }, vp),
|
|
1470
|
+
({ task, viewport: vp }) => renderPage(context, devServer.url, task, outputDir, { pageTimeout, settleTime, captureInteractionStates, maxInteractions }, vp),
|
|
868
1471
|
options.onProgress
|
|
869
1472
|
);
|
|
870
1473
|
} else {
|
|
871
1474
|
results = await processWithConcurrency(
|
|
872
1475
|
tasks,
|
|
873
1476
|
concurrency,
|
|
874
|
-
(task) => renderPage(context, devServer.url, task, outputDir, { pageTimeout, settleTime }),
|
|
1477
|
+
(task) => renderPage(context, devServer.url, task, outputDir, { pageTimeout, settleTime, captureInteractionStates, maxInteractions }),
|
|
875
1478
|
options.onProgress
|
|
876
1479
|
);
|
|
877
1480
|
}
|
|
878
1481
|
let projectName = "unknown";
|
|
879
1482
|
try {
|
|
880
|
-
const pkg = await import(
|
|
1483
|
+
const pkg = await import(join6(projectRoot, "package.json"), { with: { type: "json" } });
|
|
881
1484
|
projectName = pkg.default?.name || "unknown";
|
|
882
1485
|
} catch {
|
|
883
1486
|
}
|
|
@@ -886,8 +1489,8 @@ async function preRenderPages(tasks, options) {
|
|
|
886
1489
|
projectName,
|
|
887
1490
|
useMultiViewport ? viewports : void 0
|
|
888
1491
|
);
|
|
889
|
-
await
|
|
890
|
-
|
|
1492
|
+
await writeFile2(
|
|
1493
|
+
join6(outputDir, "manifest.json"),
|
|
891
1494
|
JSON.stringify(manifest, null, 2),
|
|
892
1495
|
"utf-8"
|
|
893
1496
|
);
|
|
@@ -905,14 +1508,14 @@ async function preRenderPages(tasks, options) {
|
|
|
905
1508
|
|
|
906
1509
|
// src/server/canvas-server.ts
|
|
907
1510
|
import { createServer as createServer2 } from "http";
|
|
908
|
-
import { join as
|
|
1511
|
+
import { join as join8 } from "path";
|
|
909
1512
|
import { existsSync as existsSync5 } from "fs";
|
|
910
1513
|
import sirv from "sirv";
|
|
911
1514
|
|
|
912
1515
|
// src/server/api-routes.ts
|
|
913
|
-
import { readFile as
|
|
1516
|
+
import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
|
|
914
1517
|
import { existsSync as existsSync4 } from "fs";
|
|
915
|
-
import { join as
|
|
1518
|
+
import { join as join7, dirname as dirname2 } from "path";
|
|
916
1519
|
import { randomUUID } from "crypto";
|
|
917
1520
|
async function handleApiRequest(req, res, c2dDir) {
|
|
918
1521
|
const url = req.url ?? "/";
|
|
@@ -945,12 +1548,12 @@ async function handleApiRequest(req, res, c2dDir) {
|
|
|
945
1548
|
return false;
|
|
946
1549
|
}
|
|
947
1550
|
async function handleGetManifest(res, c2dDir) {
|
|
948
|
-
const manifestPath =
|
|
1551
|
+
const manifestPath = join7(c2dDir, "manifest.json");
|
|
949
1552
|
if (!existsSync4(manifestPath)) {
|
|
950
1553
|
sendJson(res, 404, { error: "manifest.json not found" });
|
|
951
1554
|
return;
|
|
952
1555
|
}
|
|
953
|
-
const data = await
|
|
1556
|
+
const data = await readFile4(manifestPath, "utf-8");
|
|
954
1557
|
sendRawJson(res, 200, data);
|
|
955
1558
|
}
|
|
956
1559
|
async function handleGetComments(res, c2dDir) {
|
|
@@ -1015,11 +1618,11 @@ async function handlePostDrawings(req, res, c2dDir) {
|
|
|
1015
1618
|
sendJson(res, 200, { ok: true });
|
|
1016
1619
|
}
|
|
1017
1620
|
async function loadDrawings(c2dDir) {
|
|
1018
|
-
const drawingsPath =
|
|
1621
|
+
const drawingsPath = join7(c2dDir, "drawings.json");
|
|
1019
1622
|
if (!existsSync4(drawingsPath)) {
|
|
1020
1623
|
return [];
|
|
1021
1624
|
}
|
|
1022
|
-
const data = await
|
|
1625
|
+
const data = await readFile4(drawingsPath, "utf-8");
|
|
1023
1626
|
try {
|
|
1024
1627
|
const parsed = JSON.parse(data);
|
|
1025
1628
|
return Array.isArray(parsed) ? parsed : [];
|
|
@@ -1028,12 +1631,12 @@ async function loadDrawings(c2dDir) {
|
|
|
1028
1631
|
}
|
|
1029
1632
|
}
|
|
1030
1633
|
async function saveDrawings(c2dDir, drawings) {
|
|
1031
|
-
const drawingsPath =
|
|
1634
|
+
const drawingsPath = join7(c2dDir, "drawings.json");
|
|
1032
1635
|
const dir = dirname2(drawingsPath);
|
|
1033
1636
|
if (!existsSync4(dir)) {
|
|
1034
|
-
await
|
|
1637
|
+
await mkdir3(dir, { recursive: true });
|
|
1035
1638
|
}
|
|
1036
|
-
await
|
|
1639
|
+
await writeFile3(drawingsPath, JSON.stringify(drawings, null, 2), "utf-8");
|
|
1037
1640
|
}
|
|
1038
1641
|
function isValidCommentInput(value) {
|
|
1039
1642
|
if (typeof value !== "object" || value === null) return false;
|
|
@@ -1041,11 +1644,11 @@ function isValidCommentInput(value) {
|
|
|
1041
1644
|
return typeof obj.x === "number" && typeof obj.y === "number" && typeof obj.text === "string" && typeof obj.author === "string";
|
|
1042
1645
|
}
|
|
1043
1646
|
async function loadComments(c2dDir) {
|
|
1044
|
-
const commentsPath =
|
|
1647
|
+
const commentsPath = join7(c2dDir, "comments.json");
|
|
1045
1648
|
if (!existsSync4(commentsPath)) {
|
|
1046
1649
|
return [];
|
|
1047
1650
|
}
|
|
1048
|
-
const data = await
|
|
1651
|
+
const data = await readFile4(commentsPath, "utf-8");
|
|
1049
1652
|
try {
|
|
1050
1653
|
const parsed = JSON.parse(data);
|
|
1051
1654
|
return Array.isArray(parsed) ? parsed : [];
|
|
@@ -1054,12 +1657,12 @@ async function loadComments(c2dDir) {
|
|
|
1054
1657
|
}
|
|
1055
1658
|
}
|
|
1056
1659
|
async function saveComments(c2dDir, comments) {
|
|
1057
|
-
const commentsPath =
|
|
1660
|
+
const commentsPath = join7(c2dDir, "comments.json");
|
|
1058
1661
|
const dir = dirname2(commentsPath);
|
|
1059
1662
|
if (!existsSync4(dir)) {
|
|
1060
|
-
await
|
|
1663
|
+
await mkdir3(dir, { recursive: true });
|
|
1061
1664
|
}
|
|
1062
|
-
await
|
|
1665
|
+
await writeFile3(commentsPath, JSON.stringify(comments, null, 2), "utf-8");
|
|
1063
1666
|
}
|
|
1064
1667
|
function readRequestBody(req) {
|
|
1065
1668
|
return new Promise((resolve2, reject) => {
|
|
@@ -1109,9 +1712,9 @@ function tryListen(requestHandler, port) {
|
|
|
1109
1712
|
async function startCanvasServer(options) {
|
|
1110
1713
|
const { port = 4800, canvasDir, c2dDir, projectRoot } = options;
|
|
1111
1714
|
const canvasHandler = sirv(canvasDir, { single: true, dev: true });
|
|
1112
|
-
const rendersDir =
|
|
1715
|
+
const rendersDir = join8(c2dDir, "renders");
|
|
1113
1716
|
const rendersHandler = sirv(rendersDir, { dev: true });
|
|
1114
|
-
const publicDir = projectRoot ?
|
|
1717
|
+
const publicDir = projectRoot ? join8(projectRoot, "public") : null;
|
|
1115
1718
|
const publicHandler = publicDir && existsSync5(publicDir) ? sirv(publicDir, { dev: true }) : null;
|
|
1116
1719
|
const requestHandler = async (req, res) => {
|
|
1117
1720
|
const url = req.url ?? "/";
|
|
@@ -1176,12 +1779,12 @@ async function startCanvasServer(options) {
|
|
|
1176
1779
|
|
|
1177
1780
|
// src/config.ts
|
|
1178
1781
|
import { existsSync as existsSync6 } from "fs";
|
|
1179
|
-
import { readFile as
|
|
1180
|
-
import { join as
|
|
1782
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1783
|
+
import { join as join9 } from "path";
|
|
1181
1784
|
var DEFAULT_PORT = 4800;
|
|
1182
1785
|
async function loadConfig(projectRoot) {
|
|
1183
1786
|
let fileConfig = {};
|
|
1184
|
-
const configPath =
|
|
1787
|
+
const configPath = join9(projectRoot, "c2d.config.js");
|
|
1185
1788
|
if (existsSync6(configPath)) {
|
|
1186
1789
|
try {
|
|
1187
1790
|
const mod = await import(configPath);
|
|
@@ -1198,24 +1801,51 @@ async function loadConfig(projectRoot) {
|
|
|
1198
1801
|
devServerCommand: fileConfig.devServerCommand
|
|
1199
1802
|
};
|
|
1200
1803
|
}
|
|
1201
|
-
async function
|
|
1202
|
-
const hasNextConfig = existsSync6(join8(projectRoot, "next.config.ts")) || existsSync6(join8(projectRoot, "next.config.js")) || existsSync6(join8(projectRoot, "next.config.mjs"));
|
|
1203
|
-
let appDir = join8(projectRoot, "app");
|
|
1204
|
-
let hasAppDir = existsSync6(appDir);
|
|
1205
|
-
if (!hasAppDir) {
|
|
1206
|
-
appDir = join8(projectRoot, "src", "app");
|
|
1207
|
-
hasAppDir = existsSync6(appDir);
|
|
1208
|
-
}
|
|
1804
|
+
async function detectProject(projectRoot) {
|
|
1209
1805
|
let projectName = "unknown";
|
|
1210
1806
|
try {
|
|
1211
|
-
const pkg = JSON.parse(await
|
|
1807
|
+
const pkg = JSON.parse(await readFile5(join9(projectRoot, "package.json"), "utf-8"));
|
|
1212
1808
|
projectName = pkg.name || "unknown";
|
|
1213
1809
|
} catch {
|
|
1214
1810
|
}
|
|
1811
|
+
const hasNextConfig = existsSync6(join9(projectRoot, "next.config.ts")) || existsSync6(join9(projectRoot, "next.config.js")) || existsSync6(join9(projectRoot, "next.config.mjs"));
|
|
1812
|
+
if (hasNextConfig) {
|
|
1813
|
+
let appDir = join9(projectRoot, "app");
|
|
1814
|
+
let hasAppDir = existsSync6(appDir);
|
|
1815
|
+
if (!hasAppDir) {
|
|
1816
|
+
appDir = join9(projectRoot, "src", "app");
|
|
1817
|
+
hasAppDir = existsSync6(appDir);
|
|
1818
|
+
}
|
|
1819
|
+
if (hasAppDir) {
|
|
1820
|
+
return { isSupported: true, projectType: "nextjs-app", appDir, projectRoot, projectName };
|
|
1821
|
+
}
|
|
1822
|
+
appDir = join9(projectRoot, "pages");
|
|
1823
|
+
hasAppDir = existsSync6(appDir);
|
|
1824
|
+
if (!hasAppDir) {
|
|
1825
|
+
appDir = join9(projectRoot, "src", "pages");
|
|
1826
|
+
hasAppDir = existsSync6(appDir);
|
|
1827
|
+
}
|
|
1828
|
+
if (hasAppDir) {
|
|
1829
|
+
return { isSupported: true, projectType: "nextjs-pages", appDir, projectRoot, projectName };
|
|
1830
|
+
}
|
|
1831
|
+
return { isSupported: false, projectType: "unknown", appDir: null, projectRoot, projectName };
|
|
1832
|
+
}
|
|
1833
|
+
try {
|
|
1834
|
+
const pkg = JSON.parse(await readFile5(join9(projectRoot, "package.json"), "utf-8"));
|
|
1835
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1836
|
+
if ("react-router-dom" in deps || "react-router" in deps) {
|
|
1837
|
+
return { isSupported: true, projectType: "react-router", appDir: null, projectRoot, projectName };
|
|
1838
|
+
}
|
|
1839
|
+
} catch {
|
|
1840
|
+
}
|
|
1841
|
+
return { isSupported: false, projectType: "unknown", appDir: null, projectRoot, projectName };
|
|
1842
|
+
}
|
|
1843
|
+
async function detectNextJsProject(projectRoot) {
|
|
1844
|
+
const result = await detectProject(projectRoot);
|
|
1215
1845
|
return {
|
|
1216
|
-
isNextJs:
|
|
1217
|
-
appDir:
|
|
1218
|
-
projectName
|
|
1846
|
+
isNextJs: result.projectType === "nextjs-app" || result.projectType === "nextjs-pages",
|
|
1847
|
+
appDir: result.appDir,
|
|
1848
|
+
projectName: result.projectName
|
|
1219
1849
|
};
|
|
1220
1850
|
}
|
|
1221
1851
|
|
|
@@ -1262,21 +1892,23 @@ async function runScan(options) {
|
|
|
1262
1892
|
banner();
|
|
1263
1893
|
const config = await loadConfig(projectRoot);
|
|
1264
1894
|
header("Detecting project...");
|
|
1265
|
-
const project = await
|
|
1266
|
-
if (!project.
|
|
1267
|
-
error("
|
|
1268
|
-
log("Code to Design
|
|
1895
|
+
const project = await detectProject(projectRoot);
|
|
1896
|
+
if (!project.isSupported) {
|
|
1897
|
+
error("Unsupported project type.");
|
|
1898
|
+
log("Code to Design supports Next.js (App Router / Pages Router) and React Router (Vite) projects.");
|
|
1269
1899
|
process.exit(1);
|
|
1270
1900
|
}
|
|
1271
|
-
if (!project.appDir) {
|
|
1272
|
-
error("No app/ directory found.
|
|
1901
|
+
if (project.projectType !== "react-router" && !project.appDir) {
|
|
1902
|
+
error("No app/ or pages/ directory found.");
|
|
1273
1903
|
process.exit(1);
|
|
1274
1904
|
}
|
|
1275
|
-
success(`Project: ${project.projectName}`);
|
|
1276
|
-
|
|
1277
|
-
|
|
1905
|
+
success(`Project: ${project.projectName} (${project.projectType})`);
|
|
1906
|
+
if (project.appDir) {
|
|
1907
|
+
success(`App directory: ${project.appDir}`);
|
|
1908
|
+
}
|
|
1909
|
+
const c2dDir = join10(projectRoot, ".c2d");
|
|
1278
1910
|
if (skipRender) {
|
|
1279
|
-
if (!existsSync7(
|
|
1911
|
+
if (!existsSync7(join10(c2dDir, "manifest.json"))) {
|
|
1280
1912
|
error("No previous renders found. Run without --skip-render first.");
|
|
1281
1913
|
process.exit(1);
|
|
1282
1914
|
}
|
|
@@ -1285,7 +1917,9 @@ async function runScan(options) {
|
|
|
1285
1917
|
return;
|
|
1286
1918
|
}
|
|
1287
1919
|
header("Discovering routes...");
|
|
1288
|
-
const
|
|
1920
|
+
const scanDir2 = project.projectType === "react-router" ? project.projectRoot : project.appDir;
|
|
1921
|
+
const routerType = project.projectType === "react-router" ? "react-router" : project.projectType === "nextjs-pages" ? "pages-router" : "app-router";
|
|
1922
|
+
const routes = await scanRoutes({ appDir: scanDir2, routerType });
|
|
1289
1923
|
if (routes.length === 0) {
|
|
1290
1924
|
error("No routes found in app/ directory.");
|
|
1291
1925
|
process.exit(1);
|
|
@@ -1328,10 +1962,10 @@ async function runScan(options) {
|
|
|
1328
1962
|
success("Using fallback mocks (no API key or no API dependencies)");
|
|
1329
1963
|
}
|
|
1330
1964
|
header(`Pre-rendering ${renderTasks.length} page states...`);
|
|
1331
|
-
if (existsSync7(
|
|
1332
|
-
await rm(
|
|
1965
|
+
if (existsSync7(join10(c2dDir, "renders"))) {
|
|
1966
|
+
await rm(join10(c2dDir, "renders"), { recursive: true });
|
|
1333
1967
|
}
|
|
1334
|
-
await
|
|
1968
|
+
await mkdir4(c2dDir, { recursive: true });
|
|
1335
1969
|
const { results, manifest } = await preRenderPages(renderTasks, {
|
|
1336
1970
|
projectRoot,
|
|
1337
1971
|
outputDir: c2dDir,
|
|
@@ -1355,7 +1989,8 @@ async function runScan(options) {
|
|
|
1355
1989
|
}
|
|
1356
1990
|
if (watch) {
|
|
1357
1991
|
const server = await startServerNonBlocking(c2dDir, config.port, open, projectRoot);
|
|
1358
|
-
|
|
1992
|
+
const watchDir = project.projectType === "react-router" ? join10(projectRoot, "src") : project.appDir;
|
|
1993
|
+
watchAndRerender(projectRoot, watchDir, c2dDir, config, routerType);
|
|
1359
1994
|
await new Promise((resolve2) => {
|
|
1360
1995
|
const shutdown = async () => {
|
|
1361
1996
|
log("\nShutting down...");
|
|
@@ -1431,20 +2066,20 @@ async function resolveCanvasDir(c2dDir) {
|
|
|
1431
2066
|
const __filename = fileURLToPath(import.meta.url);
|
|
1432
2067
|
let dir = dirname3(__filename);
|
|
1433
2068
|
for (let i = 0; i < 5; i++) {
|
|
1434
|
-
const candidate =
|
|
1435
|
-
if (existsSync7(candidate) && existsSync7(
|
|
2069
|
+
const candidate = join10(dir, "canvas-dist");
|
|
2070
|
+
if (existsSync7(candidate) && existsSync7(join10(candidate, "index.html"))) {
|
|
1436
2071
|
return candidate;
|
|
1437
2072
|
}
|
|
1438
2073
|
dir = dirname3(dir);
|
|
1439
2074
|
}
|
|
1440
|
-
const monorepoDev =
|
|
1441
|
-
if (existsSync7(monorepoDev) && existsSync7(
|
|
2075
|
+
const monorepoDev = join10(dirname3(__filename), "..", "..", "..", "..", "apps", "canvas", "dist");
|
|
2076
|
+
if (existsSync7(monorepoDev) && existsSync7(join10(monorepoDev, "index.html"))) {
|
|
1442
2077
|
return monorepoDev;
|
|
1443
2078
|
}
|
|
1444
|
-
const placeholder =
|
|
1445
|
-
await
|
|
1446
|
-
const { writeFile:
|
|
1447
|
-
await
|
|
2079
|
+
const placeholder = join10(c2dDir, "_canvas");
|
|
2080
|
+
await mkdir4(placeholder, { recursive: true });
|
|
2081
|
+
const { writeFile: writeFile4 } = await import("fs/promises");
|
|
2082
|
+
await writeFile4(join10(placeholder, "index.html"), `<!DOCTYPE html><html><body>
|
|
1448
2083
|
<h1>Code to Design</h1>
|
|
1449
2084
|
<p>Canvas app not built. Run: <code>cd apps/canvas && npx vite build</code></p>
|
|
1450
2085
|
<p><a href="/api/manifest">View Manifest</a></p>
|
|
@@ -1459,7 +2094,7 @@ function shouldIgnoreFile(filename) {
|
|
|
1459
2094
|
const ext = filename.slice(filename.lastIndexOf("."));
|
|
1460
2095
|
return !WATCH_EXTENSIONS.has(ext);
|
|
1461
2096
|
}
|
|
1462
|
-
function watchAndRerender(projectRoot, appDir, c2dDir, config) {
|
|
2097
|
+
function watchAndRerender(projectRoot, appDir, c2dDir, config, routerType) {
|
|
1463
2098
|
let debounceTimer;
|
|
1464
2099
|
let isRendering = false;
|
|
1465
2100
|
log(`Watching ${appDir} for changes...`);
|
|
@@ -1472,7 +2107,7 @@ function watchAndRerender(projectRoot, appDir, c2dDir, config) {
|
|
|
1472
2107
|
log(`
|
|
1473
2108
|
File changed: ${filename}. Re-rendering...`);
|
|
1474
2109
|
try {
|
|
1475
|
-
const routes = await scanRoutes({ appDir });
|
|
2110
|
+
const routes = await scanRoutes({ appDir, routerType });
|
|
1476
2111
|
const filteredRoutes = routes.filter(
|
|
1477
2112
|
(r) => !config.excludeRoutes.some((pattern) => r.urlPath.includes(pattern))
|
|
1478
2113
|
);
|
|
@@ -1489,10 +2124,10 @@ File changed: ${filename}. Re-rendering...`);
|
|
|
1489
2124
|
});
|
|
1490
2125
|
}
|
|
1491
2126
|
}
|
|
1492
|
-
if (existsSync7(
|
|
1493
|
-
await rm(
|
|
2127
|
+
if (existsSync7(join10(c2dDir, "renders"))) {
|
|
2128
|
+
await rm(join10(c2dDir, "renders"), { recursive: true });
|
|
1494
2129
|
}
|
|
1495
|
-
await
|
|
2130
|
+
await mkdir4(c2dDir, { recursive: true });
|
|
1496
2131
|
const { results } = await preRenderPages(renderTasks, {
|
|
1497
2132
|
projectRoot,
|
|
1498
2133
|
outputDir: c2dDir,
|
|
@@ -1512,7 +2147,8 @@ File changed: ${filename}. Re-rendering...`);
|
|
|
1512
2147
|
export {
|
|
1513
2148
|
startCanvasServer,
|
|
1514
2149
|
loadConfig,
|
|
2150
|
+
detectProject,
|
|
1515
2151
|
detectNextJsProject,
|
|
1516
2152
|
runScan
|
|
1517
2153
|
};
|
|
1518
|
-
//# sourceMappingURL=chunk-
|
|
2154
|
+
//# sourceMappingURL=chunk-AL6L2SWU.js.map
|