@designtools/codesurface 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1297,8 +1297,8 @@ function categorizeToken(name, value) {
1297
1297
  return "other";
1298
1298
  }
1299
1299
  function getTokenGroup(name) {
1300
- const n3 = name.replace(/^--/, "");
1301
- const scaleMatch = n3.match(/^([\w]+)-\d+$/);
1300
+ const n4 = name.replace(/^--/, "");
1301
+ const scaleMatch = n4.match(/^([\w]+)-\d+$/);
1302
1302
  if (scaleMatch) return scaleMatch[1];
1303
1303
  const semanticPrefixes = [
1304
1304
  "primary",
@@ -1309,19 +1309,19 @@ function getTokenGroup(name) {
1309
1309
  "warning"
1310
1310
  ];
1311
1311
  for (const prefix of semanticPrefixes) {
1312
- if (n3 === prefix || n3.startsWith(`${prefix}-`)) return prefix;
1312
+ if (n4 === prefix || n4.startsWith(`${prefix}-`)) return prefix;
1313
1313
  }
1314
- if (["background", "foreground", "card", "card-foreground", "popover", "popover-foreground"].includes(n3)) {
1314
+ if (["background", "foreground", "card", "card-foreground", "popover", "popover-foreground"].includes(n4)) {
1315
1315
  return "surface";
1316
1316
  }
1317
- if (["border", "input", "ring", "muted", "muted-foreground", "accent", "accent-foreground"].includes(n3)) {
1317
+ if (["border", "input", "ring", "muted", "muted-foreground", "accent", "accent-foreground"].includes(n4)) {
1318
1318
  return "utility";
1319
1319
  }
1320
- if (n3.startsWith("chart")) return "chart";
1321
- if (n3.startsWith("sidebar")) return "sidebar";
1322
- if (n3.startsWith("radius")) return "radius";
1323
- if (n3.startsWith("shadow")) return "shadow";
1324
- if (n3.startsWith("border-width")) return "border";
1320
+ if (n4.startsWith("chart")) return "chart";
1321
+ if (n4.startsWith("sidebar")) return "sidebar";
1322
+ if (n4.startsWith("radius")) return "radius";
1323
+ if (n4.startsWith("shadow")) return "shadow";
1324
+ if (n4.startsWith("border-width")) return "border";
1325
1325
  return "other";
1326
1326
  }
1327
1327
  function detectColorFormat(value) {
@@ -1335,6 +1335,8 @@ function detectColorFormat(value) {
1335
1335
  // src/server/lib/scan-components.ts
1336
1336
  import fs5 from "fs/promises";
1337
1337
  import path5 from "path";
1338
+ import recast2 from "recast";
1339
+ import { namedTypes as n3 } from "ast-types";
1338
1340
  async function scanComponents(projectRoot) {
1339
1341
  const componentDirs = [
1340
1342
  "components/ui",
@@ -1355,86 +1357,253 @@ async function scanComponents(projectRoot) {
1355
1357
  const fullDir = path5.join(projectRoot, componentDir);
1356
1358
  const files = await fs5.readdir(fullDir);
1357
1359
  const tsxFiles = files.filter((f) => f.endsWith(".tsx"));
1360
+ const parser = await getParser();
1358
1361
  const components = [];
1359
1362
  for (const file of tsxFiles) {
1360
1363
  const filePath = path5.join(componentDir, file);
1361
1364
  const source = await fs5.readFile(path5.join(projectRoot, filePath), "utf-8");
1362
- const entry = parseComponent(source, filePath);
1363
- if (entry) {
1364
- components.push(entry);
1365
- }
1365
+ const entries = parseComponentAST(source, filePath, parser);
1366
+ components.push(...entries);
1366
1367
  }
1367
1368
  return { components };
1368
1369
  }
1369
- function parseComponent(source, filePath) {
1370
- const cvaMatch = source.match(
1371
- /const\s+(\w+)\s*=\s*cva\(\s*(["'`])([\s\S]*?)\2\s*,\s*\{/
1372
- );
1373
- const slotMatch = source.match(/data-slot=["'](\w+)["']/);
1374
- if (!slotMatch) return null;
1375
- const dataSlot = slotMatch[1];
1376
- const name = dataSlot.charAt(0).toUpperCase() + dataSlot.slice(1);
1377
- if (!cvaMatch) {
1378
- return {
1370
+ function parseComponentAST(source, filePath, parser) {
1371
+ let ast;
1372
+ try {
1373
+ ast = recast2.parse(source, { parser });
1374
+ } catch {
1375
+ return [];
1376
+ }
1377
+ const cvaMap = /* @__PURE__ */ new Map();
1378
+ const slotToComponent = /* @__PURE__ */ new Map();
1379
+ const exportedNames = /* @__PURE__ */ new Set();
1380
+ const acceptsChildrenSet = /* @__PURE__ */ new Set();
1381
+ let currentComponentName = null;
1382
+ recast2.visit(ast, {
1383
+ // Find cva() calls: const fooVariants = cva("base classes", { variants: {...}, defaultVariants: {...} })
1384
+ visitVariableDeclaration(path17) {
1385
+ for (const decl of path17.node.declarations) {
1386
+ if (n3.VariableDeclarator.check(decl) && n3.Identifier.check(decl.id) && n3.CallExpression.check(decl.init) && isIdentifierNamed(decl.init.callee, "cva")) {
1387
+ const varName = decl.id.name;
1388
+ const args = decl.init.arguments;
1389
+ const baseClasses = extractStringValue(args[0]) || "";
1390
+ const configArg = args[1];
1391
+ const variants = configArg && n3.ObjectExpression.check(configArg) ? extractVariantsFromConfig(configArg) : [];
1392
+ cvaMap.set(varName, { baseClasses, variants });
1393
+ }
1394
+ }
1395
+ this.traverse(path17);
1396
+ },
1397
+ // Find forwardRef and function components to track data-slot and currentComponentName
1398
+ visitCallExpression(path17) {
1399
+ const node = path17.node;
1400
+ if (isForwardRefCall(node)) {
1401
+ const parent = path17.parent?.node;
1402
+ if (n3.VariableDeclarator.check(parent) && n3.Identifier.check(parent.id)) {
1403
+ currentComponentName = parent.id.name;
1404
+ }
1405
+ }
1406
+ this.traverse(path17);
1407
+ },
1408
+ // Find data-slot JSX attributes
1409
+ visitJSXAttribute(path17) {
1410
+ const attr = path17.node;
1411
+ if (n3.JSXIdentifier.check(attr.name) && attr.name.name === "data-slot" && n3.StringLiteral.check(attr.value)) {
1412
+ const slotValue = attr.value.value;
1413
+ const compName = findEnclosingComponentName(path17) || currentComponentName;
1414
+ if (compName) {
1415
+ slotToComponent.set(slotValue, compName);
1416
+ }
1417
+ }
1418
+ this.traverse(path17);
1419
+ },
1420
+ // Detect {...props} spread in JSX — indicates component accepts children
1421
+ visitJSXSpreadAttribute(path17) {
1422
+ const expr = path17.node.argument;
1423
+ if (n3.Identifier.check(expr) && expr.name === "props") {
1424
+ const compName = findEnclosingComponentName(path17) || currentComponentName;
1425
+ if (compName) {
1426
+ acceptsChildrenSet.add(compName);
1427
+ }
1428
+ }
1429
+ this.traverse(path17);
1430
+ },
1431
+ // Collect named exports: export { Button, Card, ... }
1432
+ visitExportNamedDeclaration(path17) {
1433
+ const node = path17.node;
1434
+ if (node.specifiers) {
1435
+ for (const spec of node.specifiers) {
1436
+ if (n3.ExportSpecifier.check(spec) && n3.Identifier.check(spec.exported)) {
1437
+ exportedNames.add(spec.exported.name);
1438
+ }
1439
+ }
1440
+ }
1441
+ if (node.declaration) {
1442
+ if (n3.VariableDeclaration.check(node.declaration)) {
1443
+ for (const decl of node.declaration.declarations) {
1444
+ if (n3.VariableDeclarator.check(decl) && n3.Identifier.check(decl.id)) {
1445
+ exportedNames.add(decl.id.name);
1446
+ }
1447
+ }
1448
+ } else if (n3.FunctionDeclaration.check(node.declaration) && node.declaration.id) {
1449
+ exportedNames.add(node.declaration.id.name);
1450
+ }
1451
+ }
1452
+ this.traverse(path17);
1453
+ },
1454
+ // export default function Foo
1455
+ visitExportDefaultDeclaration(path17) {
1456
+ const decl = path17.node.declaration;
1457
+ if (n3.FunctionDeclaration.check(decl) && decl.id) {
1458
+ exportedNames.add(decl.id.name);
1459
+ }
1460
+ this.traverse(path17);
1461
+ }
1462
+ });
1463
+ recast2.visit(ast, {
1464
+ visitFunctionDeclaration(path17) {
1465
+ const name = path17.node.id ? String(path17.node.id.name) : null;
1466
+ if (name) {
1467
+ currentComponentName = name;
1468
+ this.traverse(path17);
1469
+ currentComponentName = null;
1470
+ } else {
1471
+ this.traverse(path17);
1472
+ }
1473
+ },
1474
+ visitJSXAttribute(path17) {
1475
+ const attr = path17.node;
1476
+ if (n3.JSXIdentifier.check(attr.name) && attr.name.name === "data-slot" && n3.StringLiteral.check(attr.value)) {
1477
+ const slotValue = attr.value.value;
1478
+ if (!slotToComponent.has(slotValue) && currentComponentName) {
1479
+ slotToComponent.set(slotValue, currentComponentName);
1480
+ }
1481
+ }
1482
+ this.traverse(path17);
1483
+ }
1484
+ });
1485
+ const tokenRefs = extractTokenReferences(source);
1486
+ const entries = [];
1487
+ for (const [dataSlot, componentName] of slotToComponent) {
1488
+ const exportName = exportedNames.has(componentName) ? componentName : null;
1489
+ if (!exportName) continue;
1490
+ const cvaVarName = findCvaForComponent(componentName, cvaMap);
1491
+ const cvaData = cvaVarName ? cvaMap.get(cvaVarName) : null;
1492
+ const name = dataSlot.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
1493
+ entries.push({
1379
1494
  name,
1380
1495
  filePath,
1381
1496
  dataSlot,
1382
- baseClasses: "",
1383
- variants: [],
1384
- tokenReferences: extractTokenReferences(source)
1385
- };
1497
+ exportName,
1498
+ baseClasses: cvaData?.baseClasses || "",
1499
+ variants: cvaData?.variants || [],
1500
+ tokenReferences: tokenRefs,
1501
+ acceptsChildren: acceptsChildrenSet.has(componentName)
1502
+ });
1386
1503
  }
1387
- const baseClasses = cvaMatch[3].trim();
1388
- const variants = parseVariants(source);
1389
- const tokenReferences = extractTokenReferences(source);
1390
- return {
1391
- name,
1392
- filePath,
1393
- dataSlot,
1394
- baseClasses,
1395
- variants,
1396
- tokenReferences
1397
- };
1504
+ return entries;
1505
+ }
1506
+ function isIdentifierNamed(node, name) {
1507
+ return n3.Identifier.check(node) && node.name === name;
1508
+ }
1509
+ function isObjectProperty(node) {
1510
+ return n3.Property.check(node) || n3.ObjectProperty.check(node);
1511
+ }
1512
+ function isForwardRefCall(node) {
1513
+ if (!n3.CallExpression.check(node)) return false;
1514
+ if (isIdentifierNamed(node.callee, "forwardRef")) return true;
1515
+ if (n3.MemberExpression.check(node.callee) && isIdentifierNamed(node.callee.object, "React") && isIdentifierNamed(node.callee.property, "forwardRef")) {
1516
+ return true;
1517
+ }
1518
+ return false;
1398
1519
  }
1399
- function parseVariants(source) {
1520
+ function extractStringValue(node) {
1521
+ if (!node) return null;
1522
+ if (n3.StringLiteral.check(node) || n3.Literal.check(node) && typeof node.value === "string") {
1523
+ return String(node.value);
1524
+ }
1525
+ if (n3.TemplateLiteral.check(node) && node.expressions.length === 0 && node.quasis.length === 1) {
1526
+ return node.quasis[0].value.cooked || node.quasis[0].value.raw;
1527
+ }
1528
+ return null;
1529
+ }
1530
+ function extractVariantsFromConfig(configObj) {
1400
1531
  const dimensions = [];
1401
- const variantsBlock = source.match(/variants\s*:\s*\{([\s\S]*?)\}\s*,?\s*defaultVariants/);
1402
- if (!variantsBlock) return dimensions;
1403
- const block = variantsBlock[1];
1404
- const dimRegex = /(\w+)\s*:\s*\{([^}]+)\}/g;
1405
- let dimMatch;
1406
- while ((dimMatch = dimRegex.exec(block)) !== null) {
1407
- const dimName = dimMatch[1];
1408
- const dimBody = dimMatch[2];
1532
+ const variantsProp = findObjProperty(configObj, "variants");
1533
+ if (!variantsProp || !n3.ObjectExpression.check(variantsProp.value)) return dimensions;
1534
+ const defaultVariantsProp = findObjProperty(configObj, "defaultVariants");
1535
+ const defaults = {};
1536
+ if (defaultVariantsProp && n3.ObjectExpression.check(defaultVariantsProp.value)) {
1537
+ for (const prop of defaultVariantsProp.value.properties) {
1538
+ if (isObjectProperty(prop)) {
1539
+ const key = getPropertyKeyName(prop);
1540
+ const val = extractStringValue(prop.value);
1541
+ if (key && val) defaults[key] = val;
1542
+ }
1543
+ }
1544
+ }
1545
+ for (const dimProp of variantsProp.value.properties) {
1546
+ if (!isObjectProperty(dimProp)) continue;
1547
+ const dimName = getPropertyKeyName(dimProp);
1548
+ if (!dimName || !n3.ObjectExpression.check(dimProp.value)) continue;
1409
1549
  const options = [];
1410
1550
  const classes = {};
1411
- const optRegex = /["']?([\w-]+)["']?\s*:\s*\n?\s*["'`]([^"'`]*)["'`]/g;
1412
- let optMatch;
1413
- while ((optMatch = optRegex.exec(dimBody)) !== null) {
1414
- options.push(optMatch[1]);
1415
- classes[optMatch[1]] = optMatch[2].trim();
1416
- }
1417
- const defaultVariantsSection = source.match(
1418
- /defaultVariants\s*:\s*\{([^}]+)\}/
1419
- );
1420
- let defaultVal = options[0] || "";
1421
- if (defaultVariantsSection) {
1422
- const defMatch = defaultVariantsSection[1].match(
1423
- new RegExp(`${dimName}\\s*:\\s*["'](\\w+)["']`)
1424
- );
1425
- if (defMatch) defaultVal = defMatch[1];
1551
+ for (const optProp of dimProp.value.properties) {
1552
+ if (!isObjectProperty(optProp)) continue;
1553
+ const optName = getPropertyKeyName(optProp);
1554
+ const optValue = extractStringValue(optProp.value);
1555
+ if (optName) {
1556
+ options.push(optName);
1557
+ classes[optName] = optValue || "";
1558
+ }
1426
1559
  }
1427
1560
  if (options.length > 0) {
1428
1561
  dimensions.push({
1429
1562
  name: dimName,
1430
1563
  options,
1431
- default: defaultVal,
1564
+ default: defaults[dimName] || options[0],
1432
1565
  classes
1433
1566
  });
1434
1567
  }
1435
1568
  }
1436
1569
  return dimensions;
1437
1570
  }
1571
+ function findObjProperty(obj, name) {
1572
+ for (const prop of obj.properties) {
1573
+ if (isObjectProperty(prop) && getPropertyKeyName(prop) === name) {
1574
+ return prop;
1575
+ }
1576
+ }
1577
+ return null;
1578
+ }
1579
+ function getPropertyKeyName(prop) {
1580
+ if (n3.Identifier.check(prop.key)) return prop.key.name;
1581
+ if (n3.StringLiteral.check(prop.key) || n3.Literal.check(prop.key) && typeof prop.key.value === "string") {
1582
+ return prop.key.value;
1583
+ }
1584
+ return null;
1585
+ }
1586
+ function findEnclosingComponentName(astPath) {
1587
+ let current = astPath.parent;
1588
+ while (current) {
1589
+ const node = current.node;
1590
+ if (n3.VariableDeclarator.check(node) && n3.Identifier.check(node.id)) {
1591
+ return node.id.name;
1592
+ }
1593
+ if (n3.FunctionDeclaration.check(node) && node.id) {
1594
+ return node.id.name;
1595
+ }
1596
+ current = current.parent;
1597
+ }
1598
+ return null;
1599
+ }
1600
+ function findCvaForComponent(componentName, cvaMap) {
1601
+ const lower = componentName.charAt(0).toLowerCase() + componentName.slice(1);
1602
+ const expected = `${lower}Variants`;
1603
+ if (cvaMap.has(expected)) return expected;
1604
+ if (cvaMap.size === 1) return cvaMap.keys().next().value;
1605
+ return null;
1606
+ }
1438
1607
  function extractTokenReferences(source) {
1439
1608
  const tokens = /* @__PURE__ */ new Set();
1440
1609
  const classStrings = source.match(/["'`][^"'`]*["'`]/g) || [];
@@ -1443,8 +1612,7 @@ function extractTokenReferences(source) {
1443
1612
  let match;
1444
1613
  while ((match = tokenPattern.exec(str)) !== null) {
1445
1614
  const val = match[1];
1446
- if (!val.match(/^\d/) && // not a number
1447
- !["xs", "sm", "md", "lg", "xl", "2xl", "3xl", "full", "none"].includes(val) && !val.startsWith("[")) {
1615
+ if (!val.match(/^\d/) && !["xs", "sm", "md", "lg", "xl", "2xl", "3xl", "full", "none"].includes(val) && !val.startsWith("[")) {
1448
1616
  tokens.add(val.split("/")[0]);
1449
1617
  }
1450
1618
  }