code-to-design 0.1.4 → 0.2.0
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-BHEMO5RW.js → chunk-HYFKUTC3.js} +509 -122
- package/dist/chunk-HYFKUTC3.js.map +1 -0
- package/dist/commands/scan.js +1 -1
- package/dist/index.d.ts +17 -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-BHEMO5RW.js.map +0 -1
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
// src/commands/scan.ts
|
|
2
|
-
import { join as
|
|
3
|
-
import { rm, mkdir as
|
|
4
|
-
import { existsSync as
|
|
2
|
+
import { join as join10 } from "path";
|
|
3
|
+
import { rm, mkdir as mkdir4 } from "fs/promises";
|
|
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;
|
|
@@ -198,7 +363,7 @@ async function readPathAliases(projectRoot) {
|
|
|
198
363
|
const configPath = join3(projectRoot, filename);
|
|
199
364
|
if (!existsSync2(configPath)) continue;
|
|
200
365
|
try {
|
|
201
|
-
const content = await
|
|
366
|
+
const content = await readFile3(configPath, "utf-8");
|
|
202
367
|
const stripped = content.replace(
|
|
203
368
|
/"(?:[^"\\]|\\.)*"|\/\/.*$|\/\*[\s\S]*?\*\//gm,
|
|
204
369
|
(match) => match.startsWith('"') ? match : ""
|
|
@@ -242,7 +407,7 @@ async function traceImports(filePath, projectRoot, aliases, maxDepth, visited =
|
|
|
242
407
|
visited.add(filePath);
|
|
243
408
|
let source;
|
|
244
409
|
try {
|
|
245
|
-
source = await
|
|
410
|
+
source = await readFile3(filePath, "utf-8");
|
|
246
411
|
} catch {
|
|
247
412
|
return results;
|
|
248
413
|
}
|
|
@@ -529,8 +694,8 @@ async function generateMocks(analysis, options) {
|
|
|
529
694
|
|
|
530
695
|
// ../core/src/render/pre-renderer.ts
|
|
531
696
|
import { chromium } from "playwright";
|
|
532
|
-
import { mkdir, writeFile } from "fs/promises";
|
|
533
|
-
import { join as
|
|
697
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
698
|
+
import { join as join6 } from "path";
|
|
534
699
|
|
|
535
700
|
// ../core/src/render/dev-server.ts
|
|
536
701
|
import { spawn } from "child_process";
|
|
@@ -633,6 +798,173 @@ ${stderr.slice(-500)}`));
|
|
|
633
798
|
};
|
|
634
799
|
}
|
|
635
800
|
|
|
801
|
+
// ../core/src/render/style-inliner.ts
|
|
802
|
+
async function inlineStylesAndCleanup(page) {
|
|
803
|
+
await page.evaluate(`(() => {
|
|
804
|
+
const links = document.querySelectorAll('link[rel="stylesheet"]');
|
|
805
|
+
links.forEach(link => {
|
|
806
|
+
try {
|
|
807
|
+
const href = link.getAttribute('href');
|
|
808
|
+
if (!href) return;
|
|
809
|
+
for (const sheet of document.styleSheets) {
|
|
810
|
+
if (sheet.href && sheet.href.includes(href.replace(/^\\//, ''))) {
|
|
811
|
+
const rules = Array.from(sheet.cssRules).map(r => r.cssText).join('\\n');
|
|
812
|
+
const style = document.createElement('style');
|
|
813
|
+
style.textContent = rules;
|
|
814
|
+
link.parentNode.replaceChild(style, link);
|
|
815
|
+
break;
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
} catch (e) {}
|
|
819
|
+
});
|
|
820
|
+
document.querySelectorAll('script').forEach(s => s.remove());
|
|
821
|
+
})()`);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
// ../core/src/render/interaction-capturer.ts
|
|
825
|
+
import { mkdir, writeFile } from "fs/promises";
|
|
826
|
+
import { join as join5 } from "path";
|
|
827
|
+
function slugifyRoute(urlPath) {
|
|
828
|
+
if (urlPath === "/") return "index";
|
|
829
|
+
return urlPath.replace(/^\//, "").replace(/\//g, "-").replace(/:/g, "_").replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
830
|
+
}
|
|
831
|
+
async function findClickableElements(page, maxElements) {
|
|
832
|
+
return page.evaluate(`((max) => {
|
|
833
|
+
const selectors = [
|
|
834
|
+
'[role="tab"]:not([aria-selected="true"]):not([aria-disabled="true"])',
|
|
835
|
+
'[role="button"]:not([aria-disabled="true"]):not([disabled])',
|
|
836
|
+
'button:not([disabled]):not([type="submit"])',
|
|
837
|
+
'[data-tab]:not(.active):not(.selected)',
|
|
838
|
+
'.tab:not(.active):not(.selected)',
|
|
839
|
+
'a[href="#"]:not(.active)',
|
|
840
|
+
'a[href^="#"]:not([href="#"]):not(.active)',
|
|
841
|
+
];
|
|
842
|
+
|
|
843
|
+
const seen = new Set();
|
|
844
|
+
const results = [];
|
|
845
|
+
|
|
846
|
+
for (const sel of selectors) {
|
|
847
|
+
if (results.length >= max) break;
|
|
848
|
+
const elements = document.querySelectorAll(sel);
|
|
849
|
+
|
|
850
|
+
for (const el of elements) {
|
|
851
|
+
if (results.length >= max) break;
|
|
852
|
+
if (seen.has(el)) continue;
|
|
853
|
+
seen.add(el);
|
|
854
|
+
|
|
855
|
+
const rect = el.getBoundingClientRect();
|
|
856
|
+
if (rect.width === 0 || rect.height === 0) continue;
|
|
857
|
+
const style = window.getComputedStyle(el);
|
|
858
|
+
if (style.display === 'none' || style.visibility === 'hidden') continue;
|
|
859
|
+
|
|
860
|
+
if (el.tagName === 'A') {
|
|
861
|
+
const href = el.getAttribute('href') || '';
|
|
862
|
+
if (href.startsWith('http') || href.startsWith('//')) continue;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
if (el.tagName === 'BUTTON' && el.type === 'submit') continue;
|
|
866
|
+
|
|
867
|
+
const text = (el.textContent || '').trim().slice(0, 50);
|
|
868
|
+
const ariaLabel = el.getAttribute('aria-label');
|
|
869
|
+
const role = el.getAttribute('role');
|
|
870
|
+
const tag = el.tagName.toLowerCase();
|
|
871
|
+
|
|
872
|
+
let desc = '';
|
|
873
|
+
if (role === 'tab') desc = 'Tab: ' + (ariaLabel || text || 'unnamed');
|
|
874
|
+
else if (role === 'button') desc = 'Button: ' + (ariaLabel || text || 'unnamed');
|
|
875
|
+
else if (tag === 'button') desc = 'Button: ' + (ariaLabel || text || 'unnamed');
|
|
876
|
+
else desc = 'Clickable: ' + (ariaLabel || text || 'unnamed');
|
|
877
|
+
|
|
878
|
+
let uniqueSelector = '';
|
|
879
|
+
const id = el.getAttribute('id');
|
|
880
|
+
if (id) {
|
|
881
|
+
uniqueSelector = '#' + CSS.escape(id);
|
|
882
|
+
} else {
|
|
883
|
+
const dataTestId = el.getAttribute('data-testid');
|
|
884
|
+
if (dataTestId) {
|
|
885
|
+
uniqueSelector = '[data-testid="' + CSS.escape(dataTestId) + '"]';
|
|
886
|
+
} else {
|
|
887
|
+
const allMatching = document.querySelectorAll(sel);
|
|
888
|
+
const idx = Array.from(allMatching).indexOf(el);
|
|
889
|
+
uniqueSelector = '__INDEX__' + sel + '__' + idx;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
results.push({ selector: uniqueSelector, description: desc });
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
return results;
|
|
898
|
+
})(${maxElements})`);
|
|
899
|
+
}
|
|
900
|
+
async function captureInteractions(page, pageUrl, route, stateName, outputDir, options) {
|
|
901
|
+
const maxInteractions = options?.maxInteractions ?? 5;
|
|
902
|
+
const settleTime = options?.settleTime ?? 500;
|
|
903
|
+
const routeSlug = slugifyRoute(route.urlPath);
|
|
904
|
+
const stateDir = join5(outputDir, "renders", routeSlug);
|
|
905
|
+
await mkdir(stateDir, { recursive: true });
|
|
906
|
+
const clickables = await findClickableElements(page, maxInteractions);
|
|
907
|
+
if (clickables.length === 0) return [];
|
|
908
|
+
const results = [];
|
|
909
|
+
for (let i = 0; i < clickables.length; i++) {
|
|
910
|
+
const clickable = clickables[i];
|
|
911
|
+
const htmlRelPath = join5("renders", routeSlug, `${stateName}_interaction_${i}.html`);
|
|
912
|
+
const htmlAbsPath = join5(outputDir, htmlRelPath);
|
|
913
|
+
try {
|
|
914
|
+
let clicked = false;
|
|
915
|
+
if (clickable.selector.startsWith("__INDEX__")) {
|
|
916
|
+
const parts = clickable.selector.slice("__INDEX__".length);
|
|
917
|
+
const lastUnderscoreIdx = parts.lastIndexOf("__");
|
|
918
|
+
const sel = parts.slice(0, lastUnderscoreIdx);
|
|
919
|
+
const idx = parseInt(parts.slice(lastUnderscoreIdx + 2), 10);
|
|
920
|
+
const elements = await page.$$(sel);
|
|
921
|
+
if (elements[idx]) {
|
|
922
|
+
await elements[idx].click();
|
|
923
|
+
clicked = true;
|
|
924
|
+
}
|
|
925
|
+
} else {
|
|
926
|
+
const element = await page.$(clickable.selector);
|
|
927
|
+
if (element) {
|
|
928
|
+
await element.click();
|
|
929
|
+
clicked = true;
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
if (!clicked) {
|
|
933
|
+
results.push({
|
|
934
|
+
elementDescription: clickable.description,
|
|
935
|
+
htmlPath: htmlRelPath,
|
|
936
|
+
success: false,
|
|
937
|
+
error: "Element not found on re-query"
|
|
938
|
+
});
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
await page.waitForTimeout(settleTime);
|
|
942
|
+
await inlineStylesAndCleanup(page);
|
|
943
|
+
const html = await page.content();
|
|
944
|
+
await writeFile(htmlAbsPath, html, "utf-8");
|
|
945
|
+
results.push({
|
|
946
|
+
elementDescription: clickable.description,
|
|
947
|
+
htmlPath: htmlRelPath,
|
|
948
|
+
success: true
|
|
949
|
+
});
|
|
950
|
+
} catch (err) {
|
|
951
|
+
results.push({
|
|
952
|
+
elementDescription: clickable.description,
|
|
953
|
+
htmlPath: htmlRelPath,
|
|
954
|
+
success: false,
|
|
955
|
+
error: err instanceof Error ? err.message : String(err)
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
try {
|
|
959
|
+
await page.goto(pageUrl, { waitUntil: "networkidle", timeout: 1e4 });
|
|
960
|
+
await page.waitForTimeout(settleTime);
|
|
961
|
+
} catch {
|
|
962
|
+
break;
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
return results;
|
|
966
|
+
}
|
|
967
|
+
|
|
636
968
|
// ../core/src/render/pre-renderer.ts
|
|
637
969
|
var DEFAULT_CONCURRENCY = 3;
|
|
638
970
|
var DEFAULT_PAGE_TIMEOUT = 15e3;
|
|
@@ -650,7 +982,7 @@ function buildUrlPath(route, mockConfig) {
|
|
|
650
982
|
}
|
|
651
983
|
return path;
|
|
652
984
|
}
|
|
653
|
-
function
|
|
985
|
+
function slugifyRoute2(urlPath) {
|
|
654
986
|
if (urlPath === "/") return "index";
|
|
655
987
|
return urlPath.replace(/^\//, "").replace(/\//g, "-").replace(/:/g, "_").replace(/[^a-zA-Z0-9_-]/g, "_");
|
|
656
988
|
}
|
|
@@ -707,14 +1039,14 @@ async function setupMockInterception(page, devServerUrl, mockConfig, authConfig)
|
|
|
707
1039
|
}
|
|
708
1040
|
async function renderPage(context, devServerUrl, task, outputDir, options, viewport) {
|
|
709
1041
|
const { route, mockConfig, authConfig } = task;
|
|
710
|
-
const routeSlug =
|
|
711
|
-
const stateDir =
|
|
712
|
-
await
|
|
1042
|
+
const routeSlug = slugifyRoute2(route.urlPath);
|
|
1043
|
+
const stateDir = join6(outputDir, "renders", routeSlug);
|
|
1044
|
+
await mkdir2(stateDir, { recursive: true });
|
|
713
1045
|
const filePrefix = viewport ? `${mockConfig.stateName}_${viewport.name}` : mockConfig.stateName;
|
|
714
|
-
const htmlRelPath =
|
|
715
|
-
const pngRelPath =
|
|
716
|
-
const htmlAbsPath =
|
|
717
|
-
const pngAbsPath =
|
|
1046
|
+
const htmlRelPath = join6("renders", routeSlug, `${filePrefix}.html`);
|
|
1047
|
+
const pngRelPath = join6("renders", routeSlug, `${filePrefix}.png`);
|
|
1048
|
+
const htmlAbsPath = join6(outputDir, htmlRelPath);
|
|
1049
|
+
const pngAbsPath = join6(outputDir, pngRelPath);
|
|
718
1050
|
const page = await context.newPage();
|
|
719
1051
|
if (viewport) {
|
|
720
1052
|
await page.setViewportSize({ width: viewport.width, height: viewport.height });
|
|
@@ -728,42 +1060,47 @@ async function renderPage(context, devServerUrl, task, outputDir, options, viewp
|
|
|
728
1060
|
timeout: options.pageTimeout
|
|
729
1061
|
});
|
|
730
1062
|
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
|
-
})()`);
|
|
1063
|
+
await inlineStylesAndCleanup(page);
|
|
750
1064
|
const html = await page.content();
|
|
751
|
-
await
|
|
1065
|
+
await writeFile2(htmlAbsPath, html, "utf-8");
|
|
752
1066
|
await page.screenshot({ path: pngAbsPath, fullPage: true });
|
|
1067
|
+
const interactionStates = ["success", "empty"];
|
|
1068
|
+
const shouldCapture = options.captureInteractionStates !== false && interactionStates.includes(mockConfig.stateName);
|
|
1069
|
+
let interactions;
|
|
1070
|
+
if (shouldCapture) {
|
|
1071
|
+
await page.goto(fullUrl, { waitUntil: "networkidle", timeout: options.pageTimeout });
|
|
1072
|
+
await page.waitForTimeout(options.settleTime);
|
|
1073
|
+
const interactionResults = await captureInteractions(
|
|
1074
|
+
page,
|
|
1075
|
+
fullUrl,
|
|
1076
|
+
route,
|
|
1077
|
+
mockConfig.stateName,
|
|
1078
|
+
outputDir,
|
|
1079
|
+
{ maxInteractions: options.maxInteractions, settleTime: 500 }
|
|
1080
|
+
);
|
|
1081
|
+
const successful = interactionResults.filter((r) => r.success);
|
|
1082
|
+
if (successful.length > 0) {
|
|
1083
|
+
interactions = successful.map((r) => ({
|
|
1084
|
+
description: r.elementDescription,
|
|
1085
|
+
htmlPath: r.htmlPath
|
|
1086
|
+
}));
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
753
1089
|
return {
|
|
754
1090
|
route,
|
|
755
1091
|
stateName: mockConfig.stateName,
|
|
756
1092
|
htmlPath: htmlRelPath,
|
|
757
1093
|
screenshotPath: pngRelPath,
|
|
758
1094
|
success: true,
|
|
759
|
-
viewportName: viewport?.name
|
|
1095
|
+
viewportName: viewport?.name,
|
|
1096
|
+
interactions
|
|
760
1097
|
};
|
|
761
1098
|
} catch (err) {
|
|
762
1099
|
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
763
1100
|
const errorHtml = `<!DOCTYPE html><html><body style="display:flex;align-items:center;justify-content:center;height:100vh;font-family:sans-serif;color:#666;">
|
|
764
1101
|
<div style="text-align:center"><h2>Render Failed</h2><p>${route.urlPath} [${mockConfig.stateName}]</p><pre style="color:#c00">${errorMsg}</pre></div>
|
|
765
1102
|
</body></html>`;
|
|
766
|
-
await
|
|
1103
|
+
await writeFile2(htmlAbsPath, errorHtml, "utf-8").catch(() => {
|
|
767
1104
|
});
|
|
768
1105
|
return {
|
|
769
1106
|
route,
|
|
@@ -820,7 +1157,8 @@ function buildManifest(results, projectName, viewports) {
|
|
|
820
1157
|
status: result.success ? "ok" : "error",
|
|
821
1158
|
error: result.error,
|
|
822
1159
|
viewport: result.viewportName,
|
|
823
|
-
viewportWidth: result.viewportName ? viewportWidthMap.get(result.viewportName) : void 0
|
|
1160
|
+
viewportWidth: result.viewportName ? viewportWidthMap.get(result.viewportName) : void 0,
|
|
1161
|
+
interactions: result.interactions
|
|
824
1162
|
});
|
|
825
1163
|
}
|
|
826
1164
|
return {
|
|
@@ -832,16 +1170,18 @@ function buildManifest(results, projectName, viewports) {
|
|
|
832
1170
|
async function preRenderPages(tasks, options) {
|
|
833
1171
|
const {
|
|
834
1172
|
projectRoot,
|
|
835
|
-
outputDir =
|
|
1173
|
+
outputDir = join6(projectRoot, ".c2d"),
|
|
836
1174
|
concurrency = DEFAULT_CONCURRENCY,
|
|
837
1175
|
pageTimeout = DEFAULT_PAGE_TIMEOUT,
|
|
838
1176
|
settleTime = DEFAULT_SETTLE_TIME,
|
|
839
1177
|
viewportWidth = DEFAULT_VIEWPORT.width,
|
|
840
|
-
viewportHeight = DEFAULT_VIEWPORT.height
|
|
1178
|
+
viewportHeight = DEFAULT_VIEWPORT.height,
|
|
1179
|
+
captureInteractions: captureInteractionStates = true,
|
|
1180
|
+
maxInteractions
|
|
841
1181
|
} = options;
|
|
842
1182
|
const viewports = options.viewports;
|
|
843
1183
|
const useMultiViewport = viewports && viewports.length > 0;
|
|
844
|
-
await
|
|
1184
|
+
await mkdir2(outputDir, { recursive: true });
|
|
845
1185
|
let devServer = null;
|
|
846
1186
|
try {
|
|
847
1187
|
devServer = await startDevServer(projectRoot, {
|
|
@@ -864,20 +1204,20 @@ async function preRenderPages(tasks, options) {
|
|
|
864
1204
|
results = await processWithConcurrency(
|
|
865
1205
|
expandedItems,
|
|
866
1206
|
concurrency,
|
|
867
|
-
({ task, viewport: vp }) => renderPage(context, devServer.url, task, outputDir, { pageTimeout, settleTime }, vp),
|
|
1207
|
+
({ task, viewport: vp }) => renderPage(context, devServer.url, task, outputDir, { pageTimeout, settleTime, captureInteractionStates, maxInteractions }, vp),
|
|
868
1208
|
options.onProgress
|
|
869
1209
|
);
|
|
870
1210
|
} else {
|
|
871
1211
|
results = await processWithConcurrency(
|
|
872
1212
|
tasks,
|
|
873
1213
|
concurrency,
|
|
874
|
-
(task) => renderPage(context, devServer.url, task, outputDir, { pageTimeout, settleTime }),
|
|
1214
|
+
(task) => renderPage(context, devServer.url, task, outputDir, { pageTimeout, settleTime, captureInteractionStates, maxInteractions }),
|
|
875
1215
|
options.onProgress
|
|
876
1216
|
);
|
|
877
1217
|
}
|
|
878
1218
|
let projectName = "unknown";
|
|
879
1219
|
try {
|
|
880
|
-
const pkg = await import(
|
|
1220
|
+
const pkg = await import(join6(projectRoot, "package.json"), { with: { type: "json" } });
|
|
881
1221
|
projectName = pkg.default?.name || "unknown";
|
|
882
1222
|
} catch {
|
|
883
1223
|
}
|
|
@@ -886,8 +1226,8 @@ async function preRenderPages(tasks, options) {
|
|
|
886
1226
|
projectName,
|
|
887
1227
|
useMultiViewport ? viewports : void 0
|
|
888
1228
|
);
|
|
889
|
-
await
|
|
890
|
-
|
|
1229
|
+
await writeFile2(
|
|
1230
|
+
join6(outputDir, "manifest.json"),
|
|
891
1231
|
JSON.stringify(manifest, null, 2),
|
|
892
1232
|
"utf-8"
|
|
893
1233
|
);
|
|
@@ -905,13 +1245,14 @@ async function preRenderPages(tasks, options) {
|
|
|
905
1245
|
|
|
906
1246
|
// src/server/canvas-server.ts
|
|
907
1247
|
import { createServer as createServer2 } from "http";
|
|
908
|
-
import { join as
|
|
1248
|
+
import { join as join8 } from "path";
|
|
1249
|
+
import { existsSync as existsSync5 } from "fs";
|
|
909
1250
|
import sirv from "sirv";
|
|
910
1251
|
|
|
911
1252
|
// src/server/api-routes.ts
|
|
912
|
-
import { readFile as
|
|
1253
|
+
import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
|
|
913
1254
|
import { existsSync as existsSync4 } from "fs";
|
|
914
|
-
import { join as
|
|
1255
|
+
import { join as join7, dirname as dirname2 } from "path";
|
|
915
1256
|
import { randomUUID } from "crypto";
|
|
916
1257
|
async function handleApiRequest(req, res, c2dDir) {
|
|
917
1258
|
const url = req.url ?? "/";
|
|
@@ -944,12 +1285,12 @@ async function handleApiRequest(req, res, c2dDir) {
|
|
|
944
1285
|
return false;
|
|
945
1286
|
}
|
|
946
1287
|
async function handleGetManifest(res, c2dDir) {
|
|
947
|
-
const manifestPath =
|
|
1288
|
+
const manifestPath = join7(c2dDir, "manifest.json");
|
|
948
1289
|
if (!existsSync4(manifestPath)) {
|
|
949
1290
|
sendJson(res, 404, { error: "manifest.json not found" });
|
|
950
1291
|
return;
|
|
951
1292
|
}
|
|
952
|
-
const data = await
|
|
1293
|
+
const data = await readFile4(manifestPath, "utf-8");
|
|
953
1294
|
sendRawJson(res, 200, data);
|
|
954
1295
|
}
|
|
955
1296
|
async function handleGetComments(res, c2dDir) {
|
|
@@ -1014,11 +1355,11 @@ async function handlePostDrawings(req, res, c2dDir) {
|
|
|
1014
1355
|
sendJson(res, 200, { ok: true });
|
|
1015
1356
|
}
|
|
1016
1357
|
async function loadDrawings(c2dDir) {
|
|
1017
|
-
const drawingsPath =
|
|
1358
|
+
const drawingsPath = join7(c2dDir, "drawings.json");
|
|
1018
1359
|
if (!existsSync4(drawingsPath)) {
|
|
1019
1360
|
return [];
|
|
1020
1361
|
}
|
|
1021
|
-
const data = await
|
|
1362
|
+
const data = await readFile4(drawingsPath, "utf-8");
|
|
1022
1363
|
try {
|
|
1023
1364
|
const parsed = JSON.parse(data);
|
|
1024
1365
|
return Array.isArray(parsed) ? parsed : [];
|
|
@@ -1027,12 +1368,12 @@ async function loadDrawings(c2dDir) {
|
|
|
1027
1368
|
}
|
|
1028
1369
|
}
|
|
1029
1370
|
async function saveDrawings(c2dDir, drawings) {
|
|
1030
|
-
const drawingsPath =
|
|
1371
|
+
const drawingsPath = join7(c2dDir, "drawings.json");
|
|
1031
1372
|
const dir = dirname2(drawingsPath);
|
|
1032
1373
|
if (!existsSync4(dir)) {
|
|
1033
|
-
await
|
|
1374
|
+
await mkdir3(dir, { recursive: true });
|
|
1034
1375
|
}
|
|
1035
|
-
await
|
|
1376
|
+
await writeFile3(drawingsPath, JSON.stringify(drawings, null, 2), "utf-8");
|
|
1036
1377
|
}
|
|
1037
1378
|
function isValidCommentInput(value) {
|
|
1038
1379
|
if (typeof value !== "object" || value === null) return false;
|
|
@@ -1040,11 +1381,11 @@ function isValidCommentInput(value) {
|
|
|
1040
1381
|
return typeof obj.x === "number" && typeof obj.y === "number" && typeof obj.text === "string" && typeof obj.author === "string";
|
|
1041
1382
|
}
|
|
1042
1383
|
async function loadComments(c2dDir) {
|
|
1043
|
-
const commentsPath =
|
|
1384
|
+
const commentsPath = join7(c2dDir, "comments.json");
|
|
1044
1385
|
if (!existsSync4(commentsPath)) {
|
|
1045
1386
|
return [];
|
|
1046
1387
|
}
|
|
1047
|
-
const data = await
|
|
1388
|
+
const data = await readFile4(commentsPath, "utf-8");
|
|
1048
1389
|
try {
|
|
1049
1390
|
const parsed = JSON.parse(data);
|
|
1050
1391
|
return Array.isArray(parsed) ? parsed : [];
|
|
@@ -1053,12 +1394,12 @@ async function loadComments(c2dDir) {
|
|
|
1053
1394
|
}
|
|
1054
1395
|
}
|
|
1055
1396
|
async function saveComments(c2dDir, comments) {
|
|
1056
|
-
const commentsPath =
|
|
1397
|
+
const commentsPath = join7(c2dDir, "comments.json");
|
|
1057
1398
|
const dir = dirname2(commentsPath);
|
|
1058
1399
|
if (!existsSync4(dir)) {
|
|
1059
|
-
await
|
|
1400
|
+
await mkdir3(dir, { recursive: true });
|
|
1060
1401
|
}
|
|
1061
|
-
await
|
|
1402
|
+
await writeFile3(commentsPath, JSON.stringify(comments, null, 2), "utf-8");
|
|
1062
1403
|
}
|
|
1063
1404
|
function readRequestBody(req) {
|
|
1064
1405
|
return new Promise((resolve2, reject) => {
|
|
@@ -1106,10 +1447,12 @@ function tryListen(requestHandler, port) {
|
|
|
1106
1447
|
});
|
|
1107
1448
|
}
|
|
1108
1449
|
async function startCanvasServer(options) {
|
|
1109
|
-
const { port = 4800, canvasDir, c2dDir } = options;
|
|
1450
|
+
const { port = 4800, canvasDir, c2dDir, projectRoot } = options;
|
|
1110
1451
|
const canvasHandler = sirv(canvasDir, { single: true, dev: true });
|
|
1111
|
-
const rendersDir =
|
|
1452
|
+
const rendersDir = join8(c2dDir, "renders");
|
|
1112
1453
|
const rendersHandler = sirv(rendersDir, { dev: true });
|
|
1454
|
+
const publicDir = projectRoot ? join8(projectRoot, "public") : null;
|
|
1455
|
+
const publicHandler = publicDir && existsSync5(publicDir) ? sirv(publicDir, { dev: true }) : null;
|
|
1113
1456
|
const requestHandler = async (req, res) => {
|
|
1114
1457
|
const url = req.url ?? "/";
|
|
1115
1458
|
const method = req.method ?? "GET";
|
|
@@ -1143,6 +1486,15 @@ async function startCanvasServer(options) {
|
|
|
1143
1486
|
});
|
|
1144
1487
|
return;
|
|
1145
1488
|
}
|
|
1489
|
+
if (publicHandler) {
|
|
1490
|
+
publicHandler(req, res, () => {
|
|
1491
|
+
canvasHandler(req, res, () => {
|
|
1492
|
+
res.writeHead(404);
|
|
1493
|
+
res.end("Not found");
|
|
1494
|
+
});
|
|
1495
|
+
});
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1146
1498
|
canvasHandler(req, res, () => {
|
|
1147
1499
|
res.writeHead(404);
|
|
1148
1500
|
res.end("Not found");
|
|
@@ -1163,14 +1515,14 @@ async function startCanvasServer(options) {
|
|
|
1163
1515
|
}
|
|
1164
1516
|
|
|
1165
1517
|
// src/config.ts
|
|
1166
|
-
import { existsSync as
|
|
1167
|
-
import { readFile as
|
|
1168
|
-
import { join as
|
|
1518
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1519
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1520
|
+
import { join as join9 } from "path";
|
|
1169
1521
|
var DEFAULT_PORT = 4800;
|
|
1170
1522
|
async function loadConfig(projectRoot) {
|
|
1171
1523
|
let fileConfig = {};
|
|
1172
|
-
const configPath =
|
|
1173
|
-
if (
|
|
1524
|
+
const configPath = join9(projectRoot, "c2d.config.js");
|
|
1525
|
+
if (existsSync6(configPath)) {
|
|
1174
1526
|
try {
|
|
1175
1527
|
const mod = await import(configPath);
|
|
1176
1528
|
fileConfig = mod.default || mod;
|
|
@@ -1186,24 +1538,51 @@ async function loadConfig(projectRoot) {
|
|
|
1186
1538
|
devServerCommand: fileConfig.devServerCommand
|
|
1187
1539
|
};
|
|
1188
1540
|
}
|
|
1189
|
-
async function
|
|
1190
|
-
const hasNextConfig = existsSync5(join8(projectRoot, "next.config.ts")) || existsSync5(join8(projectRoot, "next.config.js")) || existsSync5(join8(projectRoot, "next.config.mjs"));
|
|
1191
|
-
let appDir = join8(projectRoot, "app");
|
|
1192
|
-
let hasAppDir = existsSync5(appDir);
|
|
1193
|
-
if (!hasAppDir) {
|
|
1194
|
-
appDir = join8(projectRoot, "src", "app");
|
|
1195
|
-
hasAppDir = existsSync5(appDir);
|
|
1196
|
-
}
|
|
1541
|
+
async function detectProject(projectRoot) {
|
|
1197
1542
|
let projectName = "unknown";
|
|
1198
1543
|
try {
|
|
1199
|
-
const pkg = JSON.parse(await
|
|
1544
|
+
const pkg = JSON.parse(await readFile5(join9(projectRoot, "package.json"), "utf-8"));
|
|
1200
1545
|
projectName = pkg.name || "unknown";
|
|
1201
1546
|
} catch {
|
|
1202
1547
|
}
|
|
1548
|
+
const hasNextConfig = existsSync6(join9(projectRoot, "next.config.ts")) || existsSync6(join9(projectRoot, "next.config.js")) || existsSync6(join9(projectRoot, "next.config.mjs"));
|
|
1549
|
+
if (hasNextConfig) {
|
|
1550
|
+
let appDir = join9(projectRoot, "app");
|
|
1551
|
+
let hasAppDir = existsSync6(appDir);
|
|
1552
|
+
if (!hasAppDir) {
|
|
1553
|
+
appDir = join9(projectRoot, "src", "app");
|
|
1554
|
+
hasAppDir = existsSync6(appDir);
|
|
1555
|
+
}
|
|
1556
|
+
if (hasAppDir) {
|
|
1557
|
+
return { isSupported: true, projectType: "nextjs-app", appDir, projectRoot, projectName };
|
|
1558
|
+
}
|
|
1559
|
+
appDir = join9(projectRoot, "pages");
|
|
1560
|
+
hasAppDir = existsSync6(appDir);
|
|
1561
|
+
if (!hasAppDir) {
|
|
1562
|
+
appDir = join9(projectRoot, "src", "pages");
|
|
1563
|
+
hasAppDir = existsSync6(appDir);
|
|
1564
|
+
}
|
|
1565
|
+
if (hasAppDir) {
|
|
1566
|
+
return { isSupported: true, projectType: "nextjs-pages", appDir, projectRoot, projectName };
|
|
1567
|
+
}
|
|
1568
|
+
return { isSupported: false, projectType: "unknown", appDir: null, projectRoot, projectName };
|
|
1569
|
+
}
|
|
1570
|
+
try {
|
|
1571
|
+
const pkg = JSON.parse(await readFile5(join9(projectRoot, "package.json"), "utf-8"));
|
|
1572
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
1573
|
+
if ("react-router-dom" in deps || "react-router" in deps) {
|
|
1574
|
+
return { isSupported: true, projectType: "react-router", appDir: null, projectRoot, projectName };
|
|
1575
|
+
}
|
|
1576
|
+
} catch {
|
|
1577
|
+
}
|
|
1578
|
+
return { isSupported: false, projectType: "unknown", appDir: null, projectRoot, projectName };
|
|
1579
|
+
}
|
|
1580
|
+
async function detectNextJsProject(projectRoot) {
|
|
1581
|
+
const result = await detectProject(projectRoot);
|
|
1203
1582
|
return {
|
|
1204
|
-
isNextJs:
|
|
1205
|
-
appDir:
|
|
1206
|
-
projectName
|
|
1583
|
+
isNextJs: result.projectType === "nextjs-app" || result.projectType === "nextjs-pages",
|
|
1584
|
+
appDir: result.appDir,
|
|
1585
|
+
projectName: result.projectName
|
|
1207
1586
|
};
|
|
1208
1587
|
}
|
|
1209
1588
|
|
|
@@ -1250,30 +1629,34 @@ async function runScan(options) {
|
|
|
1250
1629
|
banner();
|
|
1251
1630
|
const config = await loadConfig(projectRoot);
|
|
1252
1631
|
header("Detecting project...");
|
|
1253
|
-
const project = await
|
|
1254
|
-
if (!project.
|
|
1255
|
-
error("
|
|
1256
|
-
log("Code to Design
|
|
1632
|
+
const project = await detectProject(projectRoot);
|
|
1633
|
+
if (!project.isSupported) {
|
|
1634
|
+
error("Unsupported project type.");
|
|
1635
|
+
log("Code to Design supports Next.js (App Router / Pages Router) and React Router (Vite) projects.");
|
|
1257
1636
|
process.exit(1);
|
|
1258
1637
|
}
|
|
1259
|
-
if (!project.appDir) {
|
|
1260
|
-
error("No app/ directory found.
|
|
1638
|
+
if (project.projectType !== "react-router" && !project.appDir) {
|
|
1639
|
+
error("No app/ or pages/ directory found.");
|
|
1261
1640
|
process.exit(1);
|
|
1262
1641
|
}
|
|
1263
|
-
success(`Project: ${project.projectName}`);
|
|
1264
|
-
|
|
1265
|
-
|
|
1642
|
+
success(`Project: ${project.projectName} (${project.projectType})`);
|
|
1643
|
+
if (project.appDir) {
|
|
1644
|
+
success(`App directory: ${project.appDir}`);
|
|
1645
|
+
}
|
|
1646
|
+
const c2dDir = join10(projectRoot, ".c2d");
|
|
1266
1647
|
if (skipRender) {
|
|
1267
|
-
if (!
|
|
1648
|
+
if (!existsSync7(join10(c2dDir, "manifest.json"))) {
|
|
1268
1649
|
error("No previous renders found. Run without --skip-render first.");
|
|
1269
1650
|
process.exit(1);
|
|
1270
1651
|
}
|
|
1271
1652
|
log("Skipping render, using existing canvas data...");
|
|
1272
|
-
await startServer(c2dDir, config.port, open);
|
|
1653
|
+
await startServer(c2dDir, config.port, open, projectRoot);
|
|
1273
1654
|
return;
|
|
1274
1655
|
}
|
|
1275
1656
|
header("Discovering routes...");
|
|
1276
|
-
const
|
|
1657
|
+
const scanDir2 = project.projectType === "react-router" ? project.projectRoot : project.appDir;
|
|
1658
|
+
const routerType = project.projectType === "react-router" ? "react-router" : project.projectType === "nextjs-pages" ? "pages-router" : "app-router";
|
|
1659
|
+
const routes = await scanRoutes({ appDir: scanDir2, routerType });
|
|
1277
1660
|
if (routes.length === 0) {
|
|
1278
1661
|
error("No routes found in app/ directory.");
|
|
1279
1662
|
process.exit(1);
|
|
@@ -1316,10 +1699,10 @@ async function runScan(options) {
|
|
|
1316
1699
|
success("Using fallback mocks (no API key or no API dependencies)");
|
|
1317
1700
|
}
|
|
1318
1701
|
header(`Pre-rendering ${renderTasks.length} page states...`);
|
|
1319
|
-
if (
|
|
1320
|
-
await rm(
|
|
1702
|
+
if (existsSync7(join10(c2dDir, "renders"))) {
|
|
1703
|
+
await rm(join10(c2dDir, "renders"), { recursive: true });
|
|
1321
1704
|
}
|
|
1322
|
-
await
|
|
1705
|
+
await mkdir4(c2dDir, { recursive: true });
|
|
1323
1706
|
const { results, manifest } = await preRenderPages(renderTasks, {
|
|
1324
1707
|
projectRoot,
|
|
1325
1708
|
outputDir: c2dDir,
|
|
@@ -1342,8 +1725,9 @@ async function runScan(options) {
|
|
|
1342
1725
|
}
|
|
1343
1726
|
}
|
|
1344
1727
|
if (watch) {
|
|
1345
|
-
const server = await startServerNonBlocking(c2dDir, config.port, open);
|
|
1346
|
-
|
|
1728
|
+
const server = await startServerNonBlocking(c2dDir, config.port, open, projectRoot);
|
|
1729
|
+
const watchDir = project.projectType === "react-router" ? join10(projectRoot, "src") : project.appDir;
|
|
1730
|
+
watchAndRerender(projectRoot, watchDir, c2dDir, config, routerType);
|
|
1347
1731
|
await new Promise((resolve2) => {
|
|
1348
1732
|
const shutdown = async () => {
|
|
1349
1733
|
log("\nShutting down...");
|
|
@@ -1354,10 +1738,10 @@ async function runScan(options) {
|
|
|
1354
1738
|
process.on("SIGTERM", shutdown);
|
|
1355
1739
|
});
|
|
1356
1740
|
} else {
|
|
1357
|
-
await startServer(c2dDir, config.port, open);
|
|
1741
|
+
await startServer(c2dDir, config.port, open, projectRoot);
|
|
1358
1742
|
}
|
|
1359
1743
|
}
|
|
1360
|
-
async function startServer(c2dDir, port, open) {
|
|
1744
|
+
async function startServer(c2dDir, port, open, projectRoot) {
|
|
1361
1745
|
header("Starting canvas server...");
|
|
1362
1746
|
const canvasDir = await resolveCanvasDir(c2dDir);
|
|
1363
1747
|
if (!canvasDir) {
|
|
@@ -1366,7 +1750,8 @@ async function startServer(c2dDir, port, open) {
|
|
|
1366
1750
|
const server = await startCanvasServer({
|
|
1367
1751
|
port,
|
|
1368
1752
|
canvasDir,
|
|
1369
|
-
c2dDir
|
|
1753
|
+
c2dDir,
|
|
1754
|
+
projectRoot
|
|
1370
1755
|
});
|
|
1371
1756
|
success(`Canvas server running at ${server.url}`);
|
|
1372
1757
|
log("Share this URL with your team for collaborative review");
|
|
@@ -1388,7 +1773,7 @@ async function startServer(c2dDir, port, open) {
|
|
|
1388
1773
|
process.on("SIGTERM", shutdown);
|
|
1389
1774
|
});
|
|
1390
1775
|
}
|
|
1391
|
-
async function startServerNonBlocking(c2dDir, port, open) {
|
|
1776
|
+
async function startServerNonBlocking(c2dDir, port, open, projectRoot) {
|
|
1392
1777
|
header("Starting canvas server...");
|
|
1393
1778
|
const canvasDir = await resolveCanvasDir(c2dDir);
|
|
1394
1779
|
if (!canvasDir) {
|
|
@@ -1397,7 +1782,8 @@ async function startServerNonBlocking(c2dDir, port, open) {
|
|
|
1397
1782
|
const server = await startCanvasServer({
|
|
1398
1783
|
port,
|
|
1399
1784
|
canvasDir,
|
|
1400
|
-
c2dDir
|
|
1785
|
+
c2dDir,
|
|
1786
|
+
projectRoot
|
|
1401
1787
|
});
|
|
1402
1788
|
success(`Canvas server running at ${server.url}`);
|
|
1403
1789
|
log("Share this URL with your team for collaborative review");
|
|
@@ -1417,20 +1803,20 @@ async function resolveCanvasDir(c2dDir) {
|
|
|
1417
1803
|
const __filename = fileURLToPath(import.meta.url);
|
|
1418
1804
|
let dir = dirname3(__filename);
|
|
1419
1805
|
for (let i = 0; i < 5; i++) {
|
|
1420
|
-
const candidate =
|
|
1421
|
-
if (
|
|
1806
|
+
const candidate = join10(dir, "canvas-dist");
|
|
1807
|
+
if (existsSync7(candidate) && existsSync7(join10(candidate, "index.html"))) {
|
|
1422
1808
|
return candidate;
|
|
1423
1809
|
}
|
|
1424
1810
|
dir = dirname3(dir);
|
|
1425
1811
|
}
|
|
1426
|
-
const monorepoDev =
|
|
1427
|
-
if (
|
|
1812
|
+
const monorepoDev = join10(dirname3(__filename), "..", "..", "..", "..", "apps", "canvas", "dist");
|
|
1813
|
+
if (existsSync7(monorepoDev) && existsSync7(join10(monorepoDev, "index.html"))) {
|
|
1428
1814
|
return monorepoDev;
|
|
1429
1815
|
}
|
|
1430
|
-
const placeholder =
|
|
1431
|
-
await
|
|
1432
|
-
const { writeFile:
|
|
1433
|
-
await
|
|
1816
|
+
const placeholder = join10(c2dDir, "_canvas");
|
|
1817
|
+
await mkdir4(placeholder, { recursive: true });
|
|
1818
|
+
const { writeFile: writeFile4 } = await import("fs/promises");
|
|
1819
|
+
await writeFile4(join10(placeholder, "index.html"), `<!DOCTYPE html><html><body>
|
|
1434
1820
|
<h1>Code to Design</h1>
|
|
1435
1821
|
<p>Canvas app not built. Run: <code>cd apps/canvas && npx vite build</code></p>
|
|
1436
1822
|
<p><a href="/api/manifest">View Manifest</a></p>
|
|
@@ -1445,7 +1831,7 @@ function shouldIgnoreFile(filename) {
|
|
|
1445
1831
|
const ext = filename.slice(filename.lastIndexOf("."));
|
|
1446
1832
|
return !WATCH_EXTENSIONS.has(ext);
|
|
1447
1833
|
}
|
|
1448
|
-
function watchAndRerender(projectRoot, appDir, c2dDir, config) {
|
|
1834
|
+
function watchAndRerender(projectRoot, appDir, c2dDir, config, routerType) {
|
|
1449
1835
|
let debounceTimer;
|
|
1450
1836
|
let isRendering = false;
|
|
1451
1837
|
log(`Watching ${appDir} for changes...`);
|
|
@@ -1458,7 +1844,7 @@ function watchAndRerender(projectRoot, appDir, c2dDir, config) {
|
|
|
1458
1844
|
log(`
|
|
1459
1845
|
File changed: ${filename}. Re-rendering...`);
|
|
1460
1846
|
try {
|
|
1461
|
-
const routes = await scanRoutes({ appDir });
|
|
1847
|
+
const routes = await scanRoutes({ appDir, routerType });
|
|
1462
1848
|
const filteredRoutes = routes.filter(
|
|
1463
1849
|
(r) => !config.excludeRoutes.some((pattern) => r.urlPath.includes(pattern))
|
|
1464
1850
|
);
|
|
@@ -1475,10 +1861,10 @@ File changed: ${filename}. Re-rendering...`);
|
|
|
1475
1861
|
});
|
|
1476
1862
|
}
|
|
1477
1863
|
}
|
|
1478
|
-
if (
|
|
1479
|
-
await rm(
|
|
1864
|
+
if (existsSync7(join10(c2dDir, "renders"))) {
|
|
1865
|
+
await rm(join10(c2dDir, "renders"), { recursive: true });
|
|
1480
1866
|
}
|
|
1481
|
-
await
|
|
1867
|
+
await mkdir4(c2dDir, { recursive: true });
|
|
1482
1868
|
const { results } = await preRenderPages(renderTasks, {
|
|
1483
1869
|
projectRoot,
|
|
1484
1870
|
outputDir: c2dDir,
|
|
@@ -1498,7 +1884,8 @@ File changed: ${filename}. Re-rendering...`);
|
|
|
1498
1884
|
export {
|
|
1499
1885
|
startCanvasServer,
|
|
1500
1886
|
loadConfig,
|
|
1887
|
+
detectProject,
|
|
1501
1888
|
detectNextJsProject,
|
|
1502
1889
|
runScan
|
|
1503
1890
|
};
|
|
1504
|
-
//# sourceMappingURL=chunk-
|
|
1891
|
+
//# sourceMappingURL=chunk-HYFKUTC3.js.map
|