@oamm/textor 1.0.7 ā 1.0.9
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 +42 -1
- package/dist/bin/textor.js +288 -113
- package/dist/bin/textor.js.map +1 -1
- package/dist/index.cjs +189 -104
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +189 -104
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -279,6 +279,20 @@ pnpm textor normalize-state
|
|
|
279
279
|
**Options:**
|
|
280
280
|
- `--dry-run`: Print the normalized state without writing it.
|
|
281
281
|
|
|
282
|
+
### prune-missing
|
|
283
|
+
Remove missing references from Textor state. This command identifies files that are tracked in the state but are no longer present on disk and removes them from `.textor/state.json`.
|
|
284
|
+
|
|
285
|
+
**Safe-by-default**: This command never deletes any files from your disk; it only updates the state tracker.
|
|
286
|
+
|
|
287
|
+
```bash
|
|
288
|
+
pnpm textor prune-missing
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
**Options:**
|
|
292
|
+
- `--dry-run`: Preview what would be removed from the state without making changes.
|
|
293
|
+
- `--yes`: Skip confirmation prompt.
|
|
294
|
+
- `--no-interactive`: Disable interactive prompts (useful for CI).
|
|
295
|
+
|
|
282
296
|
## šļø Technical Architecture
|
|
283
297
|
|
|
284
298
|
Textor is designed with enterprise-grade robustness, moving beyond simple scaffolding to provide a reliable refactoring engine.
|
|
@@ -341,6 +355,8 @@ The .textor/config.json file allows full control over the tool's behavior.
|
|
|
341
355
|
"indexFile": "index.astro"
|
|
342
356
|
},
|
|
343
357
|
"importAliases": {
|
|
358
|
+
"layouts": "@/layouts",
|
|
359
|
+
"features": "@/features"
|
|
344
360
|
},
|
|
345
361
|
"naming": {
|
|
346
362
|
"routeExtension": ".astro",
|
|
@@ -459,6 +475,31 @@ The .textor/config.json file allows full control over the tool's behavior.
|
|
|
459
475
|
```
|
|
460
476
|
*Supported formatting tools: prettier, biome, none.*
|
|
461
477
|
|
|
478
|
+
### 7. Layout Parameters
|
|
479
|
+
|
|
480
|
+
You can pass parameters to your layout component by defining `layoutProps` in `.textor/config.json`. These props support variable substitution.
|
|
481
|
+
|
|
482
|
+
```json
|
|
483
|
+
{
|
|
484
|
+
"features": {
|
|
485
|
+
"layout": "AppLayout",
|
|
486
|
+
"layoutProps": {
|
|
487
|
+
"title": "{{componentName}}",
|
|
488
|
+
"description": "Description for {{componentName}}"
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
You can also override these props via the CLI using the `--prop` flag:
|
|
495
|
+
```bash
|
|
496
|
+
pnpm textor add-section /users users/roles --prop title="Custom Title" --prop breadcrumbs='{[{ label: "Users" }]}'
|
|
497
|
+
```
|
|
498
|
+
|
|
499
|
+
Properties that start and end with curly braces `{}` are passed as JavaScript expressions, others as strings.
|
|
500
|
+
|
|
501
|
+
---
|
|
502
|
+
|
|
462
503
|
## š Template Overrides
|
|
463
504
|
|
|
464
505
|
You can customize the code generated by Textor by providing your own templates. Textor looks for override files in the `.textor/templates/` directory at your project root.
|
|
@@ -473,7 +514,7 @@ You can customize the code generated by Textor by providing your own templates.
|
|
|
473
514
|
|
|
474
515
|
| Template Name | File to create in `.textor/templates/` | Available Variables |
|
|
475
516
|
| :--- | :--- | :--- |
|
|
476
|
-
| **Route** | `route.astro` | `{{layoutName}}`, `{{layoutImportPath}}`, `{{featureImportPath}}`, `{{featureComponentName}}` |
|
|
517
|
+
| **Route** | `route.astro` | `{{layoutName}}`, `{{layoutImportPath}}`, `{{featureImportPath}}`, `{{featureComponentName}}`, plus any `layoutProps` |
|
|
477
518
|
| **Feature** | `feature.astro` or `feature.tsx` | `{{componentName}}`, `{{scriptImportPath}}` |
|
|
478
519
|
| **Component** | `component.astro` or `component.tsx` | `{{componentName}}` |
|
|
479
520
|
| **Hook** | `hook.ts` | `{{componentName}}`, `{{hookName}}` |
|
package/dist/bin/textor.js
CHANGED
|
@@ -6,6 +6,7 @@ import path from 'path';
|
|
|
6
6
|
import { createHash } from 'crypto';
|
|
7
7
|
import { exec } from 'child_process';
|
|
8
8
|
import { promisify } from 'util';
|
|
9
|
+
import readline from 'readline';
|
|
9
10
|
|
|
10
11
|
const CONFIG_DIR$1 = '.textor';
|
|
11
12
|
const CONFIG_FILE = 'config.json';
|
|
@@ -42,6 +43,7 @@ const CURRENT_CONFIG_VERSION = 2;
|
|
|
42
43
|
* @property {boolean} features.createScriptsDir
|
|
43
44
|
* @property {string} features.scriptsIndexFile
|
|
44
45
|
* @property {string} features.layout
|
|
46
|
+
* @property {Object} features.layoutProps
|
|
45
47
|
* @property {Object} components
|
|
46
48
|
* @property {boolean} components.createSubComponentsDir
|
|
47
49
|
* @property {boolean} components.createContext
|
|
@@ -108,7 +110,8 @@ const DEFAULT_CONFIG = {
|
|
|
108
110
|
createReadme: false,
|
|
109
111
|
createStories: false,
|
|
110
112
|
createIndex: false,
|
|
111
|
-
layout: 'Main'
|
|
113
|
+
layout: 'Main',
|
|
114
|
+
layoutProps: {}
|
|
112
115
|
},
|
|
113
116
|
components: {
|
|
114
117
|
framework: 'react',
|
|
@@ -1020,27 +1023,6 @@ async function formatFiles(filePaths, tool) {
|
|
|
1020
1023
|
}
|
|
1021
1024
|
}
|
|
1022
1025
|
|
|
1023
|
-
var filesystem = /*#__PURE__*/Object.freeze({
|
|
1024
|
-
__proto__: null,
|
|
1025
|
-
calculateHash: calculateHash,
|
|
1026
|
-
cleanupEmptyDirs: cleanupEmptyDirs,
|
|
1027
|
-
ensureDir: ensureDir,
|
|
1028
|
-
ensureNotExists: ensureNotExists,
|
|
1029
|
-
formatFiles: formatFiles,
|
|
1030
|
-
getSignature: getSignature,
|
|
1031
|
-
inferKind: inferKind,
|
|
1032
|
-
isEmptyDir: isEmptyDir,
|
|
1033
|
-
isTextorGenerated: isTextorGenerated,
|
|
1034
|
-
safeDelete: safeDelete,
|
|
1035
|
-
safeDeleteDir: safeDeleteDir,
|
|
1036
|
-
safeMove: safeMove,
|
|
1037
|
-
scanDirectory: scanDirectory,
|
|
1038
|
-
secureJoin: secureJoin,
|
|
1039
|
-
updateSignature: updateSignature,
|
|
1040
|
-
verifyFileIntegrity: verifyFileIntegrity,
|
|
1041
|
-
writeFileWithSignature: writeFileWithSignature
|
|
1042
|
-
});
|
|
1043
|
-
|
|
1044
1026
|
function renderNamePattern(pattern, data = {}, label = 'pattern') {
|
|
1045
1027
|
if (typeof pattern !== 'string') return null;
|
|
1046
1028
|
const trimmed = pattern.trim();
|
|
@@ -1108,13 +1090,15 @@ function getTemplateOverride(templateName, extension, data = {}) {
|
|
|
1108
1090
|
* - layoutImportPath: Path to import the layout
|
|
1109
1091
|
* - featureImportPath: Path to import the feature component
|
|
1110
1092
|
* - featureComponentName: Name of the feature component
|
|
1093
|
+
* - layoutProps: Optional properties for the layout
|
|
1111
1094
|
*/
|
|
1112
|
-
function generateRouteTemplate(layoutName, layoutImportPath, featureImportPath, featureComponentName, extension = '.astro') {
|
|
1095
|
+
function generateRouteTemplate(layoutName, layoutImportPath, featureImportPath, featureComponentName, extension = '.astro', layoutProps = {}) {
|
|
1113
1096
|
const override = getTemplateOverride('route', extension, {
|
|
1114
1097
|
layoutName,
|
|
1115
1098
|
layoutImportPath,
|
|
1116
1099
|
featureImportPath,
|
|
1117
|
-
featureComponentName
|
|
1100
|
+
featureComponentName,
|
|
1101
|
+
...layoutProps
|
|
1118
1102
|
});
|
|
1119
1103
|
if (override) return override;
|
|
1120
1104
|
|
|
@@ -1127,12 +1111,26 @@ import ${featureComponentName} from '${featureImportPath}';
|
|
|
1127
1111
|
`;
|
|
1128
1112
|
}
|
|
1129
1113
|
|
|
1114
|
+
const propsStr = Object.entries(layoutProps)
|
|
1115
|
+
.map(([key, value]) => {
|
|
1116
|
+
if (typeof value === 'string' && value.startsWith('{') && value.endsWith('}')) {
|
|
1117
|
+
return `${key}=${value}`;
|
|
1118
|
+
}
|
|
1119
|
+
if (typeof value === 'string') {
|
|
1120
|
+
return `${key}="${value}"`;
|
|
1121
|
+
}
|
|
1122
|
+
return `${key}={${JSON.stringify(value)}}`;
|
|
1123
|
+
})
|
|
1124
|
+
.join(' ');
|
|
1125
|
+
|
|
1126
|
+
const layoutOpening = propsStr ? `<${layoutName} ${propsStr}>` : `<${layoutName}>`;
|
|
1127
|
+
|
|
1130
1128
|
return `---
|
|
1131
1129
|
import ${layoutName} from '${layoutImportPath}';
|
|
1132
1130
|
import ${featureComponentName} from '${featureImportPath}';
|
|
1133
1131
|
---
|
|
1134
1132
|
|
|
1135
|
-
|
|
1133
|
+
${layoutOpening}
|
|
1136
1134
|
<${featureComponentName} />
|
|
1137
1135
|
</${layoutName}>
|
|
1138
1136
|
`;
|
|
@@ -1343,6 +1341,11 @@ function generateIndexTemplate(componentName, componentExtension) {
|
|
|
1343
1341
|
const override = getTemplateOverride('index', '.ts', { componentName, componentExtension });
|
|
1344
1342
|
if (override) return override;
|
|
1345
1343
|
|
|
1344
|
+
if (componentExtension === '.astro') {
|
|
1345
|
+
return `export * from './types';
|
|
1346
|
+
`;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1346
1349
|
return `export { default as ${componentName} } from './${componentName}${componentExtension}';
|
|
1347
1350
|
export * from './types';
|
|
1348
1351
|
`;
|
|
@@ -1798,6 +1801,7 @@ async function addSectionCommand(route, featurePath, options) {
|
|
|
1798
1801
|
const {
|
|
1799
1802
|
framework,
|
|
1800
1803
|
layout,
|
|
1804
|
+
layoutProps: configLayoutProps,
|
|
1801
1805
|
createSubComponentsDir: shouldCreateSubComponentsDir,
|
|
1802
1806
|
createScriptsDir: shouldCreateScriptsDir,
|
|
1803
1807
|
createApi: shouldCreateApi,
|
|
@@ -1969,7 +1973,7 @@ async function addSectionCommand(route, featurePath, options) {
|
|
|
1969
1973
|
}
|
|
1970
1974
|
|
|
1971
1975
|
// Update imports in the moved file
|
|
1972
|
-
await updateImportsInFile$
|
|
1976
|
+
await updateImportsInFile$2(reorg.to, reorg.from, reorg.to);
|
|
1973
1977
|
|
|
1974
1978
|
// Update hash in state after import updates
|
|
1975
1979
|
if (state.files[newRelative]) {
|
|
@@ -1998,6 +2002,32 @@ async function addSectionCommand(route, featurePath, options) {
|
|
|
1998
2002
|
if (shouldCreateScriptsDir) await ensureNotExists(scriptsIndexPath, options.force);
|
|
1999
2003
|
|
|
2000
2004
|
let layoutImportPath = null;
|
|
2005
|
+
const cliProps = options.prop || {};
|
|
2006
|
+
const rawLayoutProps = { ...configLayoutProps, ...cliProps };
|
|
2007
|
+
const layoutProps = {};
|
|
2008
|
+
|
|
2009
|
+
// Resolve variables in layoutProps
|
|
2010
|
+
const substitutionData = enrichData({
|
|
2011
|
+
componentName: featureComponentName,
|
|
2012
|
+
layoutName: layout,
|
|
2013
|
+
featureComponentName: featureComponentName
|
|
2014
|
+
});
|
|
2015
|
+
|
|
2016
|
+
for (const [key, value] of Object.entries(rawLayoutProps)) {
|
|
2017
|
+
if (typeof value === 'string') {
|
|
2018
|
+
let resolvedValue = value;
|
|
2019
|
+
for (const [varKey, varValue] of Object.entries(substitutionData)) {
|
|
2020
|
+
const regex = new RegExp(`{{${varKey}}}`, 'g');
|
|
2021
|
+
resolvedValue = resolvedValue.replace(regex, varValue);
|
|
2022
|
+
const underscoreRegex = new RegExp(`__${varKey}__`, 'g');
|
|
2023
|
+
resolvedValue = resolvedValue.replace(underscoreRegex, varValue);
|
|
2024
|
+
}
|
|
2025
|
+
layoutProps[key] = resolvedValue;
|
|
2026
|
+
} else {
|
|
2027
|
+
layoutProps[key] = value;
|
|
2028
|
+
}
|
|
2029
|
+
}
|
|
2030
|
+
|
|
2001
2031
|
if (routeFilePath && layout !== 'none') {
|
|
2002
2032
|
if (config.importAliases.layouts) {
|
|
2003
2033
|
layoutImportPath = `${config.importAliases.layouts}/${layout}.astro`;
|
|
@@ -2010,7 +2040,7 @@ async function addSectionCommand(route, featurePath, options) {
|
|
|
2010
2040
|
let featureImportPath = null;
|
|
2011
2041
|
if (routeFilePath) {
|
|
2012
2042
|
if (config.importAliases.features) {
|
|
2013
|
-
const entryPart = effectiveOptions.entry === 'index' ? '' : `/${featureComponentName}`;
|
|
2043
|
+
const entryPart = effectiveOptions.entry === 'index' ? '/index' : `/${featureComponentName}`;
|
|
2014
2044
|
// In Astro, we can often omit the extension for .tsx files, but not for .astro files if using aliases sometimes.
|
|
2015
2045
|
// However, to be safe, we use the configured extension.
|
|
2016
2046
|
featureImportPath = `${config.importAliases.features}/${normalizedFeaturePath}${entryPart}${config.naming.featureExtension}`;
|
|
@@ -2043,7 +2073,8 @@ async function addSectionCommand(route, featurePath, options) {
|
|
|
2043
2073
|
layoutImportPath,
|
|
2044
2074
|
featureImportPath,
|
|
2045
2075
|
featureComponentName,
|
|
2046
|
-
routeExtension
|
|
2076
|
+
routeExtension,
|
|
2077
|
+
layoutProps
|
|
2047
2078
|
);
|
|
2048
2079
|
routeSignature = getSignature(config, 'astro');
|
|
2049
2080
|
}
|
|
@@ -2328,7 +2359,7 @@ async function addSectionCommand(route, featurePath, options) {
|
|
|
2328
2359
|
}
|
|
2329
2360
|
}
|
|
2330
2361
|
|
|
2331
|
-
async function updateImportsInFile$
|
|
2362
|
+
async function updateImportsInFile$2(filePath, oldFilePath, newFilePath) {
|
|
2332
2363
|
if (!existsSync(filePath)) return;
|
|
2333
2364
|
|
|
2334
2365
|
let content = await readFile(filePath, 'utf-8');
|
|
@@ -2538,7 +2569,7 @@ async function removeSectionCommand(route, featurePath, options) {
|
|
|
2538
2569
|
state.files[newRelative] = { ...state.files[oldRelative] };
|
|
2539
2570
|
delete state.files[oldRelative];
|
|
2540
2571
|
|
|
2541
|
-
await updateImportsInFile(flatFilePath, loneFilePath, flatFilePath);
|
|
2572
|
+
await updateImportsInFile$1(flatFilePath, loneFilePath, flatFilePath);
|
|
2542
2573
|
|
|
2543
2574
|
// Update hash in state after import updates
|
|
2544
2575
|
const content = await readFile(flatFilePath, 'utf-8');
|
|
@@ -2568,7 +2599,7 @@ async function removeSectionCommand(route, featurePath, options) {
|
|
|
2568
2599
|
}
|
|
2569
2600
|
}
|
|
2570
2601
|
|
|
2571
|
-
async function updateImportsInFile(filePath, oldFilePath, newFilePath) {
|
|
2602
|
+
async function updateImportsInFile$1(filePath, oldFilePath, newFilePath) {
|
|
2572
2603
|
if (!existsSync(filePath)) return;
|
|
2573
2604
|
|
|
2574
2605
|
let content = await readFile(filePath, 'utf-8');
|
|
@@ -2741,9 +2772,14 @@ async function moveSectionCommand(fromRoute, fromFeature, toRoute, toFeature, op
|
|
|
2741
2772
|
const fromFeatureComponentName = getFeatureComponentName(normalizedFromFeature);
|
|
2742
2773
|
const toFeatureComponentName = getFeatureComponentName(targetFeature);
|
|
2743
2774
|
|
|
2744
|
-
//
|
|
2775
|
+
// First, update all relative imports in the file because it moved
|
|
2776
|
+
await updateImportsInFile(toRoutePath, fromRoutePath, toRoutePath);
|
|
2777
|
+
|
|
2778
|
+
let content = await readFile(toRoutePath, 'utf-8');
|
|
2779
|
+
let changed = false;
|
|
2780
|
+
|
|
2781
|
+
// Update component name in JSX tags
|
|
2745
2782
|
if (fromFeatureComponentName !== toFeatureComponentName) {
|
|
2746
|
-
let content = await readFile(toRoutePath, 'utf-8');
|
|
2747
2783
|
content = content.replace(
|
|
2748
2784
|
new RegExp(`<${fromFeatureComponentName}`, 'g'),
|
|
2749
2785
|
`<${toFeatureComponentName}`
|
|
@@ -2752,44 +2788,54 @@ async function moveSectionCommand(fromRoute, fromFeature, toRoute, toFeature, op
|
|
|
2752
2788
|
new RegExp(`</${fromFeatureComponentName}`, 'g'),
|
|
2753
2789
|
`</${toFeatureComponentName}`
|
|
2754
2790
|
);
|
|
2755
|
-
|
|
2791
|
+
changed = true;
|
|
2756
2792
|
}
|
|
2757
2793
|
|
|
2758
2794
|
if (config.importAliases.features) {
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
`import ${toFeatureComponentName} from '${aliasPath}/${toFeatureComponentName}${ext}'`
|
|
2779
|
-
);
|
|
2795
|
+
const oldAliasPath = `${config.importAliases.features}/${normalizedFromFeature}`;
|
|
2796
|
+
const newAliasPath = `${config.importAliases.features}/${targetFeature}`;
|
|
2797
|
+
|
|
2798
|
+
// Flexible regex to match import identifier and path with alias
|
|
2799
|
+
const importRegex = new RegExp(`(import\\s+)(${fromFeatureComponentName})(\\s+from\\s+['"])${oldAliasPath}(/[^'"]+)?(['"])`, 'g');
|
|
2800
|
+
|
|
2801
|
+
if (importRegex.test(content)) {
|
|
2802
|
+
content = content.replace(importRegex, (match, p1, p2, p3, subPath, p5) => {
|
|
2803
|
+
let newSubPath = subPath || '';
|
|
2804
|
+
if (subPath && subPath.includes(fromFeatureComponentName)) {
|
|
2805
|
+
newSubPath = subPath.replace(fromFeatureComponentName, toFeatureComponentName);
|
|
2806
|
+
}
|
|
2807
|
+
return `${p1}${toFeatureComponentName}${p3}${newAliasPath}${newSubPath}${p5}`;
|
|
2808
|
+
});
|
|
2809
|
+
changed = true;
|
|
2810
|
+
} else if (content.includes(oldAliasPath)) {
|
|
2811
|
+
// Fallback for path only replacement
|
|
2812
|
+
content = content.replace(new RegExp(oldAliasPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newAliasPath);
|
|
2813
|
+
changed = true;
|
|
2780
2814
|
}
|
|
2781
2815
|
} else {
|
|
2782
|
-
const oldRelativeDir = getRelativeImportPath(
|
|
2816
|
+
const oldRelativeDir = getRelativeImportPath(toRoutePath, fromFeatureDirPath);
|
|
2783
2817
|
const newRelativeDir = getRelativeImportPath(toRoutePath, toFeatureDirPath);
|
|
2784
|
-
const ext = config.naming.featureExtension === '.astro' ? '.astro' : '';
|
|
2785
2818
|
|
|
2786
|
-
|
|
2787
|
-
const
|
|
2819
|
+
// Flexible regex for relative imports
|
|
2820
|
+
const relImportRegex = new RegExp(`(import\\s+)(${fromFeatureComponentName})(\\s+from\\s+['"])${oldRelativeDir}(/[^'"]+)?(['"])`, 'g');
|
|
2788
2821
|
|
|
2789
|
-
if (
|
|
2790
|
-
|
|
2822
|
+
if (relImportRegex.test(content)) {
|
|
2823
|
+
content = content.replace(relImportRegex, (match, p1, p2, p3, subPath, p5) => {
|
|
2824
|
+
let newSubPath = subPath || '';
|
|
2825
|
+
if (subPath && subPath.includes(fromFeatureComponentName)) {
|
|
2826
|
+
newSubPath = subPath.replace(fromFeatureComponentName, toFeatureComponentName);
|
|
2827
|
+
}
|
|
2828
|
+
return `${p1}${toFeatureComponentName}${p3}${newRelativeDir}${newSubPath}${p5}`;
|
|
2829
|
+
});
|
|
2830
|
+
changed = true;
|
|
2791
2831
|
}
|
|
2792
2832
|
}
|
|
2833
|
+
|
|
2834
|
+
if (changed) {
|
|
2835
|
+
await writeFile(toRoutePath, content, 'utf-8');
|
|
2836
|
+
// Update hash in state after changes
|
|
2837
|
+
state.files[normalizedToRouteRelative].hash = calculateHash(content, config.hashing?.normalization);
|
|
2838
|
+
}
|
|
2793
2839
|
}
|
|
2794
2840
|
|
|
2795
2841
|
if (!isRouteOnly && normalizedFromFeature && normalizedToFeature && normalizedFromFeature !== normalizedToFeature) {
|
|
@@ -2860,7 +2906,6 @@ async function scanAndReplaceImports(config, state, fromInfo, toInfo, options) {
|
|
|
2860
2906
|
const { toFeaturePath, toComponentName } = toInfo;
|
|
2861
2907
|
|
|
2862
2908
|
const allFiles = new Set();
|
|
2863
|
-
const { scanDirectory, calculateHash } = await Promise.resolve().then(function () { return filesystem; });
|
|
2864
2909
|
await scanDirectory(process.cwd(), allFiles);
|
|
2865
2910
|
|
|
2866
2911
|
const featuresRoot = resolvePath(config, 'features');
|
|
@@ -3001,7 +3046,6 @@ async function moveDirectory(fromPath, toPath, state, config, options = {}) {
|
|
|
3001
3046
|
if (hasChanged) {
|
|
3002
3047
|
await writeFile(toEntryPath, content, 'utf-8');
|
|
3003
3048
|
// Re-calculate hash after content update
|
|
3004
|
-
const { calculateHash } = await Promise.resolve().then(function () { return filesystem; });
|
|
3005
3049
|
const updatedHash = calculateHash(content, config.hashing?.normalization);
|
|
3006
3050
|
|
|
3007
3051
|
const normalizedToRelative = path.relative(process.cwd(), toEntryPath).replace(/\\/g, '/');
|
|
@@ -3034,6 +3078,39 @@ async function moveDirectory(fromPath, toPath, state, config, options = {}) {
|
|
|
3034
3078
|
}
|
|
3035
3079
|
}
|
|
3036
3080
|
|
|
3081
|
+
async function updateImportsInFile(filePath, oldFilePath, newFilePath) {
|
|
3082
|
+
if (!existsSync(filePath)) return;
|
|
3083
|
+
|
|
3084
|
+
let content = await readFile(filePath, 'utf-8');
|
|
3085
|
+
const oldDir = path.dirname(oldFilePath);
|
|
3086
|
+
const newDir = path.dirname(newFilePath);
|
|
3087
|
+
|
|
3088
|
+
if (oldDir === newDir) return;
|
|
3089
|
+
|
|
3090
|
+
// Find all relative imports
|
|
3091
|
+
const relativeImportRegex = /from\s+['"](\.\.?\/[^'"]+)['"]/g;
|
|
3092
|
+
let match;
|
|
3093
|
+
const replacements = [];
|
|
3094
|
+
|
|
3095
|
+
while ((match = relativeImportRegex.exec(content)) !== null) {
|
|
3096
|
+
const relativePath = match[1];
|
|
3097
|
+
const absoluteTarget = path.resolve(oldDir, relativePath);
|
|
3098
|
+
const newRelativePath = getRelativeImportPath(newFilePath, absoluteTarget);
|
|
3099
|
+
|
|
3100
|
+
replacements.push({
|
|
3101
|
+
full: match[0],
|
|
3102
|
+
oldRel: relativePath,
|
|
3103
|
+
newRel: newRelativePath
|
|
3104
|
+
});
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3107
|
+
for (const repl of replacements) {
|
|
3108
|
+
content = content.replace(repl.full, `from '${repl.newRel}'`);
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3111
|
+
await writeFile(filePath, content, 'utf-8');
|
|
3112
|
+
}
|
|
3113
|
+
|
|
3037
3114
|
async function createComponentCommand(componentName, options) {
|
|
3038
3115
|
try {
|
|
3039
3116
|
const config = await loadConfig();
|
|
@@ -3768,67 +3845,87 @@ async function validateStateCommand(options) {
|
|
|
3768
3845
|
}
|
|
3769
3846
|
}
|
|
3770
3847
|
|
|
3771
|
-
|
|
3772
|
-
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3848
|
+
/**
|
|
3849
|
+
* Computes the drift between the state and the actual files on disk.
|
|
3850
|
+
*
|
|
3851
|
+
* @param {import('./config.js').TextorConfig} config
|
|
3852
|
+
* @param {Object} state
|
|
3853
|
+
* @returns {Promise<{
|
|
3854
|
+
* missing: string[],
|
|
3855
|
+
* modified: string[],
|
|
3856
|
+
* untracked: string[],
|
|
3857
|
+
* orphaned: string[],
|
|
3858
|
+
* synced: number
|
|
3859
|
+
* }>}
|
|
3860
|
+
*/
|
|
3861
|
+
async function getProjectStatus(config, state) {
|
|
3862
|
+
const results = {
|
|
3863
|
+
missing: [],
|
|
3864
|
+
modified: [],
|
|
3865
|
+
untracked: [], // Has signature, not in state
|
|
3866
|
+
orphaned: [], // No signature, not in state
|
|
3867
|
+
synced: 0
|
|
3868
|
+
};
|
|
3783
3869
|
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3870
|
+
const roots = [
|
|
3871
|
+
resolvePath(config, 'pages'),
|
|
3872
|
+
resolvePath(config, 'features'),
|
|
3873
|
+
resolvePath(config, 'components')
|
|
3874
|
+
].map(p => path.resolve(p));
|
|
3789
3875
|
|
|
3790
|
-
|
|
3791
|
-
|
|
3876
|
+
const diskFiles = new Set();
|
|
3877
|
+
const configSignatures = Object.values(config.signatures || {});
|
|
3792
3878
|
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
}
|
|
3879
|
+
for (const root of roots) {
|
|
3880
|
+
if (existsSync(root)) {
|
|
3881
|
+
await scanDirectory(root, diskFiles);
|
|
3797
3882
|
}
|
|
3883
|
+
}
|
|
3798
3884
|
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3885
|
+
// 1. Check state files against disk
|
|
3886
|
+
for (const relativePath in state.files) {
|
|
3887
|
+
const fullPath = path.join(process.cwd(), relativePath);
|
|
3888
|
+
|
|
3889
|
+
if (!existsSync(fullPath)) {
|
|
3890
|
+
results.missing.push(relativePath);
|
|
3891
|
+
continue;
|
|
3892
|
+
}
|
|
3807
3893
|
|
|
3808
|
-
|
|
3894
|
+
// It exists on disk, so it's not untracked/orphaned
|
|
3895
|
+
diskFiles.delete(relativePath);
|
|
3809
3896
|
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3897
|
+
const content = await readFile(fullPath, 'utf-8');
|
|
3898
|
+
const currentHash = calculateHash(content, config.hashing?.normalization);
|
|
3899
|
+
const fileData = state.files[relativePath];
|
|
3813
3900
|
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
}
|
|
3901
|
+
if (currentHash !== fileData.hash) {
|
|
3902
|
+
results.modified.push(relativePath);
|
|
3903
|
+
} else {
|
|
3904
|
+
results.synced++;
|
|
3819
3905
|
}
|
|
3906
|
+
}
|
|
3820
3907
|
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
}
|
|
3908
|
+
// 2. Check remaining disk files
|
|
3909
|
+
for (const relativePath of diskFiles) {
|
|
3910
|
+
const fullPath = path.join(process.cwd(), relativePath);
|
|
3911
|
+
const isGenerated = await isTextorGenerated(fullPath, configSignatures);
|
|
3912
|
+
|
|
3913
|
+
if (isGenerated) {
|
|
3914
|
+
results.untracked.push(relativePath);
|
|
3915
|
+
} else {
|
|
3916
|
+
results.orphaned.push(relativePath);
|
|
3831
3917
|
}
|
|
3918
|
+
}
|
|
3919
|
+
|
|
3920
|
+
return results;
|
|
3921
|
+
}
|
|
3922
|
+
|
|
3923
|
+
async function statusCommand() {
|
|
3924
|
+
try {
|
|
3925
|
+
const config = await loadConfig();
|
|
3926
|
+
const state = await loadState();
|
|
3927
|
+
|
|
3928
|
+
const results = await getProjectStatus(config, state);
|
|
3832
3929
|
|
|
3833
3930
|
// Reporting
|
|
3834
3931
|
console.log('Textor Status Report:');
|
|
@@ -4253,6 +4350,71 @@ async function normalizeStateCommand(options) {
|
|
|
4253
4350
|
}
|
|
4254
4351
|
}
|
|
4255
4352
|
|
|
4353
|
+
/**
|
|
4354
|
+
* Removes missing references from Textor state.
|
|
4355
|
+
* @param {Object} options
|
|
4356
|
+
* @param {boolean} options.dryRun
|
|
4357
|
+
* @param {boolean} options.yes
|
|
4358
|
+
*/
|
|
4359
|
+
async function pruneMissingCommand(options = {}) {
|
|
4360
|
+
try {
|
|
4361
|
+
const config = await loadConfig();
|
|
4362
|
+
const state = await loadState();
|
|
4363
|
+
|
|
4364
|
+
const results = await getProjectStatus(config, state);
|
|
4365
|
+
|
|
4366
|
+
if (results.missing.length === 0) {
|
|
4367
|
+
console.log('No missing references found.');
|
|
4368
|
+
return;
|
|
4369
|
+
}
|
|
4370
|
+
|
|
4371
|
+
console.log(`Found ${results.missing.length} missing references:`);
|
|
4372
|
+
results.missing.forEach(f => console.log(` - ${f}`));
|
|
4373
|
+
|
|
4374
|
+
if (options.dryRun) {
|
|
4375
|
+
console.log('\nDry run: no changes applied to state.');
|
|
4376
|
+
return;
|
|
4377
|
+
}
|
|
4378
|
+
|
|
4379
|
+
if (!options.yes && options.interactive !== false && process.stdin.isTTY && process.env.NODE_ENV !== 'test') {
|
|
4380
|
+
const rl = readline.createInterface({
|
|
4381
|
+
input: process.stdin,
|
|
4382
|
+
output: process.stdout
|
|
4383
|
+
});
|
|
4384
|
+
|
|
4385
|
+
const confirmed = await new Promise(resolve => {
|
|
4386
|
+
rl.question('\nDo you want to proceed with pruning? (y/N) ', (answer) => {
|
|
4387
|
+
rl.close();
|
|
4388
|
+
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
|
|
4389
|
+
});
|
|
4390
|
+
});
|
|
4391
|
+
|
|
4392
|
+
if (!confirmed) {
|
|
4393
|
+
console.log('Aborted.');
|
|
4394
|
+
return;
|
|
4395
|
+
}
|
|
4396
|
+
}
|
|
4397
|
+
|
|
4398
|
+
for (const relPath of results.missing) {
|
|
4399
|
+
delete state.files[relPath];
|
|
4400
|
+
}
|
|
4401
|
+
|
|
4402
|
+
// Reconstruct metadata
|
|
4403
|
+
state.components = reconstructComponents(state.files, config);
|
|
4404
|
+
state.sections = reconstructSections(state, config);
|
|
4405
|
+
|
|
4406
|
+
await saveState(state);
|
|
4407
|
+
console.log(`\nā Successfully removed ${results.missing.length} missing references from state.`);
|
|
4408
|
+
|
|
4409
|
+
} catch (error) {
|
|
4410
|
+
console.error('Error:', error.message);
|
|
4411
|
+
if (typeof process.exit === 'function' && process.env.NODE_ENV !== 'test') {
|
|
4412
|
+
process.exit(1);
|
|
4413
|
+
}
|
|
4414
|
+
throw error;
|
|
4415
|
+
}
|
|
4416
|
+
}
|
|
4417
|
+
|
|
4256
4418
|
const program = new Command();
|
|
4257
4419
|
|
|
4258
4420
|
program
|
|
@@ -4286,6 +4448,11 @@ program
|
|
|
4286
4448
|
.option('--index', 'Create index.ts')
|
|
4287
4449
|
.option('--no-sub-components-dir', 'Skip creating sub-components directory')
|
|
4288
4450
|
.option('--no-scripts-dir', 'Skip creating scripts directory')
|
|
4451
|
+
.option('--prop <key=value>', 'Layout property', (val, memo) => {
|
|
4452
|
+
const [key, ...rest] = val.split('=');
|
|
4453
|
+
memo[key] = rest.join('=');
|
|
4454
|
+
return memo;
|
|
4455
|
+
}, {})
|
|
4289
4456
|
.option('--dry-run', 'Show what would be created without creating')
|
|
4290
4457
|
.option('--force', 'Overwrite existing files')
|
|
4291
4458
|
.action(addSectionCommand);
|
|
@@ -4385,5 +4552,13 @@ program
|
|
|
4385
4552
|
.option('--dry-run', 'Show the normalized state without writing to disk')
|
|
4386
4553
|
.action(normalizeStateCommand);
|
|
4387
4554
|
|
|
4555
|
+
program
|
|
4556
|
+
.command('prune-missing')
|
|
4557
|
+
.description('Remove missing files from state (files that are in state but not on disk)')
|
|
4558
|
+
.option('--dry-run', 'Show what would be removed without applying')
|
|
4559
|
+
.option('--yes', 'Skip confirmation')
|
|
4560
|
+
.option('--no-interactive', 'Disable interactive prompts')
|
|
4561
|
+
.action(pruneMissingCommand);
|
|
4562
|
+
|
|
4388
4563
|
program.parse();
|
|
4389
4564
|
//# sourceMappingURL=textor.js.map
|