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