@oamm/textor 1.0.12 → 1.0.13

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/index.js CHANGED
@@ -4,6 +4,7 @@ import path from 'path';
4
4
  import { createHash } from 'crypto';
5
5
  import { exec } from 'child_process';
6
6
  import { promisify } from 'util';
7
+ import readline from 'readline';
7
8
 
8
9
  const CONFIG_DIR$1 = '.textor';
9
10
  const CONFIG_FILE = 'config.json';
@@ -495,6 +496,8 @@ function normalizeRoute(route) {
495
496
  }
496
497
  function routeToFilePath(route, options = {}) {
497
498
  const { extension = '.astro', mode = 'flat', indexFile = 'index.astro' } = options;
499
+ if (!route)
500
+ return null;
498
501
  const normalized = normalizeRoute(route);
499
502
  if (normalized === '/') {
500
503
  return indexFile;
@@ -713,6 +716,10 @@ async function safeMove(fromPath, toPath, options = {}) {
713
716
  if (!existsSync(fromPath)) {
714
717
  throw new Error(`Source file not found: ${fromPath}`);
715
718
  }
719
+ if (path.resolve(fromPath) === path.resolve(toPath)) {
720
+ const content = await readFile(toPath, 'utf-8');
721
+ return calculateHash(content, normalization);
722
+ }
716
723
  if (existsSync(toPath) && !force) {
717
724
  throw new Error(`Destination already exists: ${toPath}\n` +
718
725
  `Use --force to overwrite.`);
@@ -2348,6 +2355,189 @@ async function updateImportsInFile$1(filePath, oldFilePath, newFilePath) {
2348
2355
  await writeFile(filePath, content, 'utf-8');
2349
2356
  }
2350
2357
 
2358
+ /**
2359
+ * Updates relative imports in a file after it has been moved.
2360
+ */
2361
+ async function updateImportsInFile(filePath, oldFilePath, newFilePath) {
2362
+ if (!existsSync(filePath))
2363
+ return;
2364
+ let content = await readFile(filePath, 'utf-8');
2365
+ const oldDir = path.dirname(oldFilePath);
2366
+ const newDir = path.dirname(newFilePath);
2367
+ if (oldDir === newDir)
2368
+ return;
2369
+ // Find all relative imports
2370
+ const relativeImportRegex = /from\s+['"](\.\.?\/[^'"]+)['"]/g;
2371
+ let match;
2372
+ const replacements = [];
2373
+ while ((match = relativeImportRegex.exec(content)) !== null) {
2374
+ const relativePath = match[1];
2375
+ const absoluteTarget = path.resolve(oldDir, relativePath);
2376
+ const newRelativePath = getRelativeImportPath(newFilePath, absoluteTarget);
2377
+ replacements.push({
2378
+ full: match[0],
2379
+ oldRel: relativePath,
2380
+ newRel: newRelativePath
2381
+ });
2382
+ }
2383
+ for (const repl of replacements) {
2384
+ content = content.replace(repl.full, `from '${repl.newRel}'`);
2385
+ }
2386
+ await writeFile(filePath, content, 'utf-8');
2387
+ }
2388
+ /**
2389
+ * Moves a directory and its contents, renaming files and updating internal content/imports.
2390
+ */
2391
+ async function moveDirectory(fromPath, toPath, state, config, options = {}) {
2392
+ const { fromName, toName, owner = null, signatures = [] } = options;
2393
+ if (!existsSync(fromPath)) {
2394
+ throw new Error(`Source directory not found: ${fromPath}`);
2395
+ }
2396
+ if (existsSync(toPath) && !options.force) {
2397
+ throw new Error(`Destination already exists: ${toPath}\n` +
2398
+ `Use --force to overwrite.`);
2399
+ }
2400
+ await ensureDir(toPath);
2401
+ const entries = await readdir(fromPath);
2402
+ for (const entry of entries) {
2403
+ let targetEntry = entry;
2404
+ // Rename files if they match the component name
2405
+ if (fromName && toName && fromName !== toName) {
2406
+ if (entry.includes(fromName)) {
2407
+ targetEntry = entry.replace(fromName, toName);
2408
+ }
2409
+ }
2410
+ const fromEntryPath = path.join(fromPath, entry);
2411
+ const toEntryPath = path.join(toPath, targetEntry);
2412
+ const stats = await stat(fromEntryPath);
2413
+ if (stats.isDirectory()) {
2414
+ await moveDirectory(fromEntryPath, toEntryPath, state, config, options);
2415
+ }
2416
+ else {
2417
+ const normalizedFromRelative = path.relative(process.cwd(), fromEntryPath).replace(/\\/g, '/');
2418
+ const fileState = state.files[normalizedFromRelative];
2419
+ const newHash = await safeMove(fromEntryPath, toEntryPath, {
2420
+ force: options.force,
2421
+ expectedHash: fileState?.hash,
2422
+ acceptChanges: options.acceptChanges,
2423
+ normalization: config.hashing?.normalization,
2424
+ owner,
2425
+ actualOwner: fileState?.owner,
2426
+ signatures
2427
+ });
2428
+ // Update internal content (signatures, component names) if renaming
2429
+ if (fromName && toName && fromName !== toName) {
2430
+ let content = await readFile(toEntryPath, 'utf-8');
2431
+ let hasChanged = false;
2432
+ // Simple replacement of component names
2433
+ if (content.includes(fromName)) {
2434
+ content = content.replace(new RegExp(fromName, 'g'), toName);
2435
+ hasChanged = true;
2436
+ }
2437
+ // Also handle lowercase class names if any
2438
+ const fromLower = fromName.toLowerCase();
2439
+ const toLower = toName.toLowerCase();
2440
+ if (content.includes(fromLower)) {
2441
+ content = content.replace(new RegExp(fromLower, 'g'), toLower);
2442
+ hasChanged = true;
2443
+ }
2444
+ if (hasChanged) {
2445
+ await writeFile(toEntryPath, content, 'utf-8');
2446
+ // Re-calculate hash after content update
2447
+ const updatedHash = calculateHash(content, config.hashing?.normalization);
2448
+ const normalizedToRelative = path.relative(process.cwd(), toEntryPath).replace(/\\/g, '/');
2449
+ if (fileState) {
2450
+ state.files[normalizedToRelative] = { ...fileState, hash: updatedHash };
2451
+ delete state.files[normalizedFromRelative];
2452
+ }
2453
+ }
2454
+ else {
2455
+ // Update state for each file moved normally
2456
+ const normalizedToRelative = path.relative(process.cwd(), toEntryPath).replace(/\\/g, '/');
2457
+ if (fileState) {
2458
+ state.files[normalizedToRelative] = { ...fileState, hash: newHash };
2459
+ delete state.files[normalizedFromRelative];
2460
+ }
2461
+ }
2462
+ }
2463
+ else {
2464
+ // Update state for each file moved normally
2465
+ const normalizedToRelative = path.relative(process.cwd(), toEntryPath).replace(/\\/g, '/');
2466
+ if (fileState) {
2467
+ state.files[normalizedToRelative] = { ...fileState, hash: newHash };
2468
+ delete state.files[normalizedFromRelative];
2469
+ }
2470
+ }
2471
+ }
2472
+ }
2473
+ const remainingFiles = await readdir(fromPath);
2474
+ if (remainingFiles.length === 0) {
2475
+ await rmdir(fromPath);
2476
+ }
2477
+ }
2478
+ /**
2479
+ * Scans the project and replaces imports of a moved/renamed item.
2480
+ */
2481
+ async function scanAndReplaceImports(config, state, fromInfo, toInfo, options) {
2482
+ const { fromPath: fromItemPath, fromName, type } = fromInfo;
2483
+ const { toPath: toItemPath, toName } = toInfo;
2484
+ const allFiles = new Set();
2485
+ await scanDirectory(process.cwd(), allFiles);
2486
+ const rootPath = resolvePath(config, type === 'component' ? 'components' : 'features');
2487
+ for (const relPath of allFiles) {
2488
+ const fullPath = path.resolve(process.cwd(), relPath);
2489
+ // Skip the moved directory itself
2490
+ const toFullPath = path.resolve(toItemPath);
2491
+ if (fullPath.startsWith(toFullPath))
2492
+ continue;
2493
+ let content = await readFile(fullPath, 'utf-8');
2494
+ let changed = false;
2495
+ const aliasBase = config.importAliases[type === 'component' ? 'components' : 'features'];
2496
+ const ext = type === 'component' ? '' : (config.naming.featureExtension === '.astro' ? '.astro' : '');
2497
+ if (aliasBase) {
2498
+ const oldAlias = `${aliasBase}/${fromItemPath}`;
2499
+ const newAlias = `${aliasBase}/${toItemPath}`;
2500
+ const oldFullImport = `from '${oldAlias}/${fromName}${ext}'`;
2501
+ const newFullImport = `from '${newAlias}/${toName}${ext}'`;
2502
+ if (content.includes(oldFullImport)) {
2503
+ content = content.replace(new RegExp(oldFullImport.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newFullImport);
2504
+ changed = true;
2505
+ }
2506
+ else if (content.includes(oldAlias)) {
2507
+ content = content.replace(new RegExp(oldAlias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newAlias);
2508
+ changed = true;
2509
+ }
2510
+ }
2511
+ else {
2512
+ const oldDir = path.resolve(rootPath, fromItemPath);
2513
+ const newDir = path.resolve(rootPath, toItemPath);
2514
+ const oldRelPath = getRelativeImportPath(fullPath, oldDir);
2515
+ const newRelPath = getRelativeImportPath(fullPath, newDir);
2516
+ const oldImport = `'${oldRelPath}/${fromName}${ext}'`;
2517
+ const newImport = `'${newRelPath}/${toName}${ext}'`;
2518
+ if (content.includes(oldImport)) {
2519
+ content = content.replace(new RegExp(oldImport.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newImport);
2520
+ changed = true;
2521
+ }
2522
+ }
2523
+ if (fromName !== toName && changed) {
2524
+ content = content.replace(new RegExp(`\\b${fromName}\\b`, 'g'), toName);
2525
+ }
2526
+ if (changed) {
2527
+ if (options.dryRun) {
2528
+ console.log(` [Scan] Would update imports in ${relPath}`);
2529
+ }
2530
+ else {
2531
+ await writeFile(fullPath, content, 'utf-8');
2532
+ console.log(` [Scan] Updated imports in ${relPath}`);
2533
+ if (state.files[relPath]) {
2534
+ state.files[relPath].hash = calculateHash(content, config.hashing?.normalization);
2535
+ }
2536
+ }
2537
+ }
2538
+ }
2539
+ }
2540
+
2351
2541
  /**
2352
2542
  * Move a section (route + feature).
2353
2543
  *
@@ -2372,40 +2562,53 @@ async function moveSectionCommand(fromRoute, fromFeature, toRoute, toFeature, op
2372
2562
  let actualFromFeature = fromFeature;
2373
2563
  let actualToRoute = toRoute;
2374
2564
  let actualToFeature = toFeature;
2375
- // Shift arguments if using state
2565
+ // Shift arguments if using state or if called with fewer arguments
2376
2566
  if (!toRoute && fromRoute && fromFeature) {
2377
2567
  // textor move-section /old-route /new-route
2378
- const section = findSection(state, fromRoute);
2568
+ actualFromRoute = fromRoute;
2569
+ actualToRoute = fromFeature;
2570
+ actualFromFeature = undefined;
2571
+ actualToFeature = undefined;
2572
+ }
2573
+ // Lookup missing info from state
2574
+ if (actualFromRoute && !actualFromFeature) {
2575
+ const section = findSection(state, actualFromRoute);
2379
2576
  if (section) {
2380
- actualFromRoute = section.route;
2381
2577
  actualFromFeature = section.featurePath;
2382
- actualToRoute = fromFeature; // the second argument was actually the new route
2383
- actualToFeature = toRoute; // which is null
2384
- // If toFeature is not provided, try to derive it from the new route
2385
- if (!actualToFeature && actualToRoute) {
2386
- const oldRouteParts = actualFromRoute.split('/').filter(Boolean);
2387
- const newRouteParts = actualToRoute.split('/').filter(Boolean);
2388
- const oldFeatureParts = actualFromFeature.split('/').filter(Boolean);
2389
- // If the feature path starts with the old route parts, replace them
2390
- // We compare case-insensitively or via PascalCase to be more helpful
2391
- let match = true;
2392
- for (let i = 0; i < oldRouteParts.length; i++) {
2393
- const routePart = oldRouteParts[i].toLowerCase();
2394
- const featurePart = oldFeatureParts[i] ? oldFeatureParts[i].toLowerCase() : null;
2395
- if (featurePart !== routePart) {
2396
- match = false;
2397
- break;
2398
- }
2399
- }
2400
- if (match && oldRouteParts.length > 0) {
2401
- actualToFeature = [...newRouteParts, ...oldFeatureParts.slice(oldRouteParts.length)].join('/');
2402
- }
2403
- else {
2404
- // Otherwise just keep it the same
2405
- actualToFeature = actualFromFeature;
2406
- }
2578
+ }
2579
+ }
2580
+ else if (!actualFromRoute && actualFromFeature) {
2581
+ const section = findSection(state, actualFromFeature);
2582
+ if (section) {
2583
+ actualFromRoute = section.route;
2584
+ }
2585
+ }
2586
+ // If toFeature is not provided, try to derive it from the new route if route moved
2587
+ if (!actualToFeature && actualToRoute && actualFromRoute && actualFromRoute !== actualToRoute && actualFromFeature) {
2588
+ const oldRouteParts = actualFromRoute.split('/').filter(Boolean);
2589
+ const newRouteParts = actualToRoute.split('/').filter(Boolean);
2590
+ const oldFeatureParts = actualFromFeature.split('/').filter(Boolean);
2591
+ let match = true;
2592
+ for (let i = 0; i < oldRouteParts.length; i++) {
2593
+ const routePart = oldRouteParts[i].toLowerCase();
2594
+ const featurePart = oldFeatureParts[i] ? oldFeatureParts[i].toLowerCase() : null;
2595
+ if (featurePart !== routePart) {
2596
+ match = false;
2597
+ break;
2407
2598
  }
2408
2599
  }
2600
+ if (match && oldRouteParts.length > 0) {
2601
+ actualToFeature = [...newRouteParts, ...oldFeatureParts.slice(oldRouteParts.length)].join('/');
2602
+ }
2603
+ else {
2604
+ actualToFeature = actualFromFeature;
2605
+ }
2606
+ }
2607
+ else if (!actualToFeature) {
2608
+ actualToFeature = actualFromFeature;
2609
+ }
2610
+ if (!actualToRoute) {
2611
+ actualToRoute = actualFromRoute;
2409
2612
  }
2410
2613
  const isRouteOnly = options.keepFeature || (!actualToFeature && actualToRoute && !actualFromFeature);
2411
2614
  if (isRouteOnly && !actualToRoute) {
@@ -2430,12 +2633,14 @@ async function moveSectionCommand(fromRoute, fromFeature, toRoute, toFeature, op
2430
2633
  mode: config.routing.mode,
2431
2634
  indexFile: config.routing.indexFile
2432
2635
  });
2433
- const fromRoutePath = secureJoin(pagesRoot, fromRouteFile);
2434
- const toRoutePath = secureJoin(pagesRoot, toRouteFile);
2636
+ const fromRoutePath = fromRouteFile ? secureJoin(pagesRoot, fromRouteFile) : null;
2637
+ const toRoutePath = toRouteFile ? secureJoin(pagesRoot, toRouteFile) : null;
2435
2638
  const movedFiles = [];
2436
2639
  if (options.dryRun) {
2437
2640
  console.log('Dry run - would move:');
2438
- console.log(` Route: ${fromRoutePath} -> ${toRoutePath}`);
2641
+ if (fromRoutePath && toRoutePath && fromRoutePath !== toRoutePath) {
2642
+ console.log(` Route: ${fromRoutePath} -> ${toRoutePath}`);
2643
+ }
2439
2644
  if (!isRouteOnly && normalizedFromFeature && normalizedToFeature) {
2440
2645
  const fromFeaturePath = secureJoin(featuresRoot, normalizedFromFeature);
2441
2646
  const toFeaturePath = secureJoin(featuresRoot, normalizedToFeature);
@@ -2443,81 +2648,90 @@ async function moveSectionCommand(fromRoute, fromFeature, toRoute, toFeature, op
2443
2648
  }
2444
2649
  return;
2445
2650
  }
2446
- const normalizedFromRouteRelative = path.relative(process.cwd(), fromRoutePath).replace(/\\/g, '/');
2447
- const routeFileState = state.files[normalizedFromRouteRelative];
2448
- const newRouteHash = await safeMove(fromRoutePath, toRoutePath, {
2449
- force: options.force,
2450
- expectedHash: routeFileState?.hash,
2451
- acceptChanges: options.acceptChanges,
2452
- owner: normalizedFromRoute,
2453
- actualOwner: routeFileState?.owner,
2454
- signatures: configSignatures
2455
- });
2456
- movedFiles.push({ from: fromRoutePath, to: toRoutePath });
2457
- // Update state for moved route file
2458
- const normalizedToRouteRelative = path.relative(process.cwd(), toRoutePath).replace(/\\/g, '/');
2459
- if (routeFileState) {
2460
- state.files[normalizedToRouteRelative] = { ...routeFileState, hash: newRouteHash };
2461
- delete state.files[normalizedFromRouteRelative];
2462
- }
2463
- // Update imports in the moved route file
2464
- const targetFeature = normalizedToFeature || normalizedFromFeature;
2465
- if (targetFeature) {
2466
- const fromFeatureDirPath = secureJoin(featuresRoot, normalizedFromFeature);
2467
- const toFeatureDirPath = secureJoin(featuresRoot, targetFeature);
2468
- const fromFeatureComponentName = getFeatureComponentName(normalizedFromFeature);
2469
- const toFeatureComponentName = getFeatureComponentName(targetFeature);
2470
- // First, update all relative imports in the file because it moved
2471
- await updateImportsInFile(toRoutePath, fromRoutePath, toRoutePath);
2472
- let content = await readFile(toRoutePath, 'utf-8');
2473
- let changed = false;
2474
- // Update component name in JSX tags
2475
- if (fromFeatureComponentName !== toFeatureComponentName) {
2476
- content = content.replace(new RegExp(`<${fromFeatureComponentName}`, 'g'), `<${toFeatureComponentName}`);
2477
- content = content.replace(new RegExp(`</${fromFeatureComponentName}`, 'g'), `</${toFeatureComponentName}`);
2478
- changed = true;
2651
+ let normalizedToRouteRelative = null;
2652
+ if (fromRoutePath && toRoutePath) {
2653
+ const normalizedFromRouteRelative = path.relative(process.cwd(), fromRoutePath).replace(/\\/g, '/');
2654
+ const routeFileState = state.files[normalizedFromRouteRelative];
2655
+ const newRouteHash = await safeMove(fromRoutePath, toRoutePath, {
2656
+ force: options.force,
2657
+ expectedHash: routeFileState?.hash,
2658
+ acceptChanges: options.acceptChanges,
2659
+ owner: normalizedFromRoute,
2660
+ actualOwner: routeFileState?.owner,
2661
+ signatures: configSignatures
2662
+ });
2663
+ if (fromRoutePath !== toRoutePath) {
2664
+ movedFiles.push({ from: fromRoutePath, to: toRoutePath });
2665
+ }
2666
+ // Update state for moved route file
2667
+ normalizedToRouteRelative = path.relative(process.cwd(), toRoutePath).replace(/\\/g, '/');
2668
+ if (routeFileState) {
2669
+ state.files[normalizedToRouteRelative] = { ...routeFileState, hash: newRouteHash };
2670
+ if (fromRoutePath !== toRoutePath) {
2671
+ delete state.files[normalizedFromRouteRelative];
2672
+ }
2479
2673
  }
2480
- if (config.importAliases.features) {
2481
- const oldAliasPath = `${config.importAliases.features}/${normalizedFromFeature}`;
2482
- const newAliasPath = `${config.importAliases.features}/${targetFeature}`;
2483
- // Flexible regex to match import identifier and path with alias
2484
- const importRegex = new RegExp(`(import\\s+)(${fromFeatureComponentName})(\\s+from\\s+['"])${oldAliasPath}(/[^'"]+)?(['"])`, 'g');
2485
- if (importRegex.test(content)) {
2486
- content = content.replace(importRegex, (match, p1, p2, p3, subPath, p5) => {
2487
- let newSubPath = subPath || '';
2488
- if (subPath && subPath.includes(fromFeatureComponentName)) {
2489
- newSubPath = subPath.replace(fromFeatureComponentName, toFeatureComponentName);
2490
- }
2491
- return `${p1}${toFeatureComponentName}${p3}${newAliasPath}${newSubPath}${p5}`;
2492
- });
2674
+ // Update imports in the route file (even if it didn't move, as feature might have)
2675
+ const targetFeature = normalizedToFeature || normalizedFromFeature;
2676
+ if (targetFeature && existsSync(toRoutePath)) {
2677
+ const fromFeatureDirPath = secureJoin(featuresRoot, normalizedFromFeature);
2678
+ const toFeatureDirPath = secureJoin(featuresRoot, targetFeature);
2679
+ const fromFeatureComponentName = getFeatureComponentName(normalizedFromFeature);
2680
+ const toFeatureComponentName = getFeatureComponentName(targetFeature);
2681
+ // First, update all relative imports in the file because it moved (or stayed)
2682
+ await updateImportsInFile(toRoutePath, fromRoutePath, toRoutePath);
2683
+ let content = await readFile(toRoutePath, 'utf-8');
2684
+ let changed = false;
2685
+ // Update component name in JSX tags
2686
+ if (fromFeatureComponentName !== toFeatureComponentName) {
2687
+ content = content.replace(new RegExp(`<${fromFeatureComponentName}`, 'g'), `<${toFeatureComponentName}`);
2688
+ content = content.replace(new RegExp(`</${fromFeatureComponentName}`, 'g'), `</${toFeatureComponentName}`);
2493
2689
  changed = true;
2494
2690
  }
2495
- else if (content.includes(oldAliasPath)) {
2496
- // Fallback for path only replacement
2497
- content = content.replace(new RegExp(oldAliasPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newAliasPath);
2498
- changed = true;
2691
+ if (config.importAliases.features) {
2692
+ const oldAliasPath = `${config.importAliases.features}/${normalizedFromFeature}`;
2693
+ const newAliasPath = `${config.importAliases.features}/${targetFeature}`;
2694
+ // Flexible regex to match import identifier and path with alias
2695
+ const importRegex = new RegExp(`(import\\s+)(${fromFeatureComponentName})(\\s+from\\s+['"])${oldAliasPath}(/[^'"]+)?(['"])`, 'g');
2696
+ if (importRegex.test(content)) {
2697
+ content = content.replace(importRegex, (match, p1, p2, p3, subPath, p5) => {
2698
+ let newSubPath = subPath || '';
2699
+ if (subPath && subPath.includes(fromFeatureComponentName)) {
2700
+ newSubPath = subPath.replace(fromFeatureComponentName, toFeatureComponentName);
2701
+ }
2702
+ return `${p1}${toFeatureComponentName}${p3}${newAliasPath}${newSubPath}${p5}`;
2703
+ });
2704
+ changed = true;
2705
+ }
2706
+ else if (content.includes(oldAliasPath)) {
2707
+ // Fallback for path only replacement
2708
+ content = content.replace(new RegExp(oldAliasPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newAliasPath);
2709
+ changed = true;
2710
+ }
2499
2711
  }
2500
- }
2501
- else {
2502
- const oldRelativeDir = getRelativeImportPath(toRoutePath, fromFeatureDirPath);
2503
- const newRelativeDir = getRelativeImportPath(toRoutePath, toFeatureDirPath);
2504
- // Flexible regex for relative imports
2505
- const relImportRegex = new RegExp(`(import\\s+)(${fromFeatureComponentName})(\\s+from\\s+['"])${oldRelativeDir}(/[^'"]+)?(['"])`, 'g');
2506
- if (relImportRegex.test(content)) {
2507
- content = content.replace(relImportRegex, (match, p1, p2, p3, subPath, p5) => {
2508
- let newSubPath = subPath || '';
2509
- if (subPath && subPath.includes(fromFeatureComponentName)) {
2510
- newSubPath = subPath.replace(fromFeatureComponentName, toFeatureComponentName);
2511
- }
2512
- return `${p1}${toFeatureComponentName}${p3}${newRelativeDir}${newSubPath}${p5}`;
2513
- });
2514
- changed = true;
2712
+ else {
2713
+ const oldRelativeDir = getRelativeImportPath(toRoutePath, fromFeatureDirPath);
2714
+ const newRelativeDir = getRelativeImportPath(toRoutePath, toFeatureDirPath);
2715
+ // Flexible regex for relative imports
2716
+ const relImportRegex = new RegExp(`(import\\s+)(${fromFeatureComponentName})(\\s+from\\s+['"])${oldRelativeDir}(/[^'"]+)?(['"])`, 'g');
2717
+ if (relImportRegex.test(content)) {
2718
+ content = content.replace(relImportRegex, (match, p1, p2, p3, subPath, p5) => {
2719
+ let newSubPath = subPath || '';
2720
+ if (subPath && subPath.includes(fromFeatureComponentName)) {
2721
+ newSubPath = subPath.replace(fromFeatureComponentName, toFeatureComponentName);
2722
+ }
2723
+ return `${p1}${toFeatureComponentName}${p3}${newRelativeDir}${newSubPath}${p5}`;
2724
+ });
2725
+ changed = true;
2726
+ }
2727
+ }
2728
+ if (changed) {
2729
+ await writeFile(toRoutePath, content, 'utf-8');
2730
+ // Update hash in state after changes
2731
+ if (state.files[normalizedToRouteRelative]) {
2732
+ state.files[normalizedToRouteRelative].hash = calculateHash(content, config.hashing?.normalization);
2733
+ }
2515
2734
  }
2516
- }
2517
- if (changed) {
2518
- await writeFile(toRoutePath, content, 'utf-8');
2519
- // Update hash in state after changes
2520
- state.files[normalizedToRouteRelative].hash = calculateHash(content, config.hashing?.normalization);
2521
2735
  }
2522
2736
  }
2523
2737
  if (!isRouteOnly && normalizedFromFeature && normalizedToFeature && normalizedFromFeature !== normalizedToFeature) {
@@ -2539,14 +2753,17 @@ async function moveSectionCommand(fromRoute, fromFeature, toRoute, toFeature, op
2539
2753
  }
2540
2754
  if (options.scan && (normalizedFromFeature || normalizedToFeature)) {
2541
2755
  await scanAndReplaceImports(config, state, {
2542
- fromFeaturePath: normalizedFromFeature,
2543
- fromComponentName: getFeatureComponentName(normalizedFromFeature)
2756
+ fromPath: normalizedFromFeature,
2757
+ fromName: getFeatureComponentName(normalizedFromFeature),
2758
+ type: 'feature'
2544
2759
  }, {
2545
- toFeaturePath: normalizedToFeature || normalizedFromFeature,
2546
- toComponentName: getFeatureComponentName(normalizedToFeature || normalizedFromFeature)
2760
+ toPath: normalizedToFeature || normalizedFromFeature,
2761
+ toName: getFeatureComponentName(normalizedToFeature || normalizedFromFeature)
2547
2762
  }, options);
2548
2763
  }
2549
- await cleanupEmptyDirs(path.dirname(fromRoutePath), pagesRoot);
2764
+ if (fromRoutePath && toRoutePath && fromRoutePath !== toRoutePath) {
2765
+ await cleanupEmptyDirs(path.dirname(fromRoutePath), pagesRoot);
2766
+ }
2550
2767
  console.log('✓ Moved:');
2551
2768
  movedFiles.forEach(item => {
2552
2769
  console.log(` ${item.from}`);
@@ -2554,6 +2771,14 @@ async function moveSectionCommand(fromRoute, fromFeature, toRoute, toFeature, op
2554
2771
  });
2555
2772
  if (movedFiles.length > 0) {
2556
2773
  const existingSection = fromSection;
2774
+ // Update ownership in state if route moved
2775
+ if (normalizedFromRoute && normalizedToRoute && normalizedFromRoute !== normalizedToRoute) {
2776
+ for (const f in state.files) {
2777
+ if (state.files[f].owner === normalizedFromRoute) {
2778
+ state.files[f].owner = normalizedToRoute;
2779
+ }
2780
+ }
2781
+ }
2557
2782
  // Update section data in state
2558
2783
  state.sections = state.sections.filter(s => s.route !== normalizedFromRoute);
2559
2784
  state.sections.push({
@@ -2574,182 +2799,6 @@ async function moveSectionCommand(fromRoute, fromFeature, toRoute, toFeature, op
2574
2799
  throw error;
2575
2800
  }
2576
2801
  }
2577
- async function scanAndReplaceImports(config, state, fromInfo, toInfo, options) {
2578
- const { fromFeaturePath, fromComponentName } = fromInfo;
2579
- const { toFeaturePath, toComponentName } = toInfo;
2580
- const allFiles = new Set();
2581
- await scanDirectory(process.cwd(), allFiles);
2582
- const featuresRoot = resolvePath(config, 'features');
2583
- for (const relPath of allFiles) {
2584
- const fullPath = path.join(process.cwd(), relPath);
2585
- // Skip the moved directory itself as it was already handled
2586
- if (fullPath.startsWith(path.resolve(toFeaturePath)))
2587
- continue;
2588
- let content = await readFile(fullPath, 'utf-8');
2589
- let changed = false;
2590
- const ext = config.naming.featureExtension === '.astro' ? '.astro' : '';
2591
- // Handle Aliases
2592
- if (config.importAliases.features) {
2593
- const oldAlias = `${config.importAliases.features}/${fromFeaturePath}`;
2594
- const newAlias = `${config.importAliases.features}/${toFeaturePath}`;
2595
- // Update component name and path if both changed
2596
- const oldFullImport = `from '${oldAlias}/${fromComponentName}${ext}'`;
2597
- const newFullImport = `from '${newAlias}/${toComponentName}${ext}'`;
2598
- if (content.includes(oldFullImport)) {
2599
- content = content.replace(new RegExp(oldFullImport.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newFullImport);
2600
- changed = true;
2601
- }
2602
- else if (content.includes(oldAlias)) {
2603
- content = content.replace(new RegExp(oldAlias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newAlias);
2604
- changed = true;
2605
- }
2606
- }
2607
- else {
2608
- // Handle Relative Imports (more complex)
2609
- // This is best-effort: we look for imports that resolve to the old feature path
2610
- const fromFeatureDir = secureJoin(featuresRoot, fromFeaturePath);
2611
- const toFeatureDir = secureJoin(featuresRoot, toFeaturePath);
2612
- const oldRelPath = getRelativeImportPath(fullPath, fromFeatureDir);
2613
- const newRelPath = getRelativeImportPath(fullPath, toFeatureDir);
2614
- const oldImport = `'${oldRelPath}/${fromComponentName}${ext}'`;
2615
- const newImport = `'${newRelPath}/${toComponentName}${ext}'`;
2616
- if (content.includes(oldImport)) {
2617
- content = content.replace(new RegExp(oldImport.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newImport);
2618
- changed = true;
2619
- }
2620
- }
2621
- // Update component name in JSX and imports if it changed
2622
- if (fromComponentName !== toComponentName && changed) {
2623
- content = content.replace(new RegExp(`\\b${fromComponentName}\\b`, 'g'), toComponentName);
2624
- }
2625
- if (changed) {
2626
- if (options.dryRun) {
2627
- console.log(` [Scan] Would update imports in ${relPath}`);
2628
- }
2629
- else {
2630
- await writeFile(fullPath, content, 'utf-8');
2631
- console.log(` [Scan] Updated imports in ${relPath}`);
2632
- // Update state hash if this file is managed
2633
- if (state.files[relPath]) {
2634
- state.files[relPath].hash = calculateHash(content, config.hashing?.normalization);
2635
- }
2636
- }
2637
- }
2638
- }
2639
- }
2640
- async function moveDirectory(fromPath, toPath, state, config, options = {}) {
2641
- const { fromName, toName, owner = null } = options;
2642
- if (!existsSync(fromPath)) {
2643
- throw new Error(`Source directory not found: ${fromPath}`);
2644
- }
2645
- if (existsSync(toPath) && !options.force) {
2646
- throw new Error(`Destination already exists: ${toPath}\n` +
2647
- `Use --force to overwrite.`);
2648
- }
2649
- await ensureDir(toPath);
2650
- const entries = await readdir(fromPath);
2651
- for (const entry of entries) {
2652
- let targetEntry = entry;
2653
- // Rename files if they match the component name
2654
- if (fromName && toName && fromName !== toName) {
2655
- if (entry.includes(fromName)) {
2656
- targetEntry = entry.replace(fromName, toName);
2657
- }
2658
- }
2659
- const fromEntryPath = path.join(fromPath, entry);
2660
- const toEntryPath = path.join(toPath, targetEntry);
2661
- const stats = await stat(fromEntryPath);
2662
- if (stats.isDirectory()) {
2663
- await moveDirectory(fromEntryPath, toEntryPath, state, config, options);
2664
- }
2665
- else {
2666
- const normalizedFromRelative = path.relative(process.cwd(), fromEntryPath).replace(/\\/g, '/');
2667
- const fileState = state.files[normalizedFromRelative];
2668
- const newHash = await safeMove(fromEntryPath, toEntryPath, {
2669
- force: options.force,
2670
- expectedHash: fileState?.hash,
2671
- acceptChanges: options.acceptChanges,
2672
- normalization: config.hashing?.normalization,
2673
- owner,
2674
- actualOwner: fileState?.owner
2675
- });
2676
- // Update internal content (signatures, component names) if renaming
2677
- if (fromName && toName && fromName !== toName) {
2678
- let content = await readFile(toEntryPath, 'utf-8');
2679
- let hasChanged = false;
2680
- // Simple replacement of component names
2681
- if (content.includes(fromName)) {
2682
- content = content.replace(new RegExp(fromName, 'g'), toName);
2683
- hasChanged = true;
2684
- }
2685
- // Also handle lowercase class names if any
2686
- const fromLower = fromName.toLowerCase();
2687
- const toLower = toName.toLowerCase();
2688
- if (content.includes(fromLower)) {
2689
- content = content.replace(new RegExp(fromLower, 'g'), toLower);
2690
- hasChanged = true;
2691
- }
2692
- if (hasChanged) {
2693
- await writeFile(toEntryPath, content, 'utf-8');
2694
- // Re-calculate hash after content update
2695
- const updatedHash = calculateHash(content, config.hashing?.normalization);
2696
- const normalizedToRelative = path.relative(process.cwd(), toEntryPath).replace(/\\/g, '/');
2697
- if (fileState) {
2698
- state.files[normalizedToRelative] = { ...fileState, hash: updatedHash };
2699
- delete state.files[normalizedFromRelative];
2700
- }
2701
- }
2702
- else {
2703
- // Update state for each file moved normally
2704
- const normalizedToRelative = path.relative(process.cwd(), toEntryPath).replace(/\\/g, '/');
2705
- if (fileState) {
2706
- state.files[normalizedToRelative] = { ...fileState, hash: newHash };
2707
- delete state.files[normalizedFromRelative];
2708
- }
2709
- }
2710
- }
2711
- else {
2712
- // Update state for each file moved normally
2713
- const normalizedToRelative = path.relative(process.cwd(), toEntryPath).replace(/\\/g, '/');
2714
- if (fileState) {
2715
- state.files[normalizedToRelative] = { ...fileState, hash: newHash };
2716
- delete state.files[normalizedFromRelative];
2717
- }
2718
- }
2719
- }
2720
- }
2721
- const remainingFiles = await readdir(fromPath);
2722
- if (remainingFiles.length === 0) {
2723
- await rmdir(fromPath);
2724
- }
2725
- }
2726
- async function updateImportsInFile(filePath, oldFilePath, newFilePath) {
2727
- if (!existsSync(filePath))
2728
- return;
2729
- let content = await readFile(filePath, 'utf-8');
2730
- const oldDir = path.dirname(oldFilePath);
2731
- const newDir = path.dirname(newFilePath);
2732
- if (oldDir === newDir)
2733
- return;
2734
- // Find all relative imports
2735
- const relativeImportRegex = /from\s+['"](\.\.?\/[^'"]+)['"]/g;
2736
- let match;
2737
- const replacements = [];
2738
- while ((match = relativeImportRegex.exec(content)) !== null) {
2739
- const relativePath = match[1];
2740
- const absoluteTarget = path.resolve(oldDir, relativePath);
2741
- const newRelativePath = getRelativeImportPath(newFilePath, absoluteTarget);
2742
- replacements.push({
2743
- full: match[0],
2744
- oldRel: relativePath,
2745
- newRel: newRelativePath
2746
- });
2747
- }
2748
- for (const repl of replacements) {
2749
- content = content.replace(repl.full, `from '${repl.newRel}'`);
2750
- }
2751
- await writeFile(filePath, content, 'utf-8');
2752
- }
2753
2802
 
2754
2803
  async function createComponentCommand(componentName, options) {
2755
2804
  try {
@@ -3792,5 +3841,149 @@ async function normalizeStateCommand(options) {
3792
3841
  }
3793
3842
  }
3794
3843
 
3795
- export { addSectionCommand, adoptCommand, createComponentCommand, initCommand, listSectionsCommand, moveSectionCommand, normalizeStateCommand, removeComponentCommand, removeSectionCommand, statusCommand, syncCommand, upgradeConfigCommand, validateStateCommand };
3844
+ /**
3845
+ * Removes missing references from Textor state.
3846
+ * @param {Object} options
3847
+ * @param {boolean} options.dryRun
3848
+ * @param {boolean} options.yes
3849
+ */
3850
+ async function pruneMissingCommand(options = {}) {
3851
+ try {
3852
+ const config = await loadConfig();
3853
+ const state = await loadState();
3854
+ const results = await getProjectStatus(config, state);
3855
+ if (results.missing.length === 0) {
3856
+ console.log('No missing references found.');
3857
+ return;
3858
+ }
3859
+ console.log(`Found ${results.missing.length} missing references:`);
3860
+ results.missing.forEach(f => console.log(` - ${f}`));
3861
+ if (options.dryRun) {
3862
+ console.log('\nDry run: no changes applied to state.');
3863
+ return;
3864
+ }
3865
+ if (!options.yes && options.interactive !== false && process.stdin.isTTY && process.env.NODE_ENV !== 'test') {
3866
+ const rl = readline.createInterface({
3867
+ input: process.stdin,
3868
+ output: process.stdout
3869
+ });
3870
+ const confirmed = await new Promise(resolve => {
3871
+ rl.question('\nDo you want to proceed with pruning? (y/N) ', (answer) => {
3872
+ rl.close();
3873
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
3874
+ });
3875
+ });
3876
+ if (!confirmed) {
3877
+ console.log('Aborted.');
3878
+ return;
3879
+ }
3880
+ }
3881
+ for (const relPath of results.missing) {
3882
+ delete state.files[relPath];
3883
+ }
3884
+ // Reconstruct metadata
3885
+ state.components = reconstructComponents(state.files, config);
3886
+ state.sections = reconstructSections(state, config);
3887
+ await saveState(state);
3888
+ console.log(`\n✓ Successfully removed ${results.missing.length} missing references from state.`);
3889
+ }
3890
+ catch (error) {
3891
+ console.error('Error:', error.message);
3892
+ if (typeof process.exit === 'function' && process.env.NODE_ENV !== 'test') {
3893
+ process.exit(1);
3894
+ }
3895
+ throw error;
3896
+ }
3897
+ }
3898
+
3899
+ /**
3900
+ * Dispatcher for rename commands.
3901
+ */
3902
+ async function renameCommand(type, oldName, newName, options) {
3903
+ try {
3904
+ if (!type || !oldName || !newName) {
3905
+ throw new Error('Usage: textor rename <route|feature|component> <oldName> <newName>');
3906
+ }
3907
+ if (type === 'route' || type === 'path') {
3908
+ const normalizedOld = normalizeRoute(oldName);
3909
+ const normalizedNew = normalizeRoute(newName);
3910
+ // By default, move-section will try to move the feature if it matches the route.
3911
+ // For a simple "rename route", we might want to keep that behavior or not.
3912
+ // Usually "rename route" means just the URL/file.
3913
+ return await moveSectionCommand(normalizedOld, undefined, normalizedNew, undefined, options);
3914
+ }
3915
+ if (type === 'feature') {
3916
+ const state = await loadState();
3917
+ const normalizedOld = featureToDirectoryPath(oldName);
3918
+ const normalizedNew = featureToDirectoryPath(newName);
3919
+ const section = findSection(state, normalizedOld);
3920
+ if (section) {
3921
+ // If it's a managed section, move it using section logic
3922
+ return await moveSectionCommand(section.route, section.featurePath, section.route, normalizedNew, options);
3923
+ }
3924
+ else {
3925
+ // Standalone feature move
3926
+ return await moveSectionCommand(undefined, normalizedOld, undefined, normalizedNew, options);
3927
+ }
3928
+ }
3929
+ if (type === 'component') {
3930
+ return await renameComponent(oldName, newName, options);
3931
+ }
3932
+ throw new Error(`Unknown rename type: ${type}. Supported types: route, feature, component.`);
3933
+ }
3934
+ catch (error) {
3935
+ console.error('Error:', error.message);
3936
+ if (typeof process.exit === 'function' && process.env.NODE_ENV !== 'test') {
3937
+ process.exit(1);
3938
+ }
3939
+ throw error;
3940
+ }
3941
+ }
3942
+ /**
3943
+ * Specialized logic for renaming shared components.
3944
+ */
3945
+ async function renameComponent(oldName, newName, options) {
3946
+ const config = await loadConfig();
3947
+ const state = await loadState();
3948
+ const normalizedOldName = normalizeComponentName(oldName);
3949
+ const normalizedNewName = normalizeComponentName(newName);
3950
+ const component = findComponent(state, normalizedOldName);
3951
+ const componentsRoot = resolvePath(config, 'components');
3952
+ const fromPath = component
3953
+ ? path.resolve(process.cwd(), component.path)
3954
+ : path.join(componentsRoot, normalizedOldName);
3955
+ const toPath = path.join(componentsRoot, normalizedNewName);
3956
+ if (options.dryRun) {
3957
+ console.log(`Dry run - would rename component: ${normalizedOldName} -> ${normalizedNewName}`);
3958
+ console.log(` Path: ${fromPath} -> ${toPath}`);
3959
+ return;
3960
+ }
3961
+ const signatures = Object.values(config.signatures || {});
3962
+ await moveDirectory(fromPath, toPath, state, config, {
3963
+ ...options,
3964
+ fromName: normalizedOldName,
3965
+ toName: normalizedNewName,
3966
+ signatures
3967
+ });
3968
+ if (options.scan) {
3969
+ await scanAndReplaceImports(config, state, {
3970
+ fromPath: normalizedOldName,
3971
+ fromName: normalizedOldName,
3972
+ type: 'component'
3973
+ }, {
3974
+ toPath: normalizedNewName,
3975
+ toName: normalizedNewName
3976
+ }, options);
3977
+ }
3978
+ await cleanupEmptyDirs(path.dirname(fromPath), componentsRoot);
3979
+ // Update state metadata
3980
+ if (component) {
3981
+ component.name = normalizedNewName;
3982
+ component.path = path.relative(process.cwd(), toPath).replace(/\\/g, '/');
3983
+ }
3984
+ await saveState(state);
3985
+ console.log(`✓ Renamed component ${normalizedOldName} to ${normalizedNewName}`);
3986
+ }
3987
+
3988
+ export { addSectionCommand, adoptCommand, createComponentCommand, initCommand, listSectionsCommand, moveSectionCommand, normalizeStateCommand, pruneMissingCommand, removeComponentCommand, removeSectionCommand, renameCommand, statusCommand, syncCommand, upgradeConfigCommand, validateStateCommand };
3796
3989
  //# sourceMappingURL=index.js.map