code-to-design 0.1.5 → 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-WX4KLWOS.js → chunk-HYFKUTC3.js} +485 -112
- package/dist/chunk-HYFKUTC3.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;
|
|
@@ -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,14 +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";
|
|
909
1249
|
import { existsSync as existsSync5 } from "fs";
|
|
910
1250
|
import sirv from "sirv";
|
|
911
1251
|
|
|
912
1252
|
// src/server/api-routes.ts
|
|
913
|
-
import { readFile as
|
|
1253
|
+
import { readFile as readFile4, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
|
|
914
1254
|
import { existsSync as existsSync4 } from "fs";
|
|
915
|
-
import { join as
|
|
1255
|
+
import { join as join7, dirname as dirname2 } from "path";
|
|
916
1256
|
import { randomUUID } from "crypto";
|
|
917
1257
|
async function handleApiRequest(req, res, c2dDir) {
|
|
918
1258
|
const url = req.url ?? "/";
|
|
@@ -945,12 +1285,12 @@ async function handleApiRequest(req, res, c2dDir) {
|
|
|
945
1285
|
return false;
|
|
946
1286
|
}
|
|
947
1287
|
async function handleGetManifest(res, c2dDir) {
|
|
948
|
-
const manifestPath =
|
|
1288
|
+
const manifestPath = join7(c2dDir, "manifest.json");
|
|
949
1289
|
if (!existsSync4(manifestPath)) {
|
|
950
1290
|
sendJson(res, 404, { error: "manifest.json not found" });
|
|
951
1291
|
return;
|
|
952
1292
|
}
|
|
953
|
-
const data = await
|
|
1293
|
+
const data = await readFile4(manifestPath, "utf-8");
|
|
954
1294
|
sendRawJson(res, 200, data);
|
|
955
1295
|
}
|
|
956
1296
|
async function handleGetComments(res, c2dDir) {
|
|
@@ -1015,11 +1355,11 @@ async function handlePostDrawings(req, res, c2dDir) {
|
|
|
1015
1355
|
sendJson(res, 200, { ok: true });
|
|
1016
1356
|
}
|
|
1017
1357
|
async function loadDrawings(c2dDir) {
|
|
1018
|
-
const drawingsPath =
|
|
1358
|
+
const drawingsPath = join7(c2dDir, "drawings.json");
|
|
1019
1359
|
if (!existsSync4(drawingsPath)) {
|
|
1020
1360
|
return [];
|
|
1021
1361
|
}
|
|
1022
|
-
const data = await
|
|
1362
|
+
const data = await readFile4(drawingsPath, "utf-8");
|
|
1023
1363
|
try {
|
|
1024
1364
|
const parsed = JSON.parse(data);
|
|
1025
1365
|
return Array.isArray(parsed) ? parsed : [];
|
|
@@ -1028,12 +1368,12 @@ async function loadDrawings(c2dDir) {
|
|
|
1028
1368
|
}
|
|
1029
1369
|
}
|
|
1030
1370
|
async function saveDrawings(c2dDir, drawings) {
|
|
1031
|
-
const drawingsPath =
|
|
1371
|
+
const drawingsPath = join7(c2dDir, "drawings.json");
|
|
1032
1372
|
const dir = dirname2(drawingsPath);
|
|
1033
1373
|
if (!existsSync4(dir)) {
|
|
1034
|
-
await
|
|
1374
|
+
await mkdir3(dir, { recursive: true });
|
|
1035
1375
|
}
|
|
1036
|
-
await
|
|
1376
|
+
await writeFile3(drawingsPath, JSON.stringify(drawings, null, 2), "utf-8");
|
|
1037
1377
|
}
|
|
1038
1378
|
function isValidCommentInput(value) {
|
|
1039
1379
|
if (typeof value !== "object" || value === null) return false;
|
|
@@ -1041,11 +1381,11 @@ function isValidCommentInput(value) {
|
|
|
1041
1381
|
return typeof obj.x === "number" && typeof obj.y === "number" && typeof obj.text === "string" && typeof obj.author === "string";
|
|
1042
1382
|
}
|
|
1043
1383
|
async function loadComments(c2dDir) {
|
|
1044
|
-
const commentsPath =
|
|
1384
|
+
const commentsPath = join7(c2dDir, "comments.json");
|
|
1045
1385
|
if (!existsSync4(commentsPath)) {
|
|
1046
1386
|
return [];
|
|
1047
1387
|
}
|
|
1048
|
-
const data = await
|
|
1388
|
+
const data = await readFile4(commentsPath, "utf-8");
|
|
1049
1389
|
try {
|
|
1050
1390
|
const parsed = JSON.parse(data);
|
|
1051
1391
|
return Array.isArray(parsed) ? parsed : [];
|
|
@@ -1054,12 +1394,12 @@ async function loadComments(c2dDir) {
|
|
|
1054
1394
|
}
|
|
1055
1395
|
}
|
|
1056
1396
|
async function saveComments(c2dDir, comments) {
|
|
1057
|
-
const commentsPath =
|
|
1397
|
+
const commentsPath = join7(c2dDir, "comments.json");
|
|
1058
1398
|
const dir = dirname2(commentsPath);
|
|
1059
1399
|
if (!existsSync4(dir)) {
|
|
1060
|
-
await
|
|
1400
|
+
await mkdir3(dir, { recursive: true });
|
|
1061
1401
|
}
|
|
1062
|
-
await
|
|
1402
|
+
await writeFile3(commentsPath, JSON.stringify(comments, null, 2), "utf-8");
|
|
1063
1403
|
}
|
|
1064
1404
|
function readRequestBody(req) {
|
|
1065
1405
|
return new Promise((resolve2, reject) => {
|
|
@@ -1109,9 +1449,9 @@ function tryListen(requestHandler, port) {
|
|
|
1109
1449
|
async function startCanvasServer(options) {
|
|
1110
1450
|
const { port = 4800, canvasDir, c2dDir, projectRoot } = options;
|
|
1111
1451
|
const canvasHandler = sirv(canvasDir, { single: true, dev: true });
|
|
1112
|
-
const rendersDir =
|
|
1452
|
+
const rendersDir = join8(c2dDir, "renders");
|
|
1113
1453
|
const rendersHandler = sirv(rendersDir, { dev: true });
|
|
1114
|
-
const publicDir = projectRoot ?
|
|
1454
|
+
const publicDir = projectRoot ? join8(projectRoot, "public") : null;
|
|
1115
1455
|
const publicHandler = publicDir && existsSync5(publicDir) ? sirv(publicDir, { dev: true }) : null;
|
|
1116
1456
|
const requestHandler = async (req, res) => {
|
|
1117
1457
|
const url = req.url ?? "/";
|
|
@@ -1176,12 +1516,12 @@ async function startCanvasServer(options) {
|
|
|
1176
1516
|
|
|
1177
1517
|
// src/config.ts
|
|
1178
1518
|
import { existsSync as existsSync6 } from "fs";
|
|
1179
|
-
import { readFile as
|
|
1180
|
-
import { join as
|
|
1519
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
1520
|
+
import { join as join9 } from "path";
|
|
1181
1521
|
var DEFAULT_PORT = 4800;
|
|
1182
1522
|
async function loadConfig(projectRoot) {
|
|
1183
1523
|
let fileConfig = {};
|
|
1184
|
-
const configPath =
|
|
1524
|
+
const configPath = join9(projectRoot, "c2d.config.js");
|
|
1185
1525
|
if (existsSync6(configPath)) {
|
|
1186
1526
|
try {
|
|
1187
1527
|
const mod = await import(configPath);
|
|
@@ -1198,24 +1538,51 @@ async function loadConfig(projectRoot) {
|
|
|
1198
1538
|
devServerCommand: fileConfig.devServerCommand
|
|
1199
1539
|
};
|
|
1200
1540
|
}
|
|
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
|
-
}
|
|
1541
|
+
async function detectProject(projectRoot) {
|
|
1209
1542
|
let projectName = "unknown";
|
|
1210
1543
|
try {
|
|
1211
|
-
const pkg = JSON.parse(await
|
|
1544
|
+
const pkg = JSON.parse(await readFile5(join9(projectRoot, "package.json"), "utf-8"));
|
|
1212
1545
|
projectName = pkg.name || "unknown";
|
|
1213
1546
|
} catch {
|
|
1214
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);
|
|
1215
1582
|
return {
|
|
1216
|
-
isNextJs:
|
|
1217
|
-
appDir:
|
|
1218
|
-
projectName
|
|
1583
|
+
isNextJs: result.projectType === "nextjs-app" || result.projectType === "nextjs-pages",
|
|
1584
|
+
appDir: result.appDir,
|
|
1585
|
+
projectName: result.projectName
|
|
1219
1586
|
};
|
|
1220
1587
|
}
|
|
1221
1588
|
|
|
@@ -1262,21 +1629,23 @@ async function runScan(options) {
|
|
|
1262
1629
|
banner();
|
|
1263
1630
|
const config = await loadConfig(projectRoot);
|
|
1264
1631
|
header("Detecting project...");
|
|
1265
|
-
const project = await
|
|
1266
|
-
if (!project.
|
|
1267
|
-
error("
|
|
1268
|
-
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.");
|
|
1269
1636
|
process.exit(1);
|
|
1270
1637
|
}
|
|
1271
|
-
if (!project.appDir) {
|
|
1272
|
-
error("No app/ directory found.
|
|
1638
|
+
if (project.projectType !== "react-router" && !project.appDir) {
|
|
1639
|
+
error("No app/ or pages/ directory found.");
|
|
1273
1640
|
process.exit(1);
|
|
1274
1641
|
}
|
|
1275
|
-
success(`Project: ${project.projectName}`);
|
|
1276
|
-
|
|
1277
|
-
|
|
1642
|
+
success(`Project: ${project.projectName} (${project.projectType})`);
|
|
1643
|
+
if (project.appDir) {
|
|
1644
|
+
success(`App directory: ${project.appDir}`);
|
|
1645
|
+
}
|
|
1646
|
+
const c2dDir = join10(projectRoot, ".c2d");
|
|
1278
1647
|
if (skipRender) {
|
|
1279
|
-
if (!existsSync7(
|
|
1648
|
+
if (!existsSync7(join10(c2dDir, "manifest.json"))) {
|
|
1280
1649
|
error("No previous renders found. Run without --skip-render first.");
|
|
1281
1650
|
process.exit(1);
|
|
1282
1651
|
}
|
|
@@ -1285,7 +1654,9 @@ async function runScan(options) {
|
|
|
1285
1654
|
return;
|
|
1286
1655
|
}
|
|
1287
1656
|
header("Discovering routes...");
|
|
1288
|
-
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 });
|
|
1289
1660
|
if (routes.length === 0) {
|
|
1290
1661
|
error("No routes found in app/ directory.");
|
|
1291
1662
|
process.exit(1);
|
|
@@ -1328,10 +1699,10 @@ async function runScan(options) {
|
|
|
1328
1699
|
success("Using fallback mocks (no API key or no API dependencies)");
|
|
1329
1700
|
}
|
|
1330
1701
|
header(`Pre-rendering ${renderTasks.length} page states...`);
|
|
1331
|
-
if (existsSync7(
|
|
1332
|
-
await rm(
|
|
1702
|
+
if (existsSync7(join10(c2dDir, "renders"))) {
|
|
1703
|
+
await rm(join10(c2dDir, "renders"), { recursive: true });
|
|
1333
1704
|
}
|
|
1334
|
-
await
|
|
1705
|
+
await mkdir4(c2dDir, { recursive: true });
|
|
1335
1706
|
const { results, manifest } = await preRenderPages(renderTasks, {
|
|
1336
1707
|
projectRoot,
|
|
1337
1708
|
outputDir: c2dDir,
|
|
@@ -1355,7 +1726,8 @@ async function runScan(options) {
|
|
|
1355
1726
|
}
|
|
1356
1727
|
if (watch) {
|
|
1357
1728
|
const server = await startServerNonBlocking(c2dDir, config.port, open, projectRoot);
|
|
1358
|
-
|
|
1729
|
+
const watchDir = project.projectType === "react-router" ? join10(projectRoot, "src") : project.appDir;
|
|
1730
|
+
watchAndRerender(projectRoot, watchDir, c2dDir, config, routerType);
|
|
1359
1731
|
await new Promise((resolve2) => {
|
|
1360
1732
|
const shutdown = async () => {
|
|
1361
1733
|
log("\nShutting down...");
|
|
@@ -1431,20 +1803,20 @@ async function resolveCanvasDir(c2dDir) {
|
|
|
1431
1803
|
const __filename = fileURLToPath(import.meta.url);
|
|
1432
1804
|
let dir = dirname3(__filename);
|
|
1433
1805
|
for (let i = 0; i < 5; i++) {
|
|
1434
|
-
const candidate =
|
|
1435
|
-
if (existsSync7(candidate) && existsSync7(
|
|
1806
|
+
const candidate = join10(dir, "canvas-dist");
|
|
1807
|
+
if (existsSync7(candidate) && existsSync7(join10(candidate, "index.html"))) {
|
|
1436
1808
|
return candidate;
|
|
1437
1809
|
}
|
|
1438
1810
|
dir = dirname3(dir);
|
|
1439
1811
|
}
|
|
1440
|
-
const monorepoDev =
|
|
1441
|
-
if (existsSync7(monorepoDev) && existsSync7(
|
|
1812
|
+
const monorepoDev = join10(dirname3(__filename), "..", "..", "..", "..", "apps", "canvas", "dist");
|
|
1813
|
+
if (existsSync7(monorepoDev) && existsSync7(join10(monorepoDev, "index.html"))) {
|
|
1442
1814
|
return monorepoDev;
|
|
1443
1815
|
}
|
|
1444
|
-
const placeholder =
|
|
1445
|
-
await
|
|
1446
|
-
const { writeFile:
|
|
1447
|
-
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>
|
|
1448
1820
|
<h1>Code to Design</h1>
|
|
1449
1821
|
<p>Canvas app not built. Run: <code>cd apps/canvas && npx vite build</code></p>
|
|
1450
1822
|
<p><a href="/api/manifest">View Manifest</a></p>
|
|
@@ -1459,7 +1831,7 @@ function shouldIgnoreFile(filename) {
|
|
|
1459
1831
|
const ext = filename.slice(filename.lastIndexOf("."));
|
|
1460
1832
|
return !WATCH_EXTENSIONS.has(ext);
|
|
1461
1833
|
}
|
|
1462
|
-
function watchAndRerender(projectRoot, appDir, c2dDir, config) {
|
|
1834
|
+
function watchAndRerender(projectRoot, appDir, c2dDir, config, routerType) {
|
|
1463
1835
|
let debounceTimer;
|
|
1464
1836
|
let isRendering = false;
|
|
1465
1837
|
log(`Watching ${appDir} for changes...`);
|
|
@@ -1472,7 +1844,7 @@ function watchAndRerender(projectRoot, appDir, c2dDir, config) {
|
|
|
1472
1844
|
log(`
|
|
1473
1845
|
File changed: ${filename}. Re-rendering...`);
|
|
1474
1846
|
try {
|
|
1475
|
-
const routes = await scanRoutes({ appDir });
|
|
1847
|
+
const routes = await scanRoutes({ appDir, routerType });
|
|
1476
1848
|
const filteredRoutes = routes.filter(
|
|
1477
1849
|
(r) => !config.excludeRoutes.some((pattern) => r.urlPath.includes(pattern))
|
|
1478
1850
|
);
|
|
@@ -1489,10 +1861,10 @@ File changed: ${filename}. Re-rendering...`);
|
|
|
1489
1861
|
});
|
|
1490
1862
|
}
|
|
1491
1863
|
}
|
|
1492
|
-
if (existsSync7(
|
|
1493
|
-
await rm(
|
|
1864
|
+
if (existsSync7(join10(c2dDir, "renders"))) {
|
|
1865
|
+
await rm(join10(c2dDir, "renders"), { recursive: true });
|
|
1494
1866
|
}
|
|
1495
|
-
await
|
|
1867
|
+
await mkdir4(c2dDir, { recursive: true });
|
|
1496
1868
|
const { results } = await preRenderPages(renderTasks, {
|
|
1497
1869
|
projectRoot,
|
|
1498
1870
|
outputDir: c2dDir,
|
|
@@ -1512,7 +1884,8 @@ File changed: ${filename}. Re-rendering...`);
|
|
|
1512
1884
|
export {
|
|
1513
1885
|
startCanvasServer,
|
|
1514
1886
|
loadConfig,
|
|
1887
|
+
detectProject,
|
|
1515
1888
|
detectNextJsProject,
|
|
1516
1889
|
runScan
|
|
1517
1890
|
};
|
|
1518
|
-
//# sourceMappingURL=chunk-
|
|
1891
|
+
//# sourceMappingURL=chunk-HYFKUTC3.js.map
|