@oamm/textor 1.0.12 → 1.0.14
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 +35 -0
- package/dist/bin/textor.js +668 -434
- package/dist/bin/textor.js.map +1 -1
- package/dist/index.cjs +646 -384
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +26 -1
- package/dist/index.js +644 -385
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/bin/textor.js
CHANGED
|
@@ -543,6 +543,7 @@ function normalizeRoute(route) {
|
|
|
543
543
|
|
|
544
544
|
function routeToFilePath(route, options = {}) {
|
|
545
545
|
const { extension = '.astro', mode = 'flat', indexFile = 'index.astro' } = options;
|
|
546
|
+
if (!route) return null;
|
|
546
547
|
const normalized = normalizeRoute(route);
|
|
547
548
|
|
|
548
549
|
if (normalized === '/') {
|
|
@@ -715,17 +716,6 @@ async function safeDelete(filePath, options = {}) {
|
|
|
715
716
|
return { deleted: true };
|
|
716
717
|
}
|
|
717
718
|
|
|
718
|
-
async function ensureNotExists(filePath, force = false) {
|
|
719
|
-
if (existsSync(filePath)) {
|
|
720
|
-
if (!force) {
|
|
721
|
-
throw new Error(
|
|
722
|
-
`File already exists: ${filePath}\n` +
|
|
723
|
-
`Use --force to overwrite.`
|
|
724
|
-
);
|
|
725
|
-
}
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
|
|
729
719
|
async function ensureDir(dirPath) {
|
|
730
720
|
await mkdir(dirPath, { recursive: true });
|
|
731
721
|
}
|
|
@@ -832,6 +822,11 @@ async function safeMove(fromPath, toPath, options = {}) {
|
|
|
832
822
|
throw new Error(`Source file not found: ${fromPath}`);
|
|
833
823
|
}
|
|
834
824
|
|
|
825
|
+
if (path.resolve(fromPath) === path.resolve(toPath)) {
|
|
826
|
+
const content = await readFile(toPath, 'utf-8');
|
|
827
|
+
return calculateHash(content, normalization);
|
|
828
|
+
}
|
|
829
|
+
|
|
835
830
|
if (existsSync(toPath) && !force) {
|
|
836
831
|
throw new Error(
|
|
837
832
|
`Destination already exists: ${toPath}\n` +
|
|
@@ -2076,19 +2071,25 @@ async function addSectionCommand(route, featurePath, options) {
|
|
|
2076
2071
|
}
|
|
2077
2072
|
}
|
|
2078
2073
|
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
if
|
|
2085
|
-
if (
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2074
|
+
const featureExists = existsSync(featureFilePath);
|
|
2075
|
+
if (featureExists && !options.force) {
|
|
2076
|
+
console.log(`ℹ Feature already exists at ${featureFilePath}. Entering additive mode.`);
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
// Check sub-items only if not in force mode
|
|
2080
|
+
if (!options.force) {
|
|
2081
|
+
if (shouldCreateIndex && existsSync(indexFilePath)) console.log(` - Skipping existing index: ${indexFilePath}`);
|
|
2082
|
+
if (shouldCreateContext && existsSync(contextFilePath)) console.log(` - Skipping existing context: ${contextFilePath}`);
|
|
2083
|
+
if (shouldCreateHooks && existsSync(hookFilePath)) console.log(` - Skipping existing hook: ${hookFilePath}`);
|
|
2084
|
+
if (shouldCreateTests && existsSync(testFilePath)) console.log(` - Skipping existing test: ${testFilePath}`);
|
|
2085
|
+
if (shouldCreateTypes && existsSync(typesFilePath)) console.log(` - Skipping existing types: ${typesFilePath}`);
|
|
2086
|
+
if (shouldCreateApi && existsSync(apiFilePath)) console.log(` - Skipping existing api: ${apiFilePath}`);
|
|
2087
|
+
if (shouldCreateServices && existsSync(servicesFilePath)) console.log(` - Skipping existing services: ${servicesFilePath}`);
|
|
2088
|
+
if (shouldCreateSchemas && existsSync(schemasFilePath)) console.log(` - Skipping existing schemas: ${schemasFilePath}`);
|
|
2089
|
+
if (shouldCreateReadme && existsSync(readmeFilePath)) console.log(` - Skipping existing readme: ${readmeFilePath}`);
|
|
2090
|
+
if (shouldCreateStories && existsSync(storiesFilePath)) console.log(` - Skipping existing stories: ${storiesFilePath}`);
|
|
2091
|
+
if (shouldCreateScriptsDir && existsSync(scriptsIndexPath)) console.log(` - Skipping existing scripts: ${scriptsIndexPath}`);
|
|
2092
|
+
}
|
|
2092
2093
|
|
|
2093
2094
|
let layoutImportPath = null;
|
|
2094
2095
|
const cliProps = options.prop || {};
|
|
@@ -2229,21 +2230,23 @@ async function addSectionCommand(route, featurePath, options) {
|
|
|
2229
2230
|
|
|
2230
2231
|
const featureSignature = getSignature(config, config.naming.featureExtension === '.astro' ? 'astro' : 'tsx');
|
|
2231
2232
|
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2233
|
+
if (!featureExists || options.force) {
|
|
2234
|
+
const featureHash = await writeFileWithSignature(
|
|
2235
|
+
featureFilePath,
|
|
2236
|
+
featureContent,
|
|
2237
|
+
featureSignature,
|
|
2238
|
+
config.hashing?.normalization
|
|
2239
|
+
);
|
|
2240
|
+
await registerFile(featureFilePath, {
|
|
2241
|
+
kind: 'feature',
|
|
2242
|
+
template: 'feature',
|
|
2243
|
+
hash: featureHash,
|
|
2244
|
+
owner: normalizedRoute
|
|
2245
|
+
});
|
|
2246
|
+
writtenFiles.push(featureFilePath);
|
|
2247
|
+
}
|
|
2245
2248
|
|
|
2246
|
-
if (shouldCreateScriptsDir) {
|
|
2249
|
+
if (shouldCreateScriptsDir && (!existsSync(scriptsIndexPath) || options.force)) {
|
|
2247
2250
|
const hash = await writeFileWithSignature(
|
|
2248
2251
|
scriptsIndexPath,
|
|
2249
2252
|
generateScriptsIndexTemplate(),
|
|
@@ -2259,7 +2262,7 @@ async function addSectionCommand(route, featurePath, options) {
|
|
|
2259
2262
|
writtenFiles.push(scriptsIndexPath);
|
|
2260
2263
|
}
|
|
2261
2264
|
|
|
2262
|
-
if (shouldCreateIndex) {
|
|
2265
|
+
if (shouldCreateIndex && (!existsSync(indexFilePath) || options.force)) {
|
|
2263
2266
|
const indexContent = generateIndexTemplate(featureComponentName, config.naming.featureExtension);
|
|
2264
2267
|
const hash = await writeFileWithSignature(
|
|
2265
2268
|
indexFilePath,
|
|
@@ -2276,7 +2279,7 @@ async function addSectionCommand(route, featurePath, options) {
|
|
|
2276
2279
|
writtenFiles.push(indexFilePath);
|
|
2277
2280
|
}
|
|
2278
2281
|
|
|
2279
|
-
if (shouldCreateApi) {
|
|
2282
|
+
if (shouldCreateApi && (!existsSync(apiFilePath) || options.force)) {
|
|
2280
2283
|
const apiContent = generateApiTemplate(featureComponentName);
|
|
2281
2284
|
const hash = await writeFileWithSignature(
|
|
2282
2285
|
apiFilePath,
|
|
@@ -2293,7 +2296,7 @@ async function addSectionCommand(route, featurePath, options) {
|
|
|
2293
2296
|
writtenFiles.push(apiFilePath);
|
|
2294
2297
|
}
|
|
2295
2298
|
|
|
2296
|
-
if (shouldCreateServices) {
|
|
2299
|
+
if (shouldCreateServices && (!existsSync(servicesFilePath) || options.force)) {
|
|
2297
2300
|
const servicesContent = generateServiceTemplate(featureComponentName);
|
|
2298
2301
|
const hash = await writeFileWithSignature(
|
|
2299
2302
|
servicesFilePath,
|
|
@@ -2310,7 +2313,7 @@ async function addSectionCommand(route, featurePath, options) {
|
|
|
2310
2313
|
writtenFiles.push(servicesFilePath);
|
|
2311
2314
|
}
|
|
2312
2315
|
|
|
2313
|
-
if (shouldCreateSchemas) {
|
|
2316
|
+
if (shouldCreateSchemas && (!existsSync(schemasFilePath) || options.force)) {
|
|
2314
2317
|
const schemasContent = generateSchemaTemplate(featureComponentName);
|
|
2315
2318
|
const hash = await writeFileWithSignature(
|
|
2316
2319
|
schemasFilePath,
|
|
@@ -2327,7 +2330,7 @@ async function addSectionCommand(route, featurePath, options) {
|
|
|
2327
2330
|
writtenFiles.push(schemasFilePath);
|
|
2328
2331
|
}
|
|
2329
2332
|
|
|
2330
|
-
if (shouldCreateHooks) {
|
|
2333
|
+
if (shouldCreateHooks && (!existsSync(hookFilePath) || options.force)) {
|
|
2331
2334
|
const hookName = getHookFunctionName(featureComponentName);
|
|
2332
2335
|
const hookContent = generateHookTemplate(featureComponentName, hookName);
|
|
2333
2336
|
const hash = await writeFileWithSignature(
|
|
@@ -2345,7 +2348,7 @@ async function addSectionCommand(route, featurePath, options) {
|
|
|
2345
2348
|
writtenFiles.push(hookFilePath);
|
|
2346
2349
|
}
|
|
2347
2350
|
|
|
2348
|
-
if (shouldCreateContext) {
|
|
2351
|
+
if (shouldCreateContext && (!existsSync(contextFilePath) || options.force)) {
|
|
2349
2352
|
const contextContent = generateContextTemplate(featureComponentName);
|
|
2350
2353
|
const hash = await writeFileWithSignature(
|
|
2351
2354
|
contextFilePath,
|
|
@@ -2362,7 +2365,7 @@ async function addSectionCommand(route, featurePath, options) {
|
|
|
2362
2365
|
writtenFiles.push(contextFilePath);
|
|
2363
2366
|
}
|
|
2364
2367
|
|
|
2365
|
-
if (shouldCreateTests) {
|
|
2368
|
+
if (shouldCreateTests && (!existsSync(testFilePath) || options.force)) {
|
|
2366
2369
|
const relativeFeaturePath = `./${path.basename(featureFilePath)}`;
|
|
2367
2370
|
const testContent = generateTestTemplate(featureComponentName, relativeFeaturePath);
|
|
2368
2371
|
const hash = await writeFileWithSignature(
|
|
@@ -2380,7 +2383,7 @@ async function addSectionCommand(route, featurePath, options) {
|
|
|
2380
2383
|
writtenFiles.push(testFilePath);
|
|
2381
2384
|
}
|
|
2382
2385
|
|
|
2383
|
-
if (shouldCreateTypes) {
|
|
2386
|
+
if (shouldCreateTypes && (!existsSync(typesFilePath) || options.force)) {
|
|
2384
2387
|
const typesContent = generateTypesTemplate(featureComponentName);
|
|
2385
2388
|
const hash = await writeFileWithSignature(
|
|
2386
2389
|
typesFilePath,
|
|
@@ -2397,7 +2400,7 @@ async function addSectionCommand(route, featurePath, options) {
|
|
|
2397
2400
|
writtenFiles.push(typesFilePath);
|
|
2398
2401
|
}
|
|
2399
2402
|
|
|
2400
|
-
if (shouldCreateReadme) {
|
|
2403
|
+
if (shouldCreateReadme && (!existsSync(readmeFilePath) || options.force)) {
|
|
2401
2404
|
const readmeContent = generateReadmeTemplate(featureComponentName);
|
|
2402
2405
|
const hash = await writeFileWithSignature(
|
|
2403
2406
|
readmeFilePath,
|
|
@@ -2414,7 +2417,7 @@ async function addSectionCommand(route, featurePath, options) {
|
|
|
2414
2417
|
writtenFiles.push(readmeFilePath);
|
|
2415
2418
|
}
|
|
2416
2419
|
|
|
2417
|
-
if (shouldCreateStories) {
|
|
2420
|
+
if (shouldCreateStories && (!existsSync(storiesFilePath) || options.force)) {
|
|
2418
2421
|
const relativePath = `./${path.basename(featureFilePath)}`;
|
|
2419
2422
|
const storiesContent = generateStoriesTemplate(featureComponentName, relativePath);
|
|
2420
2423
|
const hash = await writeFileWithSignature(
|
|
@@ -2751,6 +2754,222 @@ async function updateImportsInFile$1(filePath, oldFilePath, newFilePath) {
|
|
|
2751
2754
|
await writeFile(filePath, content, 'utf-8');
|
|
2752
2755
|
}
|
|
2753
2756
|
|
|
2757
|
+
/**
|
|
2758
|
+
* Updates relative imports in a file after it has been moved.
|
|
2759
|
+
*/
|
|
2760
|
+
async function updateImportsInFile(filePath, oldFilePath, newFilePath) {
|
|
2761
|
+
if (!existsSync(filePath)) return;
|
|
2762
|
+
|
|
2763
|
+
let content = await readFile(filePath, 'utf-8');
|
|
2764
|
+
const oldDir = path.dirname(oldFilePath);
|
|
2765
|
+
const newDir = path.dirname(newFilePath);
|
|
2766
|
+
|
|
2767
|
+
if (oldDir === newDir) return;
|
|
2768
|
+
|
|
2769
|
+
// Find all relative imports
|
|
2770
|
+
const relativeImportRegex = /from\s+['"](\.\.?\/[^'"]+)['"]/g;
|
|
2771
|
+
let match;
|
|
2772
|
+
const replacements = [];
|
|
2773
|
+
|
|
2774
|
+
while ((match = relativeImportRegex.exec(content)) !== null) {
|
|
2775
|
+
const relativePath = match[1];
|
|
2776
|
+
const absoluteTarget = path.resolve(oldDir, relativePath);
|
|
2777
|
+
const newRelativePath = getRelativeImportPath(newFilePath, absoluteTarget);
|
|
2778
|
+
|
|
2779
|
+
replacements.push({
|
|
2780
|
+
full: match[0],
|
|
2781
|
+
oldRel: relativePath,
|
|
2782
|
+
newRel: newRelativePath
|
|
2783
|
+
});
|
|
2784
|
+
}
|
|
2785
|
+
|
|
2786
|
+
for (const repl of replacements) {
|
|
2787
|
+
content = content.replace(repl.full, `from '${repl.newRel}'`);
|
|
2788
|
+
}
|
|
2789
|
+
|
|
2790
|
+
await writeFile(filePath, content, 'utf-8');
|
|
2791
|
+
}
|
|
2792
|
+
|
|
2793
|
+
/**
|
|
2794
|
+
* Moves a directory and its contents, renaming files and updating internal content/imports.
|
|
2795
|
+
*/
|
|
2796
|
+
async function moveDirectory(fromPath, toPath, state, config, options = {}) {
|
|
2797
|
+
const { fromName, toName, owner = null, signatures = [] } = options;
|
|
2798
|
+
|
|
2799
|
+
if (!existsSync(fromPath)) {
|
|
2800
|
+
throw new Error(`Source directory not found: ${fromPath}`);
|
|
2801
|
+
}
|
|
2802
|
+
|
|
2803
|
+
if (existsSync(toPath) && !options.force) {
|
|
2804
|
+
throw new Error(
|
|
2805
|
+
`Destination already exists: ${toPath}\n` +
|
|
2806
|
+
`Use --force to overwrite.`
|
|
2807
|
+
);
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
await ensureDir(toPath);
|
|
2811
|
+
|
|
2812
|
+
const entries = await readdir(fromPath);
|
|
2813
|
+
|
|
2814
|
+
for (const entry of entries) {
|
|
2815
|
+
let targetEntry = entry;
|
|
2816
|
+
|
|
2817
|
+
// Rename files if they match the component name
|
|
2818
|
+
if (fromName && toName && fromName !== toName) {
|
|
2819
|
+
if (entry.includes(fromName)) {
|
|
2820
|
+
targetEntry = entry.replace(fromName, toName);
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
|
|
2824
|
+
const fromEntryPath = path.join(fromPath, entry);
|
|
2825
|
+
const toEntryPath = path.join(toPath, targetEntry);
|
|
2826
|
+
|
|
2827
|
+
const stats = await stat(fromEntryPath);
|
|
2828
|
+
|
|
2829
|
+
if (stats.isDirectory()) {
|
|
2830
|
+
await moveDirectory(fromEntryPath, toEntryPath, state, config, options);
|
|
2831
|
+
} else {
|
|
2832
|
+
const normalizedFromRelative = path.relative(process.cwd(), fromEntryPath).replace(/\\/g, '/');
|
|
2833
|
+
const fileState = state.files[normalizedFromRelative];
|
|
2834
|
+
|
|
2835
|
+
const newHash = await safeMove(fromEntryPath, toEntryPath, {
|
|
2836
|
+
force: options.force,
|
|
2837
|
+
expectedHash: fileState?.hash,
|
|
2838
|
+
acceptChanges: options.acceptChanges,
|
|
2839
|
+
normalization: config.hashing?.normalization,
|
|
2840
|
+
owner,
|
|
2841
|
+
actualOwner: fileState?.owner,
|
|
2842
|
+
signatures
|
|
2843
|
+
});
|
|
2844
|
+
|
|
2845
|
+
// Update internal content (signatures, component names) if renaming
|
|
2846
|
+
if (fromName && toName && fromName !== toName) {
|
|
2847
|
+
let content = await readFile(toEntryPath, 'utf-8');
|
|
2848
|
+
let hasChanged = false;
|
|
2849
|
+
|
|
2850
|
+
// Simple replacement of component names
|
|
2851
|
+
if (content.includes(fromName)) {
|
|
2852
|
+
content = content.replace(new RegExp(fromName, 'g'), toName);
|
|
2853
|
+
hasChanged = true;
|
|
2854
|
+
}
|
|
2855
|
+
|
|
2856
|
+
// Also handle lowercase class names if any
|
|
2857
|
+
const fromLower = fromName.toLowerCase();
|
|
2858
|
+
const toLower = toName.toLowerCase();
|
|
2859
|
+
if (content.includes(fromLower)) {
|
|
2860
|
+
content = content.replace(new RegExp(fromLower, 'g'), toLower);
|
|
2861
|
+
hasChanged = true;
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
if (hasChanged) {
|
|
2865
|
+
await writeFile(toEntryPath, content, 'utf-8');
|
|
2866
|
+
// Re-calculate hash after content update
|
|
2867
|
+
const updatedHash = calculateHash(content, config.hashing?.normalization);
|
|
2868
|
+
|
|
2869
|
+
const normalizedToRelative = path.relative(process.cwd(), toEntryPath).replace(/\\/g, '/');
|
|
2870
|
+
if (fileState) {
|
|
2871
|
+
state.files[normalizedToRelative] = { ...fileState, hash: updatedHash };
|
|
2872
|
+
delete state.files[normalizedFromRelative];
|
|
2873
|
+
}
|
|
2874
|
+
} else {
|
|
2875
|
+
// Update state for each file moved normally
|
|
2876
|
+
const normalizedToRelative = path.relative(process.cwd(), toEntryPath).replace(/\\/g, '/');
|
|
2877
|
+
if (fileState) {
|
|
2878
|
+
state.files[normalizedToRelative] = { ...fileState, hash: newHash };
|
|
2879
|
+
delete state.files[normalizedFromRelative];
|
|
2880
|
+
}
|
|
2881
|
+
}
|
|
2882
|
+
} else {
|
|
2883
|
+
// Update state for each file moved normally
|
|
2884
|
+
const normalizedToRelative = path.relative(process.cwd(), toEntryPath).replace(/\\/g, '/');
|
|
2885
|
+
if (fileState) {
|
|
2886
|
+
state.files[normalizedToRelative] = { ...fileState, hash: newHash };
|
|
2887
|
+
delete state.files[normalizedFromRelative];
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2890
|
+
}
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
const remainingFiles = await readdir(fromPath);
|
|
2894
|
+
if (remainingFiles.length === 0) {
|
|
2895
|
+
await rmdir(fromPath);
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
|
|
2899
|
+
/**
|
|
2900
|
+
* Scans the project and replaces imports of a moved/renamed item.
|
|
2901
|
+
*/
|
|
2902
|
+
async function scanAndReplaceImports(config, state, fromInfo, toInfo, options) {
|
|
2903
|
+
const { fromPath: fromItemPath, fromName, type } = fromInfo;
|
|
2904
|
+
const { toPath: toItemPath, toName } = toInfo;
|
|
2905
|
+
|
|
2906
|
+
const allFiles = new Set();
|
|
2907
|
+
await scanDirectory(process.cwd(), allFiles);
|
|
2908
|
+
|
|
2909
|
+
const rootPath = resolvePath(config, type === 'component' ? 'components' : 'features');
|
|
2910
|
+
|
|
2911
|
+
for (const relPath of allFiles) {
|
|
2912
|
+
const fullPath = path.resolve(process.cwd(), relPath);
|
|
2913
|
+
|
|
2914
|
+
// Skip the moved directory itself
|
|
2915
|
+
const toFullPath = path.resolve(toItemPath);
|
|
2916
|
+
if (fullPath.startsWith(toFullPath)) continue;
|
|
2917
|
+
|
|
2918
|
+
let content = await readFile(fullPath, 'utf-8');
|
|
2919
|
+
let changed = false;
|
|
2920
|
+
|
|
2921
|
+
const aliasBase = config.importAliases[type === 'component' ? 'components' : 'features'];
|
|
2922
|
+
const ext = type === 'component' ? '' : (config.naming.featureExtension === '.astro' ? '.astro' : '');
|
|
2923
|
+
|
|
2924
|
+
if (aliasBase) {
|
|
2925
|
+
const oldAlias = `${aliasBase}/${fromItemPath}`;
|
|
2926
|
+
const newAlias = `${aliasBase}/${toItemPath}`;
|
|
2927
|
+
|
|
2928
|
+
const oldFullImport = `from '${oldAlias}/${fromName}${ext}'`;
|
|
2929
|
+
const newFullImport = `from '${newAlias}/${toName}${ext}'`;
|
|
2930
|
+
|
|
2931
|
+
if (content.includes(oldFullImport)) {
|
|
2932
|
+
content = content.replace(new RegExp(oldFullImport.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newFullImport);
|
|
2933
|
+
changed = true;
|
|
2934
|
+
} else if (content.includes(oldAlias)) {
|
|
2935
|
+
content = content.replace(new RegExp(oldAlias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newAlias);
|
|
2936
|
+
changed = true;
|
|
2937
|
+
}
|
|
2938
|
+
} else {
|
|
2939
|
+
const oldDir = path.resolve(rootPath, fromItemPath);
|
|
2940
|
+
const newDir = path.resolve(rootPath, toItemPath);
|
|
2941
|
+
|
|
2942
|
+
const oldRelPath = getRelativeImportPath(fullPath, oldDir);
|
|
2943
|
+
const newRelPath = getRelativeImportPath(fullPath, newDir);
|
|
2944
|
+
|
|
2945
|
+
const oldImport = `'${oldRelPath}/${fromName}${ext}'`;
|
|
2946
|
+
const newImport = `'${newRelPath}/${toName}${ext}'`;
|
|
2947
|
+
|
|
2948
|
+
if (content.includes(oldImport)) {
|
|
2949
|
+
content = content.replace(new RegExp(oldImport.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newImport);
|
|
2950
|
+
changed = true;
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
|
|
2954
|
+
if (fromName !== toName && changed) {
|
|
2955
|
+
content = content.replace(new RegExp(`\\b${fromName}\\b`, 'g'), toName);
|
|
2956
|
+
}
|
|
2957
|
+
|
|
2958
|
+
if (changed) {
|
|
2959
|
+
if (options.dryRun) {
|
|
2960
|
+
console.log(` [Scan] Would update imports in ${relPath}`);
|
|
2961
|
+
} else {
|
|
2962
|
+
await writeFile(fullPath, content, 'utf-8');
|
|
2963
|
+
console.log(` [Scan] Updated imports in ${relPath}`);
|
|
2964
|
+
|
|
2965
|
+
if (state.files[relPath]) {
|
|
2966
|
+
state.files[relPath].hash = calculateHash(content, config.hashing?.normalization);
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
}
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2754
2973
|
/**
|
|
2755
2974
|
* Move a section (route + feature).
|
|
2756
2975
|
*
|
|
@@ -2779,45 +2998,59 @@ async function moveSectionCommand(fromRoute, fromFeature, toRoute, toFeature, op
|
|
|
2779
2998
|
let actualToRoute = toRoute;
|
|
2780
2999
|
let actualToFeature = toFeature;
|
|
2781
3000
|
|
|
2782
|
-
// Shift arguments if using state
|
|
3001
|
+
// Shift arguments if using state or if called with fewer arguments
|
|
2783
3002
|
if (!toRoute && fromRoute && fromFeature) {
|
|
2784
3003
|
// textor move-section /old-route /new-route
|
|
2785
|
-
|
|
3004
|
+
actualFromRoute = fromRoute;
|
|
3005
|
+
actualToRoute = fromFeature;
|
|
3006
|
+
actualFromFeature = undefined;
|
|
3007
|
+
actualToFeature = undefined;
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
// Lookup missing info from state
|
|
3011
|
+
if (actualFromRoute && !actualFromFeature) {
|
|
3012
|
+
const section = findSection(state, actualFromRoute);
|
|
2786
3013
|
if (section) {
|
|
2787
|
-
actualFromRoute = section.route;
|
|
2788
3014
|
actualFromFeature = section.featurePath;
|
|
2789
|
-
|
|
2790
|
-
|
|
3015
|
+
}
|
|
3016
|
+
} else if (!actualFromRoute && actualFromFeature) {
|
|
3017
|
+
const section = findSection(state, actualFromFeature);
|
|
3018
|
+
if (section) {
|
|
3019
|
+
actualFromRoute = section.route;
|
|
3020
|
+
}
|
|
3021
|
+
}
|
|
3022
|
+
|
|
3023
|
+
// If toFeature is not provided, try to derive it from the new route if route moved
|
|
3024
|
+
if (!actualToFeature && actualToRoute && actualFromRoute && actualFromRoute !== actualToRoute && actualFromFeature) {
|
|
3025
|
+
const oldRouteParts = actualFromRoute.split('/').filter(Boolean);
|
|
3026
|
+
const newRouteParts = actualToRoute.split('/').filter(Boolean);
|
|
3027
|
+
const oldFeatureParts = actualFromFeature.split('/').filter(Boolean);
|
|
3028
|
+
|
|
3029
|
+
let match = true;
|
|
3030
|
+
for (let i = 0; i < oldRouteParts.length; i++) {
|
|
3031
|
+
const routePart = oldRouteParts[i].toLowerCase();
|
|
3032
|
+
const featurePart = oldFeatureParts[i] ? oldFeatureParts[i].toLowerCase() : null;
|
|
2791
3033
|
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
const newRouteParts = actualToRoute.split('/').filter(Boolean);
|
|
2796
|
-
const oldFeatureParts = actualFromFeature.split('/').filter(Boolean);
|
|
2797
|
-
|
|
2798
|
-
// If the feature path starts with the old route parts, replace them
|
|
2799
|
-
// We compare case-insensitively or via PascalCase to be more helpful
|
|
2800
|
-
let match = true;
|
|
2801
|
-
for (let i = 0; i < oldRouteParts.length; i++) {
|
|
2802
|
-
const routePart = oldRouteParts[i].toLowerCase();
|
|
2803
|
-
const featurePart = oldFeatureParts[i] ? oldFeatureParts[i].toLowerCase() : null;
|
|
2804
|
-
|
|
2805
|
-
if (featurePart !== routePart) {
|
|
2806
|
-
match = false;
|
|
2807
|
-
break;
|
|
2808
|
-
}
|
|
2809
|
-
}
|
|
2810
|
-
|
|
2811
|
-
if (match && oldRouteParts.length > 0) {
|
|
2812
|
-
actualToFeature = [...newRouteParts, ...oldFeatureParts.slice(oldRouteParts.length)].join('/');
|
|
2813
|
-
} else {
|
|
2814
|
-
// Otherwise just keep it the same
|
|
2815
|
-
actualToFeature = actualFromFeature;
|
|
2816
|
-
}
|
|
3034
|
+
if (featurePart !== routePart) {
|
|
3035
|
+
match = false;
|
|
3036
|
+
break;
|
|
2817
3037
|
}
|
|
2818
3038
|
}
|
|
3039
|
+
|
|
3040
|
+
if (match && oldRouteParts.length > 0) {
|
|
3041
|
+
actualToFeature = [...newRouteParts, ...oldFeatureParts.slice(oldRouteParts.length)].join('/');
|
|
3042
|
+
} else {
|
|
3043
|
+
actualToFeature = actualFromFeature;
|
|
3044
|
+
}
|
|
3045
|
+
} else if (!actualToFeature) {
|
|
3046
|
+
actualToFeature = actualFromFeature;
|
|
2819
3047
|
}
|
|
2820
3048
|
|
|
3049
|
+
if (!actualToRoute) {
|
|
3050
|
+
actualToRoute = actualFromRoute;
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
|
|
2821
3054
|
const isRouteOnly = options.keepFeature || (!actualToFeature && actualToRoute && !actualFromFeature);
|
|
2822
3055
|
|
|
2823
3056
|
if (isRouteOnly && !actualToRoute) {
|
|
@@ -2847,14 +3080,16 @@ async function moveSectionCommand(fromRoute, fromFeature, toRoute, toFeature, op
|
|
|
2847
3080
|
indexFile: config.routing.indexFile
|
|
2848
3081
|
});
|
|
2849
3082
|
|
|
2850
|
-
const fromRoutePath = secureJoin(pagesRoot, fromRouteFile);
|
|
2851
|
-
const toRoutePath = secureJoin(pagesRoot, toRouteFile);
|
|
3083
|
+
const fromRoutePath = fromRouteFile ? secureJoin(pagesRoot, fromRouteFile) : null;
|
|
3084
|
+
const toRoutePath = toRouteFile ? secureJoin(pagesRoot, toRouteFile) : null;
|
|
2852
3085
|
|
|
2853
3086
|
const movedFiles = [];
|
|
2854
3087
|
|
|
2855
3088
|
if (options.dryRun) {
|
|
2856
3089
|
console.log('Dry run - would move:');
|
|
2857
|
-
|
|
3090
|
+
if (fromRoutePath && toRoutePath && fromRoutePath !== toRoutePath) {
|
|
3091
|
+
console.log(` Route: ${fromRoutePath} -> ${toRoutePath}`);
|
|
3092
|
+
}
|
|
2858
3093
|
|
|
2859
3094
|
if (!isRouteOnly && normalizedFromFeature && normalizedToFeature) {
|
|
2860
3095
|
const fromFeaturePath = secureJoin(featuresRoot, normalizedFromFeature);
|
|
@@ -2865,97 +3100,107 @@ async function moveSectionCommand(fromRoute, fromFeature, toRoute, toFeature, op
|
|
|
2865
3100
|
return;
|
|
2866
3101
|
}
|
|
2867
3102
|
|
|
2868
|
-
|
|
2869
|
-
|
|
3103
|
+
let normalizedToRouteRelative = null;
|
|
3104
|
+
if (fromRoutePath && toRoutePath) {
|
|
3105
|
+
const normalizedFromRouteRelative = path.relative(process.cwd(), fromRoutePath).replace(/\\/g, '/');
|
|
3106
|
+
const routeFileState = state.files[normalizedFromRouteRelative];
|
|
2870
3107
|
|
|
2871
|
-
|
|
2872
|
-
|
|
2873
|
-
|
|
2874
|
-
|
|
2875
|
-
|
|
2876
|
-
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
2881
|
-
|
|
2882
|
-
const normalizedToRouteRelative = path.relative(process.cwd(), toRoutePath).replace(/\\/g, '/');
|
|
2883
|
-
if (routeFileState) {
|
|
2884
|
-
state.files[normalizedToRouteRelative] = { ...routeFileState, hash: newRouteHash };
|
|
2885
|
-
delete state.files[normalizedFromRouteRelative];
|
|
2886
|
-
}
|
|
2887
|
-
|
|
2888
|
-
// Update imports in the moved route file
|
|
2889
|
-
const targetFeature = normalizedToFeature || normalizedFromFeature;
|
|
2890
|
-
if (targetFeature) {
|
|
2891
|
-
const fromFeatureDirPath = secureJoin(featuresRoot, normalizedFromFeature);
|
|
2892
|
-
const toFeatureDirPath = secureJoin(featuresRoot, targetFeature);
|
|
2893
|
-
const fromFeatureComponentName = getFeatureComponentName(normalizedFromFeature);
|
|
2894
|
-
const toFeatureComponentName = getFeatureComponentName(targetFeature);
|
|
2895
|
-
|
|
2896
|
-
// First, update all relative imports in the file because it moved
|
|
2897
|
-
await updateImportsInFile(toRoutePath, fromRoutePath, toRoutePath);
|
|
2898
|
-
|
|
2899
|
-
let content = await readFile(toRoutePath, 'utf-8');
|
|
2900
|
-
let changed = false;
|
|
2901
|
-
|
|
2902
|
-
// Update component name in JSX tags
|
|
2903
|
-
if (fromFeatureComponentName !== toFeatureComponentName) {
|
|
2904
|
-
content = content.replace(
|
|
2905
|
-
new RegExp(`<${fromFeatureComponentName}`, 'g'),
|
|
2906
|
-
`<${toFeatureComponentName}`
|
|
2907
|
-
);
|
|
2908
|
-
content = content.replace(
|
|
2909
|
-
new RegExp(`</${fromFeatureComponentName}`, 'g'),
|
|
2910
|
-
`</${toFeatureComponentName}`
|
|
2911
|
-
);
|
|
2912
|
-
changed = true;
|
|
3108
|
+
const newRouteHash = await safeMove(fromRoutePath, toRoutePath, {
|
|
3109
|
+
force: options.force,
|
|
3110
|
+
expectedHash: routeFileState?.hash,
|
|
3111
|
+
acceptChanges: options.acceptChanges,
|
|
3112
|
+
owner: normalizedFromRoute,
|
|
3113
|
+
actualOwner: routeFileState?.owner,
|
|
3114
|
+
signatures: configSignatures
|
|
3115
|
+
});
|
|
3116
|
+
|
|
3117
|
+
if (fromRoutePath !== toRoutePath) {
|
|
3118
|
+
movedFiles.push({ from: fromRoutePath, to: toRoutePath });
|
|
2913
3119
|
}
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
|
|
2917
|
-
|
|
2918
|
-
|
|
2919
|
-
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
if (importRegex.test(content)) {
|
|
2923
|
-
content = content.replace(importRegex, (match, p1, p2, p3, subPath, p5) => {
|
|
2924
|
-
let newSubPath = subPath || '';
|
|
2925
|
-
if (subPath && subPath.includes(fromFeatureComponentName)) {
|
|
2926
|
-
newSubPath = subPath.replace(fromFeatureComponentName, toFeatureComponentName);
|
|
2927
|
-
}
|
|
2928
|
-
return `${p1}${toFeatureComponentName}${p3}${newAliasPath}${newSubPath}${p5}`;
|
|
2929
|
-
});
|
|
2930
|
-
changed = true;
|
|
2931
|
-
} else if (content.includes(oldAliasPath)) {
|
|
2932
|
-
// Fallback for path only replacement
|
|
2933
|
-
content = content.replace(new RegExp(oldAliasPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newAliasPath);
|
|
2934
|
-
changed = true;
|
|
3120
|
+
|
|
3121
|
+
// Update state for moved route file
|
|
3122
|
+
normalizedToRouteRelative = path.relative(process.cwd(), toRoutePath).replace(/\\/g, '/');
|
|
3123
|
+
if (routeFileState) {
|
|
3124
|
+
state.files[normalizedToRouteRelative] = { ...routeFileState, hash: newRouteHash };
|
|
3125
|
+
if (fromRoutePath !== toRoutePath) {
|
|
3126
|
+
delete state.files[normalizedFromRouteRelative];
|
|
2935
3127
|
}
|
|
2936
|
-
}
|
|
2937
|
-
|
|
2938
|
-
|
|
2939
|
-
|
|
2940
|
-
|
|
2941
|
-
const
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3130
|
+
// Update imports in the route file (even if it didn't move, as feature might have)
|
|
3131
|
+
const targetFeature = normalizedToFeature || normalizedFromFeature;
|
|
3132
|
+
if (targetFeature && existsSync(toRoutePath)) {
|
|
3133
|
+
const fromFeatureDirPath = secureJoin(featuresRoot, normalizedFromFeature);
|
|
3134
|
+
const toFeatureDirPath = secureJoin(featuresRoot, targetFeature);
|
|
3135
|
+
const fromFeatureComponentName = getFeatureComponentName(normalizedFromFeature);
|
|
3136
|
+
const toFeatureComponentName = getFeatureComponentName(targetFeature);
|
|
3137
|
+
|
|
3138
|
+
// First, update all relative imports in the file because it moved (or stayed)
|
|
3139
|
+
await updateImportsInFile(toRoutePath, fromRoutePath, toRoutePath);
|
|
3140
|
+
|
|
3141
|
+
let content = await readFile(toRoutePath, 'utf-8');
|
|
3142
|
+
let changed = false;
|
|
3143
|
+
|
|
3144
|
+
// Update component name in JSX tags
|
|
3145
|
+
if (fromFeatureComponentName !== toFeatureComponentName) {
|
|
3146
|
+
content = content.replace(
|
|
3147
|
+
new RegExp(`<${fromFeatureComponentName}`, 'g'),
|
|
3148
|
+
`<${toFeatureComponentName}`
|
|
3149
|
+
);
|
|
3150
|
+
content = content.replace(
|
|
3151
|
+
new RegExp(`</${fromFeatureComponentName}`, 'g'),
|
|
3152
|
+
`</${toFeatureComponentName}`
|
|
3153
|
+
);
|
|
2951
3154
|
changed = true;
|
|
2952
3155
|
}
|
|
2953
|
-
}
|
|
2954
3156
|
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
3157
|
+
if (config.importAliases.features) {
|
|
3158
|
+
const oldAliasPath = `${config.importAliases.features}/${normalizedFromFeature}`;
|
|
3159
|
+
const newAliasPath = `${config.importAliases.features}/${targetFeature}`;
|
|
3160
|
+
|
|
3161
|
+
// Flexible regex to match import identifier and path with alias
|
|
3162
|
+
const importRegex = new RegExp(`(import\\s+)(${fromFeatureComponentName})(\\s+from\\s+['"])${oldAliasPath}(/[^'"]+)?(['"])`, 'g');
|
|
3163
|
+
|
|
3164
|
+
if (importRegex.test(content)) {
|
|
3165
|
+
content = content.replace(importRegex, (match, p1, p2, p3, subPath, p5) => {
|
|
3166
|
+
let newSubPath = subPath || '';
|
|
3167
|
+
if (subPath && subPath.includes(fromFeatureComponentName)) {
|
|
3168
|
+
newSubPath = subPath.replace(fromFeatureComponentName, toFeatureComponentName);
|
|
3169
|
+
}
|
|
3170
|
+
return `${p1}${toFeatureComponentName}${p3}${newAliasPath}${newSubPath}${p5}`;
|
|
3171
|
+
});
|
|
3172
|
+
changed = true;
|
|
3173
|
+
} else if (content.includes(oldAliasPath)) {
|
|
3174
|
+
// Fallback for path only replacement
|
|
3175
|
+
content = content.replace(new RegExp(oldAliasPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newAliasPath);
|
|
3176
|
+
changed = true;
|
|
3177
|
+
}
|
|
3178
|
+
} else {
|
|
3179
|
+
const oldRelativeDir = getRelativeImportPath(toRoutePath, fromFeatureDirPath);
|
|
3180
|
+
const newRelativeDir = getRelativeImportPath(toRoutePath, toFeatureDirPath);
|
|
3181
|
+
|
|
3182
|
+
// Flexible regex for relative imports
|
|
3183
|
+
const relImportRegex = new RegExp(`(import\\s+)(${fromFeatureComponentName})(\\s+from\\s+['"])${oldRelativeDir}(/[^'"]+)?(['"])`, 'g');
|
|
3184
|
+
|
|
3185
|
+
if (relImportRegex.test(content)) {
|
|
3186
|
+
content = content.replace(relImportRegex, (match, p1, p2, p3, subPath, p5) => {
|
|
3187
|
+
let newSubPath = subPath || '';
|
|
3188
|
+
if (subPath && subPath.includes(fromFeatureComponentName)) {
|
|
3189
|
+
newSubPath = subPath.replace(fromFeatureComponentName, toFeatureComponentName);
|
|
3190
|
+
}
|
|
3191
|
+
return `${p1}${toFeatureComponentName}${p3}${newRelativeDir}${newSubPath}${p5}`;
|
|
3192
|
+
});
|
|
3193
|
+
changed = true;
|
|
3194
|
+
}
|
|
3195
|
+
}
|
|
3196
|
+
|
|
3197
|
+
if (changed) {
|
|
3198
|
+
await writeFile(toRoutePath, content, 'utf-8');
|
|
3199
|
+
// Update hash in state after changes
|
|
3200
|
+
if (state.files[normalizedToRouteRelative]) {
|
|
3201
|
+
state.files[normalizedToRouteRelative].hash = calculateHash(content, config.hashing?.normalization);
|
|
3202
|
+
}
|
|
3203
|
+
}
|
|
2959
3204
|
}
|
|
2960
3205
|
}
|
|
2961
3206
|
|
|
@@ -2982,15 +3227,18 @@ async function moveSectionCommand(fromRoute, fromFeature, toRoute, toFeature, op
|
|
|
2982
3227
|
|
|
2983
3228
|
if (options.scan && (normalizedFromFeature || normalizedToFeature)) {
|
|
2984
3229
|
await scanAndReplaceImports(config, state, {
|
|
2985
|
-
|
|
2986
|
-
|
|
3230
|
+
fromPath: normalizedFromFeature,
|
|
3231
|
+
fromName: getFeatureComponentName(normalizedFromFeature),
|
|
3232
|
+
type: 'feature'
|
|
2987
3233
|
}, {
|
|
2988
|
-
|
|
2989
|
-
|
|
3234
|
+
toPath: normalizedToFeature || normalizedFromFeature,
|
|
3235
|
+
toName: getFeatureComponentName(normalizedToFeature || normalizedFromFeature)
|
|
2990
3236
|
}, options);
|
|
2991
3237
|
}
|
|
2992
3238
|
|
|
2993
|
-
|
|
3239
|
+
if (fromRoutePath && toRoutePath && fromRoutePath !== toRoutePath) {
|
|
3240
|
+
await cleanupEmptyDirs(path.dirname(fromRoutePath), pagesRoot);
|
|
3241
|
+
}
|
|
2994
3242
|
|
|
2995
3243
|
console.log('✓ Moved:');
|
|
2996
3244
|
movedFiles.forEach(item => {
|
|
@@ -3001,6 +3249,15 @@ async function moveSectionCommand(fromRoute, fromFeature, toRoute, toFeature, op
|
|
|
3001
3249
|
if (movedFiles.length > 0) {
|
|
3002
3250
|
const existingSection = fromSection;
|
|
3003
3251
|
|
|
3252
|
+
// Update ownership in state if route moved
|
|
3253
|
+
if (normalizedFromRoute && normalizedToRoute && normalizedFromRoute !== normalizedToRoute) {
|
|
3254
|
+
for (const f in state.files) {
|
|
3255
|
+
if (state.files[f].owner === normalizedFromRoute) {
|
|
3256
|
+
state.files[f].owner = normalizedToRoute;
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
}
|
|
3260
|
+
|
|
3004
3261
|
// Update section data in state
|
|
3005
3262
|
state.sections = state.sections.filter(s => s.route !== normalizedFromRoute);
|
|
3006
3263
|
state.sections.push({
|
|
@@ -3023,216 +3280,6 @@ async function moveSectionCommand(fromRoute, fromFeature, toRoute, toFeature, op
|
|
|
3023
3280
|
}
|
|
3024
3281
|
}
|
|
3025
3282
|
|
|
3026
|
-
async function scanAndReplaceImports(config, state, fromInfo, toInfo, options) {
|
|
3027
|
-
const { fromFeaturePath, fromComponentName } = fromInfo;
|
|
3028
|
-
const { toFeaturePath, toComponentName } = toInfo;
|
|
3029
|
-
|
|
3030
|
-
const allFiles = new Set();
|
|
3031
|
-
await scanDirectory(process.cwd(), allFiles);
|
|
3032
|
-
|
|
3033
|
-
const featuresRoot = resolvePath(config, 'features');
|
|
3034
|
-
|
|
3035
|
-
for (const relPath of allFiles) {
|
|
3036
|
-
const fullPath = path.join(process.cwd(), relPath);
|
|
3037
|
-
|
|
3038
|
-
// Skip the moved directory itself as it was already handled
|
|
3039
|
-
if (fullPath.startsWith(path.resolve(toFeaturePath))) continue;
|
|
3040
|
-
|
|
3041
|
-
let content = await readFile(fullPath, 'utf-8');
|
|
3042
|
-
let changed = false;
|
|
3043
|
-
|
|
3044
|
-
const ext = config.naming.featureExtension === '.astro' ? '.astro' : '';
|
|
3045
|
-
|
|
3046
|
-
// Handle Aliases
|
|
3047
|
-
if (config.importAliases.features) {
|
|
3048
|
-
const oldAlias = `${config.importAliases.features}/${fromFeaturePath}`;
|
|
3049
|
-
const newAlias = `${config.importAliases.features}/${toFeaturePath}`;
|
|
3050
|
-
|
|
3051
|
-
// Update component name and path if both changed
|
|
3052
|
-
const oldFullImport = `from '${oldAlias}/${fromComponentName}${ext}'`;
|
|
3053
|
-
const newFullImport = `from '${newAlias}/${toComponentName}${ext}'`;
|
|
3054
|
-
|
|
3055
|
-
if (content.includes(oldFullImport)) {
|
|
3056
|
-
content = content.replace(new RegExp(oldFullImport.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newFullImport);
|
|
3057
|
-
changed = true;
|
|
3058
|
-
} else if (content.includes(oldAlias)) {
|
|
3059
|
-
content = content.replace(new RegExp(oldAlias.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newAlias);
|
|
3060
|
-
changed = true;
|
|
3061
|
-
}
|
|
3062
|
-
} else {
|
|
3063
|
-
// Handle Relative Imports (more complex)
|
|
3064
|
-
// This is best-effort: we look for imports that resolve to the old feature path
|
|
3065
|
-
const fromFeatureDir = secureJoin(featuresRoot, fromFeaturePath);
|
|
3066
|
-
const toFeatureDir = secureJoin(featuresRoot, toFeaturePath);
|
|
3067
|
-
|
|
3068
|
-
const oldRelPath = getRelativeImportPath(fullPath, fromFeatureDir);
|
|
3069
|
-
const newRelPath = getRelativeImportPath(fullPath, toFeatureDir);
|
|
3070
|
-
|
|
3071
|
-
const oldImport = `'${oldRelPath}/${fromComponentName}${ext}'`;
|
|
3072
|
-
const newImport = `'${newRelPath}/${toComponentName}${ext}'`;
|
|
3073
|
-
|
|
3074
|
-
if (content.includes(oldImport)) {
|
|
3075
|
-
content = content.replace(new RegExp(oldImport.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newImport);
|
|
3076
|
-
changed = true;
|
|
3077
|
-
}
|
|
3078
|
-
}
|
|
3079
|
-
|
|
3080
|
-
// Update component name in JSX and imports if it changed
|
|
3081
|
-
if (fromComponentName !== toComponentName && changed) {
|
|
3082
|
-
content = content.replace(new RegExp(`\\b${fromComponentName}\\b`, 'g'), toComponentName);
|
|
3083
|
-
}
|
|
3084
|
-
|
|
3085
|
-
if (changed) {
|
|
3086
|
-
if (options.dryRun) {
|
|
3087
|
-
console.log(` [Scan] Would update imports in ${relPath}`);
|
|
3088
|
-
} else {
|
|
3089
|
-
await writeFile(fullPath, content, 'utf-8');
|
|
3090
|
-
console.log(` [Scan] Updated imports in ${relPath}`);
|
|
3091
|
-
|
|
3092
|
-
// Update state hash if this file is managed
|
|
3093
|
-
if (state.files[relPath]) {
|
|
3094
|
-
state.files[relPath].hash = calculateHash(content, config.hashing?.normalization);
|
|
3095
|
-
}
|
|
3096
|
-
}
|
|
3097
|
-
}
|
|
3098
|
-
}
|
|
3099
|
-
}
|
|
3100
|
-
|
|
3101
|
-
async function moveDirectory(fromPath, toPath, state, config, options = {}) {
|
|
3102
|
-
const { fromName, toName, owner = null } = options;
|
|
3103
|
-
|
|
3104
|
-
if (!existsSync(fromPath)) {
|
|
3105
|
-
throw new Error(`Source directory not found: ${fromPath}`);
|
|
3106
|
-
}
|
|
3107
|
-
|
|
3108
|
-
if (existsSync(toPath) && !options.force) {
|
|
3109
|
-
throw new Error(
|
|
3110
|
-
`Destination already exists: ${toPath}\n` +
|
|
3111
|
-
`Use --force to overwrite.`
|
|
3112
|
-
);
|
|
3113
|
-
}
|
|
3114
|
-
|
|
3115
|
-
await ensureDir(toPath);
|
|
3116
|
-
|
|
3117
|
-
const entries = await readdir(fromPath);
|
|
3118
|
-
|
|
3119
|
-
for (const entry of entries) {
|
|
3120
|
-
let targetEntry = entry;
|
|
3121
|
-
|
|
3122
|
-
// Rename files if they match the component name
|
|
3123
|
-
if (fromName && toName && fromName !== toName) {
|
|
3124
|
-
if (entry.includes(fromName)) {
|
|
3125
|
-
targetEntry = entry.replace(fromName, toName);
|
|
3126
|
-
}
|
|
3127
|
-
}
|
|
3128
|
-
|
|
3129
|
-
const fromEntryPath = path.join(fromPath, entry);
|
|
3130
|
-
const toEntryPath = path.join(toPath, targetEntry);
|
|
3131
|
-
|
|
3132
|
-
const stats = await stat(fromEntryPath);
|
|
3133
|
-
|
|
3134
|
-
if (stats.isDirectory()) {
|
|
3135
|
-
await moveDirectory(fromEntryPath, toEntryPath, state, config, options);
|
|
3136
|
-
} else {
|
|
3137
|
-
const normalizedFromRelative = path.relative(process.cwd(), fromEntryPath).replace(/\\/g, '/');
|
|
3138
|
-
const fileState = state.files[normalizedFromRelative];
|
|
3139
|
-
|
|
3140
|
-
const newHash = await safeMove(fromEntryPath, toEntryPath, {
|
|
3141
|
-
force: options.force,
|
|
3142
|
-
expectedHash: fileState?.hash,
|
|
3143
|
-
acceptChanges: options.acceptChanges,
|
|
3144
|
-
normalization: config.hashing?.normalization,
|
|
3145
|
-
owner,
|
|
3146
|
-
actualOwner: fileState?.owner
|
|
3147
|
-
});
|
|
3148
|
-
|
|
3149
|
-
// Update internal content (signatures, component names) if renaming
|
|
3150
|
-
if (fromName && toName && fromName !== toName) {
|
|
3151
|
-
let content = await readFile(toEntryPath, 'utf-8');
|
|
3152
|
-
let hasChanged = false;
|
|
3153
|
-
|
|
3154
|
-
// Simple replacement of component names
|
|
3155
|
-
if (content.includes(fromName)) {
|
|
3156
|
-
content = content.replace(new RegExp(fromName, 'g'), toName);
|
|
3157
|
-
hasChanged = true;
|
|
3158
|
-
}
|
|
3159
|
-
|
|
3160
|
-
// Also handle lowercase class names if any
|
|
3161
|
-
const fromLower = fromName.toLowerCase();
|
|
3162
|
-
const toLower = toName.toLowerCase();
|
|
3163
|
-
if (content.includes(fromLower)) {
|
|
3164
|
-
content = content.replace(new RegExp(fromLower, 'g'), toLower);
|
|
3165
|
-
hasChanged = true;
|
|
3166
|
-
}
|
|
3167
|
-
|
|
3168
|
-
if (hasChanged) {
|
|
3169
|
-
await writeFile(toEntryPath, content, 'utf-8');
|
|
3170
|
-
// Re-calculate hash after content update
|
|
3171
|
-
const updatedHash = calculateHash(content, config.hashing?.normalization);
|
|
3172
|
-
|
|
3173
|
-
const normalizedToRelative = path.relative(process.cwd(), toEntryPath).replace(/\\/g, '/');
|
|
3174
|
-
if (fileState) {
|
|
3175
|
-
state.files[normalizedToRelative] = { ...fileState, hash: updatedHash };
|
|
3176
|
-
delete state.files[normalizedFromRelative];
|
|
3177
|
-
}
|
|
3178
|
-
} else {
|
|
3179
|
-
// Update state for each file moved normally
|
|
3180
|
-
const normalizedToRelative = path.relative(process.cwd(), toEntryPath).replace(/\\/g, '/');
|
|
3181
|
-
if (fileState) {
|
|
3182
|
-
state.files[normalizedToRelative] = { ...fileState, hash: newHash };
|
|
3183
|
-
delete state.files[normalizedFromRelative];
|
|
3184
|
-
}
|
|
3185
|
-
}
|
|
3186
|
-
} else {
|
|
3187
|
-
// Update state for each file moved normally
|
|
3188
|
-
const normalizedToRelative = path.relative(process.cwd(), toEntryPath).replace(/\\/g, '/');
|
|
3189
|
-
if (fileState) {
|
|
3190
|
-
state.files[normalizedToRelative] = { ...fileState, hash: newHash };
|
|
3191
|
-
delete state.files[normalizedFromRelative];
|
|
3192
|
-
}
|
|
3193
|
-
}
|
|
3194
|
-
}
|
|
3195
|
-
}
|
|
3196
|
-
|
|
3197
|
-
const remainingFiles = await readdir(fromPath);
|
|
3198
|
-
if (remainingFiles.length === 0) {
|
|
3199
|
-
await rmdir(fromPath);
|
|
3200
|
-
}
|
|
3201
|
-
}
|
|
3202
|
-
|
|
3203
|
-
async function updateImportsInFile(filePath, oldFilePath, newFilePath) {
|
|
3204
|
-
if (!existsSync(filePath)) return;
|
|
3205
|
-
|
|
3206
|
-
let content = await readFile(filePath, 'utf-8');
|
|
3207
|
-
const oldDir = path.dirname(oldFilePath);
|
|
3208
|
-
const newDir = path.dirname(newFilePath);
|
|
3209
|
-
|
|
3210
|
-
if (oldDir === newDir) return;
|
|
3211
|
-
|
|
3212
|
-
// Find all relative imports
|
|
3213
|
-
const relativeImportRegex = /from\s+['"](\.\.?\/[^'"]+)['"]/g;
|
|
3214
|
-
let match;
|
|
3215
|
-
const replacements = [];
|
|
3216
|
-
|
|
3217
|
-
while ((match = relativeImportRegex.exec(content)) !== null) {
|
|
3218
|
-
const relativePath = match[1];
|
|
3219
|
-
const absoluteTarget = path.resolve(oldDir, relativePath);
|
|
3220
|
-
const newRelativePath = getRelativeImportPath(newFilePath, absoluteTarget);
|
|
3221
|
-
|
|
3222
|
-
replacements.push({
|
|
3223
|
-
full: match[0],
|
|
3224
|
-
oldRel: relativePath,
|
|
3225
|
-
newRel: newRelativePath
|
|
3226
|
-
});
|
|
3227
|
-
}
|
|
3228
|
-
|
|
3229
|
-
for (const repl of replacements) {
|
|
3230
|
-
content = content.replace(repl.full, `from '${repl.newRel}'`);
|
|
3231
|
-
}
|
|
3232
|
-
|
|
3233
|
-
await writeFile(filePath, content, 'utf-8');
|
|
3234
|
-
}
|
|
3235
|
-
|
|
3236
3283
|
async function createComponentCommand(componentName, options) {
|
|
3237
3284
|
try {
|
|
3238
3285
|
const config = await loadConfig();
|
|
@@ -3387,20 +3434,26 @@ async function createComponentCommand(componentName, options) {
|
|
|
3387
3434
|
return;
|
|
3388
3435
|
}
|
|
3389
3436
|
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
|
|
3395
|
-
if
|
|
3396
|
-
if (
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3400
|
-
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3437
|
+
const componentExists = existsSync(componentFilePath);
|
|
3438
|
+
if (componentExists && !options.force) {
|
|
3439
|
+
console.log(`ℹ Component already exists at ${componentFilePath}. Entering additive mode.`);
|
|
3440
|
+
}
|
|
3441
|
+
|
|
3442
|
+
// Check sub-items only if not in force mode
|
|
3443
|
+
if (!options.force) {
|
|
3444
|
+
if (existsSync(indexFilePath)) console.log(` - Skipping existing index: ${indexFilePath}`);
|
|
3445
|
+
if (shouldCreateContext && existsSync(contextFilePath)) console.log(` - Skipping existing context: ${contextFilePath}`);
|
|
3446
|
+
if (shouldCreateHook && existsSync(hookFilePath)) console.log(` - Skipping existing hook: ${hookFilePath}`);
|
|
3447
|
+
if (shouldCreateTests && existsSync(testFilePath)) console.log(` - Skipping existing test: ${testFilePath}`);
|
|
3448
|
+
if (shouldCreateConfig && existsSync(configFilePath)) console.log(` - Skipping existing config: ${configFilePath}`);
|
|
3449
|
+
if (shouldCreateConstants && existsSync(constantsFilePath)) console.log(` - Skipping existing constants: ${constantsFilePath}`);
|
|
3450
|
+
if (shouldCreateTypes && existsSync(typesFilePath)) console.log(` - Skipping existing types: ${typesFilePath}`);
|
|
3451
|
+
if (shouldCreateApi && existsSync(apiFilePath)) console.log(` - Skipping existing api: ${apiFilePath}`);
|
|
3452
|
+
if (shouldCreateServices && existsSync(servicesFilePath)) console.log(` - Skipping existing services: ${servicesFilePath}`);
|
|
3453
|
+
if (shouldCreateSchemas && existsSync(schemasFilePath)) console.log(` - Skipping existing schemas: ${schemasFilePath}`);
|
|
3454
|
+
if (shouldCreateReadme && existsSync(readmeFilePath)) console.log(` - Skipping existing readme: ${readmeFilePath}`);
|
|
3455
|
+
if (shouldCreateStories && existsSync(storiesFilePath)) console.log(` - Skipping existing stories: ${storiesFilePath}`);
|
|
3456
|
+
}
|
|
3404
3457
|
|
|
3405
3458
|
await ensureDir(componentDir);
|
|
3406
3459
|
|
|
@@ -3418,37 +3471,42 @@ async function createComponentCommand(componentName, options) {
|
|
|
3418
3471
|
const componentContent = generateComponentTemplate(normalizedName, framework, config.naming.componentExtension);
|
|
3419
3472
|
const signature = getSignature(config, config.naming.componentExtension === '.astro' ? 'astro' : 'tsx');
|
|
3420
3473
|
|
|
3421
|
-
const
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3474
|
+
const writtenFiles = [];
|
|
3475
|
+
|
|
3476
|
+
if (!componentExists || options.force) {
|
|
3477
|
+
const componentHash = await writeFileWithSignature(
|
|
3478
|
+
componentFilePath,
|
|
3479
|
+
componentContent,
|
|
3480
|
+
signature,
|
|
3481
|
+
config.hashing?.normalization
|
|
3482
|
+
);
|
|
3483
|
+
await registerFile(componentFilePath, {
|
|
3484
|
+
kind: 'component',
|
|
3485
|
+
template: 'component',
|
|
3486
|
+
hash: componentHash,
|
|
3487
|
+
owner: normalizedName
|
|
3488
|
+
});
|
|
3489
|
+
writtenFiles.push(componentFilePath);
|
|
3490
|
+
}
|
|
3433
3491
|
|
|
3434
|
-
|
|
3435
|
-
|
|
3436
|
-
|
|
3437
|
-
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3492
|
+
if (!existsSync(indexFilePath) || options.force) {
|
|
3493
|
+
const indexContent = generateIndexTemplate(normalizedName, config.naming.componentExtension);
|
|
3494
|
+
const indexHash = await writeFileWithSignature(
|
|
3495
|
+
indexFilePath,
|
|
3496
|
+
indexContent,
|
|
3497
|
+
getSignature(config, 'typescript'),
|
|
3498
|
+
config.hashing?.normalization
|
|
3499
|
+
);
|
|
3500
|
+
await registerFile(indexFilePath, {
|
|
3501
|
+
kind: 'component-file',
|
|
3502
|
+
template: 'index',
|
|
3503
|
+
hash: indexHash,
|
|
3504
|
+
owner: normalizedName
|
|
3505
|
+
});
|
|
3506
|
+
writtenFiles.push(indexFilePath);
|
|
3507
|
+
}
|
|
3450
3508
|
|
|
3451
|
-
if (shouldCreateTypes) {
|
|
3509
|
+
if (shouldCreateTypes && (!existsSync(typesFilePath) || options.force)) {
|
|
3452
3510
|
const typesContent = generateTypesTemplate(normalizedName);
|
|
3453
3511
|
const hash = await writeFileWithSignature(
|
|
3454
3512
|
typesFilePath,
|
|
@@ -3465,7 +3523,7 @@ async function createComponentCommand(componentName, options) {
|
|
|
3465
3523
|
writtenFiles.push(typesFilePath);
|
|
3466
3524
|
}
|
|
3467
3525
|
|
|
3468
|
-
if (shouldCreateContext) {
|
|
3526
|
+
if (shouldCreateContext && (!existsSync(contextFilePath) || options.force)) {
|
|
3469
3527
|
const contextContent = generateContextTemplate(normalizedName);
|
|
3470
3528
|
const hash = await writeFileWithSignature(
|
|
3471
3529
|
contextFilePath,
|
|
@@ -3482,7 +3540,7 @@ async function createComponentCommand(componentName, options) {
|
|
|
3482
3540
|
writtenFiles.push(contextFilePath);
|
|
3483
3541
|
}
|
|
3484
3542
|
|
|
3485
|
-
if (shouldCreateHook) {
|
|
3543
|
+
if (shouldCreateHook && (!existsSync(hookFilePath) || options.force)) {
|
|
3486
3544
|
const hookName = getHookFunctionName(normalizedName);
|
|
3487
3545
|
const hookContent = generateHookTemplate(normalizedName, hookName);
|
|
3488
3546
|
const hash = await writeFileWithSignature(
|
|
@@ -3500,7 +3558,7 @@ async function createComponentCommand(componentName, options) {
|
|
|
3500
3558
|
writtenFiles.push(hookFilePath);
|
|
3501
3559
|
}
|
|
3502
3560
|
|
|
3503
|
-
if (shouldCreateTests) {
|
|
3561
|
+
if (shouldCreateTests && (!existsSync(testFilePath) || options.force)) {
|
|
3504
3562
|
const relativeComponentPath = `../${normalizedName}${config.naming.componentExtension}`;
|
|
3505
3563
|
const testContent = generateTestTemplate(normalizedName, relativeComponentPath);
|
|
3506
3564
|
const hash = await writeFileWithSignature(
|
|
@@ -3518,7 +3576,7 @@ async function createComponentCommand(componentName, options) {
|
|
|
3518
3576
|
writtenFiles.push(testFilePath);
|
|
3519
3577
|
}
|
|
3520
3578
|
|
|
3521
|
-
if (shouldCreateConfig) {
|
|
3579
|
+
if (shouldCreateConfig && (!existsSync(configFilePath) || options.force)) {
|
|
3522
3580
|
const configContent = generateConfigTemplate(normalizedName);
|
|
3523
3581
|
const hash = await writeFileWithSignature(
|
|
3524
3582
|
configFilePath,
|
|
@@ -3535,7 +3593,7 @@ async function createComponentCommand(componentName, options) {
|
|
|
3535
3593
|
writtenFiles.push(configFilePath);
|
|
3536
3594
|
}
|
|
3537
3595
|
|
|
3538
|
-
if (shouldCreateConstants) {
|
|
3596
|
+
if (shouldCreateConstants && (!existsSync(constantsFilePath) || options.force)) {
|
|
3539
3597
|
const constantsContent = generateConstantsTemplate(normalizedName);
|
|
3540
3598
|
const hash = await writeFileWithSignature(
|
|
3541
3599
|
constantsFilePath,
|
|
@@ -3552,7 +3610,7 @@ async function createComponentCommand(componentName, options) {
|
|
|
3552
3610
|
writtenFiles.push(constantsFilePath);
|
|
3553
3611
|
}
|
|
3554
3612
|
|
|
3555
|
-
if (shouldCreateApi) {
|
|
3613
|
+
if (shouldCreateApi && (!existsSync(apiFilePath) || options.force)) {
|
|
3556
3614
|
const apiContent = generateApiTemplate(normalizedName);
|
|
3557
3615
|
const hash = await writeFileWithSignature(
|
|
3558
3616
|
apiFilePath,
|
|
@@ -3569,7 +3627,7 @@ async function createComponentCommand(componentName, options) {
|
|
|
3569
3627
|
writtenFiles.push(apiFilePath);
|
|
3570
3628
|
}
|
|
3571
3629
|
|
|
3572
|
-
if (shouldCreateServices) {
|
|
3630
|
+
if (shouldCreateServices && (!existsSync(servicesFilePath) || options.force)) {
|
|
3573
3631
|
const servicesContent = generateServiceTemplate(normalizedName);
|
|
3574
3632
|
const hash = await writeFileWithSignature(
|
|
3575
3633
|
servicesFilePath,
|
|
@@ -3586,7 +3644,7 @@ async function createComponentCommand(componentName, options) {
|
|
|
3586
3644
|
writtenFiles.push(servicesFilePath);
|
|
3587
3645
|
}
|
|
3588
3646
|
|
|
3589
|
-
if (shouldCreateSchemas) {
|
|
3647
|
+
if (shouldCreateSchemas && (!existsSync(schemasFilePath) || options.force)) {
|
|
3590
3648
|
const schemasContent = generateSchemaTemplate(normalizedName);
|
|
3591
3649
|
const hash = await writeFileWithSignature(
|
|
3592
3650
|
schemasFilePath,
|
|
@@ -3603,7 +3661,7 @@ async function createComponentCommand(componentName, options) {
|
|
|
3603
3661
|
writtenFiles.push(schemasFilePath);
|
|
3604
3662
|
}
|
|
3605
3663
|
|
|
3606
|
-
if (shouldCreateReadme) {
|
|
3664
|
+
if (shouldCreateReadme && (!existsSync(readmeFilePath) || options.force)) {
|
|
3607
3665
|
const readmeContent = generateReadmeTemplate(normalizedName);
|
|
3608
3666
|
const hash = await writeFileWithSignature(
|
|
3609
3667
|
readmeFilePath,
|
|
@@ -3620,7 +3678,7 @@ async function createComponentCommand(componentName, options) {
|
|
|
3620
3678
|
writtenFiles.push(readmeFilePath);
|
|
3621
3679
|
}
|
|
3622
3680
|
|
|
3623
|
-
if (shouldCreateStories) {
|
|
3681
|
+
if (shouldCreateStories && (!existsSync(storiesFilePath) || options.force)) {
|
|
3624
3682
|
const relativePath = `./${normalizedName}${config.naming.componentExtension}`;
|
|
3625
3683
|
const storiesContent = generateStoriesTemplate(normalizedName, relativePath);
|
|
3626
3684
|
const hash = await writeFileWithSignature(
|
|
@@ -4551,6 +4609,166 @@ async function pruneMissingCommand(options = {}) {
|
|
|
4551
4609
|
}
|
|
4552
4610
|
}
|
|
4553
4611
|
|
|
4612
|
+
/**
|
|
4613
|
+
* Add a new item (hook, api, service, etc.) to an existing feature or component.
|
|
4614
|
+
*
|
|
4615
|
+
* @param {string} itemType The type of item to add (e.g., 'api', 'hook', 'service')
|
|
4616
|
+
* @param {string} targetName The name of the feature or component
|
|
4617
|
+
* @param {Object} options Additional options from Commander
|
|
4618
|
+
*/
|
|
4619
|
+
async function addItemCommand(itemType, targetName, options) {
|
|
4620
|
+
try {
|
|
4621
|
+
const state = await loadState();
|
|
4622
|
+
|
|
4623
|
+
// Normalize itemType
|
|
4624
|
+
let normalizedItem = itemType.toLowerCase();
|
|
4625
|
+
if (normalizedItem === 'test') normalizedItem = 'tests';
|
|
4626
|
+
if (normalizedItem === 'service') normalizedItem = 'services';
|
|
4627
|
+
if (normalizedItem === 'schema') normalizedItem = 'schemas';
|
|
4628
|
+
if (normalizedItem === 'hook') normalizedItem = 'hooks'; // for add-section
|
|
4629
|
+
|
|
4630
|
+
// Try to find as section (feature) first
|
|
4631
|
+
let section = findSection(state, targetName);
|
|
4632
|
+
let component = findComponent(state, targetName);
|
|
4633
|
+
|
|
4634
|
+
// If not found by exact name, try to find by featurePath or part of it
|
|
4635
|
+
if (!section && !component) {
|
|
4636
|
+
section = state.sections.find(s => s.featurePath === targetName || s.featurePath.endsWith('/' + targetName));
|
|
4637
|
+
}
|
|
4638
|
+
|
|
4639
|
+
if (!section && !component) {
|
|
4640
|
+
throw new Error(`Target not found in state: "${targetName}". Please use "add-section" or "create-component" directly if it's not managed by Textor.`);
|
|
4641
|
+
}
|
|
4642
|
+
|
|
4643
|
+
const flags = { [normalizedItem]: true };
|
|
4644
|
+
// Also set singular for create-component which uses 'hook'
|
|
4645
|
+
if (normalizedItem === 'hooks') flags.hook = true;
|
|
4646
|
+
|
|
4647
|
+
if (section) {
|
|
4648
|
+
console.log(`ℹ Adding ${normalizedItem} to feature: ${section.featurePath}`);
|
|
4649
|
+
return await addSectionCommand(undefined, section.featurePath, { ...options, ...flags });
|
|
4650
|
+
}
|
|
4651
|
+
|
|
4652
|
+
if (component) {
|
|
4653
|
+
console.log(`ℹ Adding ${normalizedItem} to component: ${component.name}`);
|
|
4654
|
+
// For create-component, we might need to be careful with flags that are on by default
|
|
4655
|
+
// but getEffectiveOptions should handle it if we pass them explicitly as true.
|
|
4656
|
+
return await createComponentCommand(component.name, { ...options, ...flags });
|
|
4657
|
+
}
|
|
4658
|
+
} catch (error) {
|
|
4659
|
+
console.error('Error:', error.message);
|
|
4660
|
+
if (typeof process.exit === 'function' && process.env.NODE_ENV !== 'test') {
|
|
4661
|
+
process.exit(1);
|
|
4662
|
+
}
|
|
4663
|
+
throw error;
|
|
4664
|
+
}
|
|
4665
|
+
}
|
|
4666
|
+
|
|
4667
|
+
/**
|
|
4668
|
+
* Dispatcher for rename commands.
|
|
4669
|
+
*/
|
|
4670
|
+
async function renameCommand(type, oldName, newName, options) {
|
|
4671
|
+
try {
|
|
4672
|
+
if (!type || !oldName || !newName) {
|
|
4673
|
+
throw new Error('Usage: textor rename <route|feature|component> <oldName> <newName>');
|
|
4674
|
+
}
|
|
4675
|
+
|
|
4676
|
+
if (type === 'route' || type === 'path') {
|
|
4677
|
+
const normalizedOld = normalizeRoute(oldName);
|
|
4678
|
+
const normalizedNew = normalizeRoute(newName);
|
|
4679
|
+
// By default, move-section will try to move the feature if it matches the route.
|
|
4680
|
+
// For a simple "rename route", we might want to keep that behavior or not.
|
|
4681
|
+
// Usually "rename route" means just the URL/file.
|
|
4682
|
+
return await moveSectionCommand(normalizedOld, undefined, normalizedNew, undefined, options);
|
|
4683
|
+
}
|
|
4684
|
+
|
|
4685
|
+
if (type === 'feature') {
|
|
4686
|
+
const state = await loadState();
|
|
4687
|
+
const normalizedOld = featureToDirectoryPath(oldName);
|
|
4688
|
+
const normalizedNew = featureToDirectoryPath(newName);
|
|
4689
|
+
|
|
4690
|
+
const section = findSection(state, normalizedOld);
|
|
4691
|
+
|
|
4692
|
+
if (section) {
|
|
4693
|
+
// If it's a managed section, move it using section logic
|
|
4694
|
+
return await moveSectionCommand(section.route, section.featurePath, section.route, normalizedNew, options);
|
|
4695
|
+
} else {
|
|
4696
|
+
// Standalone feature move
|
|
4697
|
+
return await moveSectionCommand(undefined, normalizedOld, undefined, normalizedNew, options);
|
|
4698
|
+
}
|
|
4699
|
+
}
|
|
4700
|
+
|
|
4701
|
+
if (type === 'component') {
|
|
4702
|
+
return await renameComponent(oldName, newName, options);
|
|
4703
|
+
}
|
|
4704
|
+
|
|
4705
|
+
throw new Error(`Unknown rename type: ${type}. Supported types: route, feature, component.`);
|
|
4706
|
+
} catch (error) {
|
|
4707
|
+
console.error('Error:', error.message);
|
|
4708
|
+
if (typeof process.exit === 'function' && process.env.NODE_ENV !== 'test') {
|
|
4709
|
+
process.exit(1);
|
|
4710
|
+
}
|
|
4711
|
+
throw error;
|
|
4712
|
+
}
|
|
4713
|
+
}
|
|
4714
|
+
|
|
4715
|
+
/**
|
|
4716
|
+
* Specialized logic for renaming shared components.
|
|
4717
|
+
*/
|
|
4718
|
+
async function renameComponent(oldName, newName, options) {
|
|
4719
|
+
const config = await loadConfig();
|
|
4720
|
+
const state = await loadState();
|
|
4721
|
+
|
|
4722
|
+
const normalizedOldName = normalizeComponentName(oldName);
|
|
4723
|
+
const normalizedNewName = normalizeComponentName(newName);
|
|
4724
|
+
|
|
4725
|
+
const component = findComponent(state, normalizedOldName);
|
|
4726
|
+
|
|
4727
|
+
const componentsRoot = resolvePath(config, 'components');
|
|
4728
|
+
const fromPath = component
|
|
4729
|
+
? path.resolve(process.cwd(), component.path)
|
|
4730
|
+
: path.join(componentsRoot, normalizedOldName);
|
|
4731
|
+
|
|
4732
|
+
const toPath = path.join(componentsRoot, normalizedNewName);
|
|
4733
|
+
|
|
4734
|
+
if (options.dryRun) {
|
|
4735
|
+
console.log(`Dry run - would rename component: ${normalizedOldName} -> ${normalizedNewName}`);
|
|
4736
|
+
console.log(` Path: ${fromPath} -> ${toPath}`);
|
|
4737
|
+
return;
|
|
4738
|
+
}
|
|
4739
|
+
|
|
4740
|
+
const signatures = Object.values(config.signatures || {});
|
|
4741
|
+
|
|
4742
|
+
await moveDirectory(fromPath, toPath, state, config, {
|
|
4743
|
+
...options,
|
|
4744
|
+
fromName: normalizedOldName,
|
|
4745
|
+
toName: normalizedNewName,
|
|
4746
|
+
signatures
|
|
4747
|
+
});
|
|
4748
|
+
|
|
4749
|
+
if (options.scan) {
|
|
4750
|
+
await scanAndReplaceImports(config, state, {
|
|
4751
|
+
fromPath: normalizedOldName,
|
|
4752
|
+
fromName: normalizedOldName,
|
|
4753
|
+
type: 'component'
|
|
4754
|
+
}, {
|
|
4755
|
+
toPath: normalizedNewName,
|
|
4756
|
+
toName: normalizedNewName
|
|
4757
|
+
}, options);
|
|
4758
|
+
}
|
|
4759
|
+
|
|
4760
|
+
await cleanupEmptyDirs(path.dirname(fromPath), componentsRoot);
|
|
4761
|
+
|
|
4762
|
+
// Update state metadata
|
|
4763
|
+
if (component) {
|
|
4764
|
+
component.name = normalizedNewName;
|
|
4765
|
+
component.path = path.relative(process.cwd(), toPath).replace(/\\/g, '/');
|
|
4766
|
+
}
|
|
4767
|
+
|
|
4768
|
+
await saveState(state);
|
|
4769
|
+
console.log(`✓ Renamed component ${normalizedOldName} to ${normalizedNewName}`);
|
|
4770
|
+
}
|
|
4771
|
+
|
|
4554
4772
|
const program = new Command();
|
|
4555
4773
|
|
|
4556
4774
|
program
|
|
@@ -4696,5 +4914,21 @@ program
|
|
|
4696
4914
|
.option('--no-interactive', 'Disable interactive prompts')
|
|
4697
4915
|
.action(pruneMissingCommand);
|
|
4698
4916
|
|
|
4917
|
+
program
|
|
4918
|
+
.command('rename <type> <oldName> <newName>')
|
|
4919
|
+
.description('Rename a route, feature, or component')
|
|
4920
|
+
.option('--scan', 'Enable repo-wide import updates')
|
|
4921
|
+
.option('--force', 'Rename even if files are modified or not generated by Textor')
|
|
4922
|
+
.option('--accept-changes', 'Allow renaming of modified files')
|
|
4923
|
+
.option('--dry-run', 'Show what would be renamed without applying')
|
|
4924
|
+
.action(renameCommand);
|
|
4925
|
+
|
|
4926
|
+
program
|
|
4927
|
+
.command('add <item> <target>')
|
|
4928
|
+
.description('Add a sub-item (api, hook, test, etc.) to an existing feature or component')
|
|
4929
|
+
.option('--force', 'Overwrite existing files')
|
|
4930
|
+
.option('--dry-run', 'Show what would be created without creating')
|
|
4931
|
+
.action(addItemCommand);
|
|
4932
|
+
|
|
4699
4933
|
program.parse();
|
|
4700
4934
|
//# sourceMappingURL=textor.js.map
|