@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/README.md +16 -0
- package/dist/bin/textor.js +516 -338
- package/dist/bin/textor.js.map +1 -1
- package/dist/index.cjs +499 -287
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.js +498 -288
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
|
|
2389
|
-
|
|
2390
|
-
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
|
|
2449
|
-
|
|
2450
|
-
|
|
2451
|
-
|
|
2452
|
-
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
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
|
|
2478
|
-
|
|
2479
|
-
|
|
2480
|
-
|
|
2481
|
-
const
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2485
|
-
|
|
2486
|
-
|
|
2487
|
-
|
|
2488
|
-
|
|
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
|
-
|
|
2493
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
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
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
2511
|
-
|
|
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
|
-
|
|
2540
|
-
|
|
2758
|
+
fromPath: normalizedFromFeature,
|
|
2759
|
+
fromName: getFeatureComponentName(normalizedFromFeature),
|
|
2760
|
+
type: 'feature'
|
|
2541
2761
|
}, {
|
|
2542
|
-
|
|
2543
|
-
|
|
2762
|
+
toPath: normalizedToFeature || normalizedFromFeature,
|
|
2763
|
+
toName: getFeatureComponentName(normalizedToFeature || normalizedFromFeature)
|
|
2544
2764
|
}, options);
|
|
2545
2765
|
}
|
|
2546
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|