code-to-design 0.2.0 → 0.2.2

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.
@@ -335,6 +335,84 @@ async function detectAuth(projectRoot, allSources) {
335
335
  return config;
336
336
  }
337
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
+
338
416
  // ../core/src/analysis/code-analyzer.ts
339
417
  var DEFAULT_MAX_TOKENS = 8e3;
340
418
  var CHARS_PER_TOKEN = 4;
@@ -454,6 +532,58 @@ function estimateTokens(text) {
454
532
  function hasApiCalls(source) {
455
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);
456
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
+ }
457
587
  async function analyzePage(route, options) {
458
588
  const { projectRoot, maxTokensPerPage = DEFAULT_MAX_TOKENS } = options;
459
589
  const maxChars = maxTokensPerPage * CHARS_PER_TOKEN;
@@ -473,6 +603,39 @@ ${content}
473
603
  }
474
604
  sourceContext += section;
475
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
+ }
476
639
  const authConfig = await detectAuth(projectRoot, allSources);
477
640
  const combinedSource = allSources.join("\n");
478
641
  const hasApi = hasApiCalls(sourceContext) || hasApiCalls(combinedSource);
@@ -490,13 +653,13 @@ ${content}
490
653
  unresolvedImports: [...new Set(unresolvedImports)],
491
654
  authConfig,
492
655
  hasApiDependencies: hasApi,
493
- estimatedTokens: estimateTokens(sourceContext)
656
+ estimatedTokens: estimateTokens(sourceContext),
657
+ apiClientPath,
658
+ extractedEndpoints: extractedEndpointsList,
659
+ apiBaseUrl
494
660
  };
495
661
  }
496
662
 
497
- // ../core/src/mock/types.ts
498
- var ALL_STATE_VARIANTS = ["success", "empty", "error", "loading"];
499
-
500
663
  // ../core/src/mock/llm-client.ts
501
664
  import Anthropic from "@anthropic-ai/sdk";
502
665
  var LlmClient = class {
@@ -524,20 +687,18 @@ var LlmClient = class {
524
687
  };
525
688
 
526
689
  // ../core/src/mock/prompt-templates.ts
527
- var SYSTEM_PROMPT = `You are a mock data generator for a UI pre-rendering tool called VibeCanvas.
528
- 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.
529
692
 
530
693
  Rules:
531
694
  1. Output ONLY valid JSON \u2014 no markdown, no code fences, no explanation.
532
- 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.
533
696
  3. Generate contextually appropriate data (e.g., real company names for a finance app, realistic usernames for a social app).
534
- 4. For each API endpoint, generate mock responses for these state variants:
535
- - "success": realistic data with multiple items
536
- - "empty": empty collections, zero counts
537
- - "error": API returns HTTP 500 with error message
538
- - "loading": same as success but with a 3000ms delay
539
- 5. Include auth mock data if the page requires authentication.
540
- 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.`;
541
702
  function buildUserPrompt(analysis) {
542
703
  const parts = [];
543
704
  parts.push(`## Page Route: ${analysis.route.urlPath}`);
@@ -545,6 +706,16 @@ function buildUserPrompt(analysis) {
545
706
  if (analysis.route.isDynamic) {
546
707
  parts.push(`## Dynamic Parameters: ${analysis.route.params.map((p) => p.name).join(", ")}`);
547
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
+ }
548
719
  parts.push("## Source Code Context");
549
720
  parts.push(analysis.sourceContext);
550
721
  if (analysis.authConfig.hasAuth) {
@@ -563,29 +734,51 @@ function buildUserPrompt(analysis) {
563
734
  parts.push(`
564
735
  ## Required Output Format
565
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
+
566
754
  Return a JSON object with this exact structure:
567
755
  {
568
- "routeParams": { "paramName": "sampleValue" }, // only if dynamic route
569
- "apiEndpoints": [
756
+ "routeParams": { "paramName": "sampleValue" },
757
+ "stateVariants": [
570
758
  {
571
- "urlPattern": "/api/endpoint", // the URL path pattern to intercept
572
- "method": "GET", // HTTP method
573
- "states": {
574
- "success": { "status": 200, "body": { ... } },
575
- "empty": { "status": 200, "body": { ... } },
576
- "error": { "status": 500, "body": { "detail": "Internal server error" } },
577
- "loading": { "status": 200, "body": { ... }, "delay": 3000 }
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
+ }
578
767
  }
579
768
  }
580
769
  ],
581
- "authMock": { // only if auth is required
770
+ "authMock": {
582
771
  "cookies": { "cookie_name": "mock_value" },
583
772
  "authCheckEndpoint": "/auth/me",
584
773
  "authCheckResponse": { "status": 200, "body": { ... } }
585
774
  }
586
775
  }
587
776
 
588
- Generate ONLY the JSON. No explanation, no markdown fences.`);
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.`);
589
782
  return parts.join("\n\n");
590
783
  }
591
784
 
@@ -609,7 +802,41 @@ function parseLlmJson(content) {
609
802
  return null;
610
803
  }
611
804
  }
612
- function convertToMockConfigs(output, analysis, variants) {
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) {
613
840
  const configs = [];
614
841
  for (const variant of variants) {
615
842
  const apiMocks = {};
@@ -659,7 +886,7 @@ function generateFallbackConfigs(analysis, variants) {
659
886
  }));
660
887
  }
661
888
  async function generateMocks(analysis, options) {
662
- const variants = options.variants ?? ALL_STATE_VARIANTS;
889
+ const variants = options.variants ?? ["success", "empty", "error", "loading"];
663
890
  if (!analysis.hasApiDependencies) {
664
891
  return {
665
892
  configs: generateFallbackConfigs(analysis, variants),
@@ -678,7 +905,15 @@ async function generateMocks(analysis, options) {
678
905
  tokenUsage: { input: response.inputTokens, output: response.outputTokens }
679
906
  };
680
907
  }
681
- const configs = convertToMockConfigs(parsed, analysis, variants);
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
+ }
682
917
  return {
683
918
  configs,
684
919
  tokenUsage: { input: response.inputTokens, output: response.outputTokens }
@@ -965,6 +1200,32 @@ async function captureInteractions(page, pageUrl, route, stateName, outputDir, o
965
1200
  return results;
966
1201
  }
967
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
+
968
1229
  // ../core/src/render/pre-renderer.ts
969
1230
  var DEFAULT_CONCURRENCY = 3;
970
1231
  var DEFAULT_PAGE_TIMEOUT = 15e3;
@@ -1004,6 +1265,34 @@ async function setupMockInterception(page, devServerUrl, mockConfig, authConfig)
1004
1265
  }));
1005
1266
  await page.context().addCookies(cookies);
1006
1267
  }
1268
+ if (authConfig.hasAuth) {
1269
+ const mockUser = mockConfig.authMock?.authCheckResponse?.body ?? {
1270
+ id: "mock_user",
1271
+ email: "demo@c2d.dev",
1272
+ name: "Demo User"
1273
+ };
1274
+ const mockToken = "c2d_mock_jwt_token";
1275
+ await page.goto(devServerUrl, { waitUntil: "commit", timeout: 1e4 }).catch(() => {
1276
+ });
1277
+ await page.evaluate(`(() => {
1278
+ // Zustand persist pattern (auth-storage)
1279
+ const zustandState = {
1280
+ state: {
1281
+ user: ${JSON.stringify(mockUser)},
1282
+ tokens: { access: "${mockToken}", refresh: "${mockToken}" },
1283
+ isAuthenticated: true,
1284
+ isLoading: false,
1285
+ },
1286
+ version: 0,
1287
+ };
1288
+ localStorage.setItem('auth-storage', JSON.stringify(zustandState));
1289
+
1290
+ // Common alternatives
1291
+ localStorage.setItem('token', '"${mockToken}"');
1292
+ localStorage.setItem('access_token', '"${mockToken}"');
1293
+ localStorage.setItem('user', JSON.stringify(${JSON.stringify(mockUser)}));
1294
+ })()`);
1295
+ }
1007
1296
  const authMockBody = mockConfig.authMock?.authCheckResponse ? JSON.stringify(mockConfig.authMock.authCheckResponse.body) : JSON.stringify({ id: "mock_user", email: "demo@c2d.dev", name: "Demo User", email_verified: true, avatar_url: null, created_at: "2026-01-01T00:00:00Z" });
1008
1297
  const AUTH_PATTERNS = ["/auth/me", "/auth/session", "/api/auth/me", "/api/user/me", "/api/auth/session"];
1009
1298
  await page.route("**/*", async (route) => {
@@ -1020,12 +1309,7 @@ async function setupMockInterception(page, devServerUrl, mockConfig, authConfig)
1020
1309
  });
1021
1310
  }
1022
1311
  for (const [pattern, mock] of Object.entries(mockConfig.apiMocks)) {
1023
- const [mockMethod, ...pathParts] = pattern.split(" ");
1024
- let mockPath = pathParts.join(" ");
1025
- mockPath = mockPath.replace(/\{[^}]+\}/g, "[^/]+");
1026
- mockPath = mockPath.replace(/\*/g, "[^/]+");
1027
- const pathRegex = new RegExp(mockPath.replace(/\//g, "\\/"));
1028
- if (method === mockMethod && pathRegex.test(url)) {
1312
+ if (matchMockUrl(url, method, pattern)) {
1029
1313
  if (mock.delay) await new Promise((r) => setTimeout(r, mock.delay));
1030
1314
  return route.fulfill({
1031
1315
  status: mock.status,
@@ -1034,6 +1318,13 @@ async function setupMockInterception(page, devServerUrl, mockConfig, authConfig)
1034
1318
  });
1035
1319
  }
1036
1320
  }
1321
+ if (isApiUrl(url)) {
1322
+ return route.fulfill({
1323
+ status: 200,
1324
+ contentType: "application/json",
1325
+ body: JSON.stringify({ data: [], message: "ok" })
1326
+ });
1327
+ }
1037
1328
  return route.continue();
1038
1329
  });
1039
1330
  }
@@ -1888,4 +2179,4 @@ export {
1888
2179
  detectNextJsProject,
1889
2180
  runScan
1890
2181
  };
1891
- //# sourceMappingURL=chunk-HYFKUTC3.js.map
2182
+ //# sourceMappingURL=chunk-3CJ7RVFI.js.map