@designtools/codesurface 0.1.9 → 0.1.10

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.
Files changed (2) hide show
  1. package/dist/cli.js +320 -25
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -41,8 +41,25 @@ async function detectFramework(projectRoot) {
41
41
  componentDirCandidates = ["components/ui", "src/components/ui"];
42
42
  }
43
43
  const appResult = await findDir(projectRoot, appDirCandidates);
44
- const componentResult = await findDir(projectRoot, componentDirCandidates);
45
- const componentFileCount = componentResult.exists ? await countFiles(projectRoot, componentResult.dir, ".tsx") : 0;
44
+ let componentResult = await findDir(projectRoot, componentDirCandidates);
45
+ let componentFileCount = componentResult.exists ? await countComponentFiles(projectRoot, componentResult.dir) : 0;
46
+ let fallbackComponentDirs = [];
47
+ if (!componentResult.exists) {
48
+ fallbackComponentDirs = await scanForComponentDirs(projectRoot);
49
+ if (fallbackComponentDirs.length > 0) {
50
+ const best = fallbackComponentDirs[0];
51
+ componentResult = { dir: best.dir, exists: true };
52
+ componentFileCount = best.fileCount;
53
+ }
54
+ }
55
+ let cssFiles = await findCssFiles(projectRoot);
56
+ let fallbackCssFiles = [];
57
+ if (cssFiles.length === 0) {
58
+ fallbackCssFiles = await scanForCssFiles(projectRoot);
59
+ if (fallbackCssFiles.length > 0) {
60
+ cssFiles = fallbackCssFiles;
61
+ }
62
+ }
46
63
  return {
47
64
  name,
48
65
  appDir: appResult.dir,
@@ -50,7 +67,9 @@ async function detectFramework(projectRoot) {
50
67
  componentDir: componentResult.dir,
51
68
  componentDirExists: componentResult.exists,
52
69
  componentFileCount,
53
- cssFiles: await findCssFiles(projectRoot)
70
+ cssFiles,
71
+ fallbackComponentDirs,
72
+ fallbackCssFiles
54
73
  };
55
74
  }
56
75
  async function findDir(root, candidates) {
@@ -64,11 +83,11 @@ async function findDir(root, candidates) {
64
83
  }
65
84
  return { dir: candidates[0], exists: false };
66
85
  }
67
- async function countFiles(root, dir, ext) {
86
+ async function countComponentFiles(root, dir) {
68
87
  const full = path.join(root, dir);
69
88
  try {
70
89
  const entries = await fs.readdir(full);
71
- return entries.filter((e) => e.endsWith(ext)).length;
90
+ return entries.filter((e) => e.endsWith(".tsx") || e.endsWith(".jsx")).length;
72
91
  } catch {
73
92
  return 0;
74
93
  }
@@ -93,6 +112,98 @@ async function findCssFiles(projectRoot) {
93
112
  }
94
113
  return found;
95
114
  }
115
+ var COMPONENT_SCAN_DIRS = [
116
+ "components",
117
+ "src/components",
118
+ "lib",
119
+ "src/lib",
120
+ "ui",
121
+ "src/ui"
122
+ ];
123
+ async function scanForComponentDirs(projectRoot) {
124
+ const results = [];
125
+ for (const parentDir of COMPONENT_SCAN_DIRS) {
126
+ const fullParent = path.join(projectRoot, parentDir);
127
+ let stat;
128
+ try {
129
+ stat = await fs.stat(fullParent);
130
+ } catch {
131
+ continue;
132
+ }
133
+ if (!stat.isDirectory()) continue;
134
+ const hits = await countDataSlotFiles(fullParent);
135
+ if (hits > 0) {
136
+ results.push({ dir: parentDir, fileCount: hits });
137
+ }
138
+ try {
139
+ const entries = await fs.readdir(fullParent, { withFileTypes: true });
140
+ for (const entry of entries) {
141
+ if (!entry.isDirectory()) continue;
142
+ const subDir = path.join(parentDir, entry.name);
143
+ const subHits = await countDataSlotFiles(path.join(projectRoot, subDir));
144
+ if (subHits > 0) {
145
+ results.push({ dir: subDir, fileCount: subHits });
146
+ }
147
+ }
148
+ } catch {
149
+ }
150
+ }
151
+ results.sort((a, b3) => b3.fileCount - a.fileCount);
152
+ return results;
153
+ }
154
+ async function countDataSlotFiles(dirPath) {
155
+ let count = 0;
156
+ try {
157
+ const files = await fs.readdir(dirPath);
158
+ for (const file of files) {
159
+ if (!file.endsWith(".tsx") && !file.endsWith(".jsx")) continue;
160
+ try {
161
+ const content = await fs.readFile(path.join(dirPath, file), "utf-8");
162
+ if (content.includes("data-slot")) {
163
+ count++;
164
+ }
165
+ } catch {
166
+ }
167
+ }
168
+ } catch {
169
+ }
170
+ return count;
171
+ }
172
+ var CSS_SCAN_DIRS = [
173
+ "app",
174
+ "src/app",
175
+ "src",
176
+ "styles",
177
+ "assets",
178
+ "theme",
179
+ "css"
180
+ ];
181
+ var CSS_CUSTOM_PROPERTY_RE = /--[\w-]+\s*:/;
182
+ async function scanForCssFiles(projectRoot) {
183
+ const found = [];
184
+ for (const dir of CSS_SCAN_DIRS) {
185
+ const fullDir = path.join(projectRoot, dir);
186
+ let entries;
187
+ try {
188
+ entries = await fs.readdir(fullDir);
189
+ } catch {
190
+ continue;
191
+ }
192
+ for (const file of entries) {
193
+ if (!file.endsWith(".css")) continue;
194
+ const relPath = path.join(dir, file);
195
+ if (found.includes(relPath)) continue;
196
+ try {
197
+ const content = await fs.readFile(path.join(projectRoot, relPath), "utf-8");
198
+ if (CSS_CUSTOM_PROPERTY_RE.test(content)) {
199
+ found.push(relPath);
200
+ }
201
+ } catch {
202
+ }
203
+ }
204
+ }
205
+ return found;
206
+ }
96
207
 
97
208
  // src/server/lib/detect-styling.ts
98
209
  import fs2 from "fs/promises";
@@ -1337,18 +1448,20 @@ import fs5 from "fs/promises";
1337
1448
  import path5 from "path";
1338
1449
  import recast2 from "recast";
1339
1450
  import { namedTypes as n3 } from "ast-types";
1340
- async function scanComponents(projectRoot) {
1341
- const componentDirs = [
1342
- "components/ui",
1343
- "src/components/ui"
1344
- ];
1345
- let componentDir = "";
1346
- for (const dir of componentDirs) {
1347
- try {
1348
- await fs5.access(path5.join(projectRoot, dir));
1349
- componentDir = dir;
1350
- break;
1351
- } catch {
1451
+ async function scanComponents(projectRoot, overrideDir) {
1452
+ let componentDir = overrideDir || "";
1453
+ if (!componentDir) {
1454
+ const componentDirs = [
1455
+ "components/ui",
1456
+ "src/components/ui"
1457
+ ];
1458
+ for (const dir of componentDirs) {
1459
+ try {
1460
+ await fs5.access(path5.join(projectRoot, dir));
1461
+ componentDir = dir;
1462
+ break;
1463
+ } catch {
1464
+ }
1352
1465
  }
1353
1466
  }
1354
1467
  if (!componentDir) {
@@ -1356,7 +1469,7 @@ async function scanComponents(projectRoot) {
1356
1469
  }
1357
1470
  const fullDir = path5.join(projectRoot, componentDir);
1358
1471
  const files = await fs5.readdir(fullDir);
1359
- const tsxFiles = files.filter((f) => f.endsWith(".tsx"));
1472
+ const tsxFiles = files.filter((f) => f.endsWith(".tsx") || f.endsWith(".jsx"));
1360
1473
  const parser = await getParser();
1361
1474
  const components = [];
1362
1475
  for (const file of tsxFiles) {
@@ -2535,12 +2648,15 @@ function isFromComponentDir(importPath, componentDir, _parentDir, leafDir) {
2535
2648
 
2536
2649
  // src/server/lib/scanner.ts
2537
2650
  var cachedScan = null;
2651
+ var predetectedFramework;
2652
+ var predetectedStyling;
2538
2653
  async function runScan(projectRoot) {
2539
- const framework = await detectFramework(projectRoot);
2540
- const styling = await detectStylingSystem(projectRoot, framework);
2654
+ const framework = predetectedFramework || await detectFramework(projectRoot);
2655
+ const styling = predetectedStyling || await detectStylingSystem(projectRoot, framework);
2656
+ const componentDir = framework.componentDirExists ? framework.componentDir : void 0;
2541
2657
  const [tokens, components, shadows, borders, gradients, spacing, usages] = await Promise.all([
2542
2658
  scanTokens(projectRoot, framework),
2543
- scanComponents(projectRoot),
2659
+ scanComponents(projectRoot, componentDir),
2544
2660
  scanShadows(projectRoot, framework, styling),
2545
2661
  scanBorders(projectRoot, framework, styling),
2546
2662
  scanGradients(projectRoot, framework, styling),
@@ -2585,7 +2701,9 @@ async function rescanGradients(projectRoot) {
2585
2701
  patchCachedScan("gradients", gradients);
2586
2702
  return gradients;
2587
2703
  }
2588
- function createScanRouter(projectRoot) {
2704
+ function createScanRouter(projectRoot, framework, styling) {
2705
+ predetectedFramework = framework;
2706
+ predetectedStyling = styling;
2589
2707
  const router = Router2();
2590
2708
  runScan(projectRoot).then(() => {
2591
2709
  console.log(" Project scanned successfully");
@@ -3383,7 +3501,7 @@ async function createServer(config) {
3383
3501
  };
3384
3502
  tryOpen();
3385
3503
  });
3386
- app.use("/scan", createScanRouter(config.projectRoot));
3504
+ app.use("/scan", createScanRouter(config.projectRoot, config.framework, config.styling));
3387
3505
  const __dirname = path16.dirname(fileURLToPath(import.meta.url));
3388
3506
  const clientDistPath = path16.join(__dirname, "client");
3389
3507
  const isDev = !fs18.existsSync(path16.join(clientDistPath, "index.html"));
@@ -3468,10 +3586,150 @@ async function promptPort(message, options) {
3468
3586
  });
3469
3587
  });
3470
3588
  }
3589
+ async function promptComponentDir(framework, projectRoot) {
3590
+ const candidates = framework.fallbackComponentDirs.length > 0 ? framework.fallbackComponentDirs : await findDirsWithComponentFiles(projectRoot);
3591
+ if (candidates.length === 0) {
3592
+ return { dir: framework.componentDir, exists: false, fileCount: 0 };
3593
+ }
3594
+ const rl = readline.createInterface({ input: process2.stdin, output: process2.stdout });
3595
+ console.log("");
3596
+ console.log(` ${yellow("?")} Where are your UI components?`);
3597
+ for (let i = 0; i < candidates.length; i++) {
3598
+ const c = candidates[i];
3599
+ console.log(` ${cyan(String(i + 1))}. ${c.dir}/ ${dim(`(${c.fileCount} files)`)}`);
3600
+ }
3601
+ const enterIdx = candidates.length + 1;
3602
+ const skipIdx = candidates.length + 2;
3603
+ console.log(` ${cyan(String(enterIdx))}. Enter a path`);
3604
+ console.log(` ${cyan(String(skipIdx))}. Skip \u2014 continue without component editing`);
3605
+ console.log("");
3606
+ return new Promise((resolve) => {
3607
+ rl.question(` ${dim("Choice [1]:")} `, (answer) => {
3608
+ rl.close();
3609
+ const trimmed = answer.trim();
3610
+ if (!trimmed || trimmed === "1") {
3611
+ const best = candidates[0];
3612
+ resolve({ dir: best.dir, exists: true, fileCount: best.fileCount });
3613
+ return;
3614
+ }
3615
+ const idx = parseInt(trimmed, 10);
3616
+ if (idx >= 1 && idx <= candidates.length) {
3617
+ const picked = candidates[idx - 1];
3618
+ resolve({ dir: picked.dir, exists: true, fileCount: picked.fileCount });
3619
+ return;
3620
+ }
3621
+ if (idx === enterIdx) {
3622
+ const rl2 = readline.createInterface({ input: process2.stdin, output: process2.stdout });
3623
+ rl2.question(` ${dim("Path:")} `, (pathAnswer) => {
3624
+ rl2.close();
3625
+ const customDir = pathAnswer.trim();
3626
+ if (customDir && fs19.existsSync(path17.join(projectRoot, customDir))) {
3627
+ resolve({ dir: customDir, exists: true, fileCount: 0 });
3628
+ } else {
3629
+ resolve({ dir: customDir || framework.componentDir, exists: false, fileCount: 0 });
3630
+ }
3631
+ });
3632
+ return;
3633
+ }
3634
+ resolve({ dir: framework.componentDir, exists: false, fileCount: 0 });
3635
+ });
3636
+ });
3637
+ }
3638
+ async function promptCssFile(framework, projectRoot) {
3639
+ const candidates = framework.fallbackCssFiles;
3640
+ if (candidates.length === 0) {
3641
+ return [];
3642
+ }
3643
+ const rl = readline.createInterface({ input: process2.stdin, output: process2.stdout });
3644
+ console.log("");
3645
+ console.log(` ${yellow("?")} Which CSS file has your design tokens / custom properties?`);
3646
+ for (let i = 0; i < candidates.length; i++) {
3647
+ console.log(` ${cyan(String(i + 1))}. ${candidates[i]}`);
3648
+ }
3649
+ const enterIdx = candidates.length + 1;
3650
+ const skipIdx = candidates.length + 2;
3651
+ console.log(` ${cyan(String(enterIdx))}. Enter a path`);
3652
+ console.log(` ${cyan(String(skipIdx))}. Skip \u2014 continue without token editing`);
3653
+ console.log("");
3654
+ return new Promise((resolve) => {
3655
+ rl.question(` ${dim("Choice [1]:")} `, (answer) => {
3656
+ rl.close();
3657
+ const trimmed = answer.trim();
3658
+ if (!trimmed || trimmed === "1") {
3659
+ resolve([candidates[0]]);
3660
+ return;
3661
+ }
3662
+ const idx = parseInt(trimmed, 10);
3663
+ if (idx >= 1 && idx <= candidates.length) {
3664
+ resolve([candidates[idx - 1]]);
3665
+ return;
3666
+ }
3667
+ if (idx === enterIdx) {
3668
+ const rl2 = readline.createInterface({ input: process2.stdin, output: process2.stdout });
3669
+ rl2.question(` ${dim("Path:")} `, (pathAnswer) => {
3670
+ rl2.close();
3671
+ const customPath = pathAnswer.trim();
3672
+ if (customPath && fs19.existsSync(path17.join(projectRoot, customPath))) {
3673
+ resolve([customPath]);
3674
+ } else {
3675
+ resolve([]);
3676
+ }
3677
+ });
3678
+ return;
3679
+ }
3680
+ resolve([]);
3681
+ });
3682
+ });
3683
+ }
3684
+ async function findDirsWithComponentFiles(projectRoot) {
3685
+ const scanDirs = [
3686
+ "components",
3687
+ "src/components",
3688
+ "lib",
3689
+ "src/lib",
3690
+ "ui",
3691
+ "src/ui"
3692
+ ];
3693
+ const results = [];
3694
+ for (const dir of scanDirs) {
3695
+ const fullDir = path17.join(projectRoot, dir);
3696
+ try {
3697
+ const stat = fs19.statSync(fullDir);
3698
+ if (!stat.isDirectory()) continue;
3699
+ } catch {
3700
+ continue;
3701
+ }
3702
+ try {
3703
+ const files = fs19.readdirSync(fullDir);
3704
+ const count = files.filter((f) => f.endsWith(".tsx") || f.endsWith(".jsx")).length;
3705
+ if (count > 0) {
3706
+ results.push({ dir, fileCount: count });
3707
+ }
3708
+ for (const file of files) {
3709
+ const subPath = path17.join(fullDir, file);
3710
+ try {
3711
+ if (!fs19.statSync(subPath).isDirectory()) continue;
3712
+ } catch {
3713
+ continue;
3714
+ }
3715
+ const subFiles = fs19.readdirSync(subPath);
3716
+ const subCount = subFiles.filter((f) => f.endsWith(".tsx") || f.endsWith(".jsx")).length;
3717
+ if (subCount > 0) {
3718
+ results.push({ dir: `${dir}/${file}`, fileCount: subCount });
3719
+ }
3720
+ }
3721
+ } catch {
3722
+ }
3723
+ }
3724
+ results.sort((a, b3) => b3.fileCount - a.fileCount);
3725
+ return results;
3726
+ }
3471
3727
  async function main() {
3472
3728
  const args = process2.argv.slice(2);
3473
3729
  let targetPort = 3e3;
3474
3730
  let toolPort = 4400;
3731
+ let componentsOverride;
3732
+ let cssOverride;
3475
3733
  for (let i = 0; i < args.length; i++) {
3476
3734
  if (args[i] === "--port" && args[i + 1]) {
3477
3735
  targetPort = parseInt(args[i + 1], 10);
@@ -3481,6 +3739,14 @@ async function main() {
3481
3739
  toolPort = parseInt(args[i + 1], 10);
3482
3740
  i++;
3483
3741
  }
3742
+ if (args[i] === "--components" && args[i + 1]) {
3743
+ componentsOverride = args[i + 1];
3744
+ i++;
3745
+ }
3746
+ if (args[i] === "--css" && args[i + 1]) {
3747
+ cssOverride = args[i + 1];
3748
+ i++;
3749
+ }
3484
3750
  }
3485
3751
  const projectRoot = process2.cwd();
3486
3752
  console.log("");
@@ -3496,6 +3762,17 @@ async function main() {
3496
3762
  process2.exit(1);
3497
3763
  }
3498
3764
  const framework = await detectFramework(projectRoot);
3765
+ if (componentsOverride) {
3766
+ framework.componentDir = componentsOverride;
3767
+ framework.componentDirExists = fs19.existsSync(path17.join(projectRoot, componentsOverride));
3768
+ if (framework.componentDirExists) {
3769
+ const files = fs19.readdirSync(path17.join(projectRoot, componentsOverride));
3770
+ framework.componentFileCount = files.filter((f) => f.endsWith(".tsx") || f.endsWith(".jsx")).length;
3771
+ }
3772
+ }
3773
+ if (cssOverride) {
3774
+ framework.cssFiles = fs19.existsSync(path17.join(projectRoot, cssOverride)) ? [cssOverride] : [];
3775
+ }
3499
3776
  const frameworkLabel = framework.name === "nextjs" ? "Next.js" : framework.name === "remix" ? "Remix" : framework.name === "vite" ? "Vite" : "Unknown";
3500
3777
  console.log(` ${green("\u2713")} Framework ${frameworkLabel}`);
3501
3778
  if (framework.appDirExists) {
@@ -3508,12 +3785,28 @@ async function main() {
3508
3785
  ` ${green("\u2713")} Components ${framework.componentDir}/ ${dim(`(${framework.componentFileCount} files)`)}`
3509
3786
  );
3510
3787
  } else {
3511
- console.log(` ${yellow("\u26A0")} Components ${dim("not found \u2014 component editing won't be available")}`);
3788
+ console.log(` ${yellow("\u26A0")} Components ${dim("not found")}`);
3789
+ if (!componentsOverride && process2.stdin.isTTY) {
3790
+ const result = await promptComponentDir(framework, projectRoot);
3791
+ if (result.exists) {
3792
+ framework.componentDir = result.dir;
3793
+ framework.componentDirExists = true;
3794
+ framework.componentFileCount = result.fileCount;
3795
+ console.log(` ${green("\u2713")} Components ${framework.componentDir}/ ${dim(`(${framework.componentFileCount} files)`)}`);
3796
+ }
3797
+ }
3512
3798
  }
3513
3799
  if (framework.cssFiles.length > 0) {
3514
3800
  console.log(` ${green("\u2713")} CSS files ${framework.cssFiles[0]}`);
3515
3801
  } else {
3516
3802
  console.log(` ${yellow("\u26A0")} CSS files ${dim("no CSS files found")}`);
3803
+ if (!cssOverride && process2.stdin.isTTY) {
3804
+ const result = await promptCssFile(framework, projectRoot);
3805
+ if (result.length > 0) {
3806
+ framework.cssFiles = result;
3807
+ console.log(` ${green("\u2713")} CSS files ${framework.cssFiles[0]}`);
3808
+ }
3809
+ }
3517
3810
  }
3518
3811
  const styling = await detectStylingSystem(projectRoot, framework);
3519
3812
  const stylingLabels = {
@@ -3584,7 +3877,9 @@ async function main() {
3584
3877
  targetPort,
3585
3878
  toolPort,
3586
3879
  projectRoot,
3587
- stylingType: styling.type
3880
+ stylingType: styling.type,
3881
+ framework,
3882
+ styling
3588
3883
  });
3589
3884
  const httpServer = await new Promise((resolve, reject) => {
3590
3885
  let attempts = 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@designtools/codesurface",
3
- "version": "0.1.9",
3
+ "version": "0.1.10",
4
4
  "description": "Visual editing CLI — hybrid architecture with selection overlays in the target app and editor UI served separately",
5
5
  "type": "module",
6
6
  "license": "CC-BY-NC-4.0",