@oamm/textor 1.0.4 → 1.0.7
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 +27 -3
- package/dist/bin/textor.js +212 -50
- package/dist/bin/textor.js.map +1 -1
- package/dist/index.cjs +184 -48
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +184 -48
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -370,7 +370,8 @@ The .textor/config.json file allows full control over the tool's behavior.
|
|
|
370
370
|
"createTypes": false,
|
|
371
371
|
"createReadme": false,
|
|
372
372
|
"createStories": false,
|
|
373
|
-
"createIndex": false
|
|
373
|
+
"createIndex": false,
|
|
374
|
+
"layout": "Main"
|
|
374
375
|
},
|
|
375
376
|
"components": {
|
|
376
377
|
"framework": "react",
|
|
@@ -466,7 +467,7 @@ You can customize the code generated by Textor by providing your own templates.
|
|
|
466
467
|
|
|
467
468
|
1. Create the `.textor/templates/` directory if it doesn't exist.
|
|
468
469
|
2. Create a file named according to the table below (e.g., `feature.astro` or `component.tsx`).
|
|
469
|
-
3. Use `{{variable}}` placeholders in your template. Textor will automatically replace them when generating files.
|
|
470
|
+
3. Use `{{variable}}` or `__variable__` placeholders in your template. Textor will automatically replace them when generating files. Using `__variable__` (e.g., `__componentName__`) is recommended for TypeScript/JavaScript templates as it is a valid identifier and avoids "broken code" warnings in your IDE.
|
|
470
471
|
|
|
471
472
|
### Supported Templates
|
|
472
473
|
|
|
@@ -495,15 +496,38 @@ You can customize the code generated by Textor by providing your own templates.
|
|
|
495
496
|
### Variables Description
|
|
496
497
|
|
|
497
498
|
- `{{componentName}}`: The PascalCase name of the feature or component (e.g., `UserCatalog`).
|
|
499
|
+
- `{{componentNameCamel}}`: camelCase version of the name (e.g., `userCatalog`).
|
|
500
|
+
- `{{componentNameKebab}}`: kebab-case version of the name (e.g., `user-catalog`).
|
|
501
|
+
- `{{componentNameSnake}}`: snake_case version of the name (e.g., `user_catalog`).
|
|
502
|
+
- `{{componentNameUpper}}`: SCREAMING_SNAKE_CASE version of the name (e.g., `USER_CATALOG`).
|
|
498
503
|
- `{{hookName}}`: The camelCase name of the generated hook (e.g., `useUserCatalog`).
|
|
499
504
|
- `{{componentPath}}`: Relative path to the component file (useful for imports in tests or stories).
|
|
500
505
|
- `{{featureComponentName}}`: The name of the feature component as imported in a route.
|
|
501
|
-
- `{{
|
|
506
|
+
- `{{featureComponentNameCamel}}`, `{{featureComponentNameKebab}}`, `{{featureComponentNameSnake}}`, `{{featureComponentNameUpper}}`, `{{featureComponentNamePascal}}`: Case variations for the feature component name.
|
|
502
507
|
- `{{layoutName}}`: The name of the layout component being used.
|
|
508
|
+
- `{{layoutNameCamel}}`, `{{layoutNameKebab}}`, `{{layoutNameSnake}}`, `{{layoutNameUpper}}`, `{{layoutNamePascal}}`: Case variations for the layout name.
|
|
503
509
|
- `{{layoutImportPath}}`: The import path for the layout component.
|
|
504
510
|
- `{{scriptImportPath}}`: Relative path to the client-side script entry point.
|
|
505
511
|
- `{{componentExtension}}`: The file extension of the component (e.g., `.astro` or `.tsx`).
|
|
506
512
|
|
|
513
|
+
### Example: Custom Route Template (`.textor/templates/route.ts`)
|
|
514
|
+
|
|
515
|
+
If you are using a custom routing library and want your templates to be valid TypeScript:
|
|
516
|
+
|
|
517
|
+
```typescript
|
|
518
|
+
/**
|
|
519
|
+
* @generated by Textor
|
|
520
|
+
* Route: {{featureComponentName}}
|
|
521
|
+
*/
|
|
522
|
+
import { defineRoute } from "my-router";
|
|
523
|
+
import __featureComponentName__ from "{{featureImportPath}}";
|
|
524
|
+
|
|
525
|
+
export const __featureComponentName__Route = defineRoute({
|
|
526
|
+
path: "/__featureComponentNameKebab__",
|
|
527
|
+
component: __featureComponentName__
|
|
528
|
+
});
|
|
529
|
+
```
|
|
530
|
+
|
|
507
531
|
### Example: Custom Feature Template (`.textor/templates/feature.astro`)
|
|
508
532
|
|
|
509
533
|
```astro
|
package/dist/bin/textor.js
CHANGED
|
@@ -36,10 +36,12 @@ const CURRENT_CONFIG_VERSION = 2;
|
|
|
36
36
|
* @property {string} signatures.typescript
|
|
37
37
|
* @property {string} signatures.javascript
|
|
38
38
|
* @property {Object} features
|
|
39
|
+
* @property {string} features.framework
|
|
39
40
|
* @property {string} features.entry
|
|
40
41
|
* @property {boolean} features.createSubComponentsDir
|
|
41
42
|
* @property {boolean} features.createScriptsDir
|
|
42
43
|
* @property {string} features.scriptsIndexFile
|
|
44
|
+
* @property {string} features.layout
|
|
43
45
|
* @property {Object} components
|
|
44
46
|
* @property {boolean} components.createSubComponentsDir
|
|
45
47
|
* @property {boolean} components.createContext
|
|
@@ -105,7 +107,8 @@ const DEFAULT_CONFIG = {
|
|
|
105
107
|
createTypes: false,
|
|
106
108
|
createReadme: false,
|
|
107
109
|
createStories: false,
|
|
108
|
-
createIndex: false
|
|
110
|
+
createIndex: false,
|
|
111
|
+
layout: 'Main'
|
|
109
112
|
},
|
|
110
113
|
components: {
|
|
111
114
|
framework: 'react',
|
|
@@ -454,12 +457,15 @@ function getEffectiveOptions(cmdOptions, config, type) {
|
|
|
454
457
|
async function initCommand(options) {
|
|
455
458
|
try {
|
|
456
459
|
const configPath = await saveConfig(DEFAULT_CONFIG, options.force);
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
460
|
+
const quiet = options?.quiet || process.env.NODE_ENV === 'test' || process.env.TEXTOR_QUIET === '1';
|
|
461
|
+
|
|
462
|
+
if (!quiet) {
|
|
463
|
+
console.log('Textor configuration created at:', configPath);
|
|
464
|
+
console.log('\nDefault configuration:');
|
|
465
|
+
console.log(JSON.stringify(DEFAULT_CONFIG, null, 2));
|
|
466
|
+
console.log('\nYou can now use Textor commands like:');
|
|
467
|
+
console.log(' textor add-section /users users/catalog --layout Main');
|
|
468
|
+
}
|
|
463
469
|
} catch (error) {
|
|
464
470
|
console.error('Error:', error.message);
|
|
465
471
|
process.exit(1);
|
|
@@ -479,6 +485,28 @@ function toPascalCase(input) {
|
|
|
479
485
|
.join('');
|
|
480
486
|
}
|
|
481
487
|
|
|
488
|
+
function toCamelCase(input) {
|
|
489
|
+
const pascal = toPascalCase(input);
|
|
490
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function toKebabCase(input) {
|
|
494
|
+
return input
|
|
495
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
|
496
|
+
.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
|
|
497
|
+
.replace(/[_\s/\\-]+/g, '-')
|
|
498
|
+
.toLowerCase()
|
|
499
|
+
.replace(/^-+|-+$/g, '');
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
function toSnakeCase(input) {
|
|
503
|
+
return toKebabCase(input).replace(/-/g, '_');
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function toScreamingSnakeCase(input) {
|
|
507
|
+
return toSnakeCase(input).toUpperCase();
|
|
508
|
+
}
|
|
509
|
+
|
|
482
510
|
function getFeatureComponentName(featurePath) {
|
|
483
511
|
return toPascalCase(featurePath);
|
|
484
512
|
}
|
|
@@ -1044,12 +1072,30 @@ function resolvePatternedPath(baseDir, pattern, data, fallback, label) {
|
|
|
1044
1072
|
return secureJoin(baseDir, fileName);
|
|
1045
1073
|
}
|
|
1046
1074
|
|
|
1075
|
+
function enrichData(data) {
|
|
1076
|
+
const enriched = { ...data };
|
|
1077
|
+
const nameKeys = ['componentName', 'featureComponentName', 'layoutName'];
|
|
1078
|
+
for (const key of nameKeys) {
|
|
1079
|
+
if (data[key] && typeof data[key] === 'string') {
|
|
1080
|
+
enriched[`${key}Camel`] = toCamelCase(data[key]);
|
|
1081
|
+
enriched[`${key}Kebab`] = toKebabCase(data[key]);
|
|
1082
|
+
enriched[`${key}Snake`] = toSnakeCase(data[key]);
|
|
1083
|
+
enriched[`${key}Upper`] = toScreamingSnakeCase(data[key]);
|
|
1084
|
+
enriched[`${key}Pascal`] = toPascalCase(data[key]);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
return enriched;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1047
1090
|
function getTemplateOverride(templateName, extension, data = {}) {
|
|
1048
1091
|
const overridePath = path.join(process.cwd(), '.textor', 'templates', `${templateName}${extension}`);
|
|
1049
1092
|
if (existsSync(overridePath)) {
|
|
1050
1093
|
let content = readFileSync(overridePath, 'utf-8');
|
|
1051
|
-
|
|
1052
|
-
|
|
1094
|
+
const finalData = enrichData(data);
|
|
1095
|
+
for (const [key, value] of Object.entries(finalData)) {
|
|
1096
|
+
const replacement = () => value || '';
|
|
1097
|
+
content = content.replace(new RegExp(`{{${key}}}`, 'g'), replacement);
|
|
1098
|
+
content = content.replace(new RegExp(`__${key}__`, 'g'), replacement);
|
|
1053
1099
|
}
|
|
1054
1100
|
return content;
|
|
1055
1101
|
}
|
|
@@ -1063,8 +1109,8 @@ function getTemplateOverride(templateName, extension, data = {}) {
|
|
|
1063
1109
|
* - featureImportPath: Path to import the feature component
|
|
1064
1110
|
* - featureComponentName: Name of the feature component
|
|
1065
1111
|
*/
|
|
1066
|
-
function generateRouteTemplate(layoutName, layoutImportPath, featureImportPath, featureComponentName) {
|
|
1067
|
-
const override = getTemplateOverride('route',
|
|
1112
|
+
function generateRouteTemplate(layoutName, layoutImportPath, featureImportPath, featureComponentName, extension = '.astro') {
|
|
1113
|
+
const override = getTemplateOverride('route', extension, {
|
|
1068
1114
|
layoutName,
|
|
1069
1115
|
layoutImportPath,
|
|
1070
1116
|
featureImportPath,
|
|
@@ -1097,9 +1143,9 @@ import ${featureComponentName} from '${featureImportPath}';
|
|
|
1097
1143
|
* - componentName: Name of the feature component
|
|
1098
1144
|
* - scriptImportPath: Path to the feature's client-side script
|
|
1099
1145
|
*/
|
|
1100
|
-
function generateFeatureTemplate(componentName, scriptImportPath, framework = 'astro') {
|
|
1101
|
-
const
|
|
1102
|
-
const override = getTemplateOverride('feature',
|
|
1146
|
+
function generateFeatureTemplate(componentName, scriptImportPath, framework = 'astro', extension) {
|
|
1147
|
+
const templateExtension = extension || (framework === 'astro' ? '.astro' : '.tsx');
|
|
1148
|
+
const override = getTemplateOverride('feature', templateExtension, { componentName, scriptImportPath });
|
|
1103
1149
|
if (override) return override;
|
|
1104
1150
|
|
|
1105
1151
|
if (framework === 'react') {
|
|
@@ -1144,9 +1190,9 @@ function generateScriptsIndexTemplate() {
|
|
|
1144
1190
|
* Component Template Variables:
|
|
1145
1191
|
* - componentName: Name of the component
|
|
1146
1192
|
*/
|
|
1147
|
-
function generateComponentTemplate(componentName, framework = 'react') {
|
|
1148
|
-
const
|
|
1149
|
-
const override = getTemplateOverride('component',
|
|
1193
|
+
function generateComponentTemplate(componentName, framework = 'react', extension) {
|
|
1194
|
+
const templateExtension = extension || (framework === 'astro' ? '.astro' : '.tsx');
|
|
1195
|
+
const override = getTemplateOverride('component', templateExtension, { componentName });
|
|
1150
1196
|
if (override) return override;
|
|
1151
1197
|
|
|
1152
1198
|
if (framework === 'react') {
|
|
@@ -1751,6 +1797,7 @@ async function addSectionCommand(route, featurePath, options) {
|
|
|
1751
1797
|
|
|
1752
1798
|
const {
|
|
1753
1799
|
framework,
|
|
1800
|
+
layout,
|
|
1754
1801
|
createSubComponentsDir: shouldCreateSubComponentsDir,
|
|
1755
1802
|
createScriptsDir: shouldCreateScriptsDir,
|
|
1756
1803
|
createApi: shouldCreateApi,
|
|
@@ -1922,7 +1969,13 @@ async function addSectionCommand(route, featurePath, options) {
|
|
|
1922
1969
|
}
|
|
1923
1970
|
|
|
1924
1971
|
// Update imports in the moved file
|
|
1925
|
-
await updateImportsInFile(reorg.to, reorg.from, reorg.to);
|
|
1972
|
+
await updateImportsInFile$1(reorg.to, reorg.from, reorg.to);
|
|
1973
|
+
|
|
1974
|
+
// Update hash in state after import updates
|
|
1975
|
+
if (state.files[newRelative]) {
|
|
1976
|
+
const content = await readFile(reorg.to, 'utf-8');
|
|
1977
|
+
state.files[newRelative].hash = calculateHash(content, config.hashing?.normalization);
|
|
1978
|
+
}
|
|
1926
1979
|
|
|
1927
1980
|
console.log(`✓ Reorganized ${oldRelative} to ${newRelative}`);
|
|
1928
1981
|
}
|
|
@@ -1945,11 +1998,11 @@ async function addSectionCommand(route, featurePath, options) {
|
|
|
1945
1998
|
if (shouldCreateScriptsDir) await ensureNotExists(scriptsIndexPath, options.force);
|
|
1946
1999
|
|
|
1947
2000
|
let layoutImportPath = null;
|
|
1948
|
-
if (routeFilePath &&
|
|
2001
|
+
if (routeFilePath && layout !== 'none') {
|
|
1949
2002
|
if (config.importAliases.layouts) {
|
|
1950
|
-
layoutImportPath = `${config.importAliases.layouts}/${
|
|
2003
|
+
layoutImportPath = `${config.importAliases.layouts}/${layout}.astro`;
|
|
1951
2004
|
} else {
|
|
1952
|
-
const layoutFilePath = secureJoin(layoutsRoot, `${
|
|
2005
|
+
const layoutFilePath = secureJoin(layoutsRoot, `${layout}.astro`);
|
|
1953
2006
|
layoutImportPath = getRelativeImportPath(routeFilePath, layoutFilePath);
|
|
1954
2007
|
}
|
|
1955
2008
|
}
|
|
@@ -1986,16 +2039,17 @@ async function addSectionCommand(route, featurePath, options) {
|
|
|
1986
2039
|
routeSignature = getSignature(config, 'typescript');
|
|
1987
2040
|
} else {
|
|
1988
2041
|
routeContent = generateRouteTemplate(
|
|
1989
|
-
|
|
2042
|
+
layout,
|
|
1990
2043
|
layoutImportPath,
|
|
1991
2044
|
featureImportPath,
|
|
1992
|
-
featureComponentName
|
|
2045
|
+
featureComponentName,
|
|
2046
|
+
routeExtension
|
|
1993
2047
|
);
|
|
1994
2048
|
routeSignature = getSignature(config, 'astro');
|
|
1995
2049
|
}
|
|
1996
2050
|
}
|
|
1997
2051
|
|
|
1998
|
-
const featureContent = generateFeatureTemplate(featureComponentName, scriptImportPath, framework);
|
|
2052
|
+
const featureContent = generateFeatureTemplate(featureComponentName, scriptImportPath, framework, config.naming.featureExtension);
|
|
1999
2053
|
|
|
2000
2054
|
const writtenFiles = [];
|
|
2001
2055
|
|
|
@@ -2257,7 +2311,7 @@ async function addSectionCommand(route, featurePath, options) {
|
|
|
2257
2311
|
name: options.name || featureComponentName,
|
|
2258
2312
|
route: normalizedRoute,
|
|
2259
2313
|
featurePath: normalizedFeaturePath,
|
|
2260
|
-
layout:
|
|
2314
|
+
layout: layout,
|
|
2261
2315
|
extension: routeExtension
|
|
2262
2316
|
});
|
|
2263
2317
|
|
|
@@ -2274,7 +2328,7 @@ async function addSectionCommand(route, featurePath, options) {
|
|
|
2274
2328
|
}
|
|
2275
2329
|
}
|
|
2276
2330
|
|
|
2277
|
-
async function updateImportsInFile(filePath, oldFilePath, newFilePath) {
|
|
2331
|
+
async function updateImportsInFile$1(filePath, oldFilePath, newFilePath) {
|
|
2278
2332
|
if (!existsSync(filePath)) return;
|
|
2279
2333
|
|
|
2280
2334
|
let content = await readFile(filePath, 'utf-8');
|
|
@@ -2317,33 +2371,43 @@ async function removeSectionCommand(route, featurePath, options) {
|
|
|
2317
2371
|
|
|
2318
2372
|
const state = await loadState();
|
|
2319
2373
|
|
|
2320
|
-
let targetRoute = route;
|
|
2321
|
-
let targetFeaturePath = featurePath;
|
|
2322
2374
|
let section = findSection(state, route);
|
|
2375
|
+
if (!section && featurePath) {
|
|
2376
|
+
section = findSection(state, featurePath);
|
|
2377
|
+
}
|
|
2378
|
+
|
|
2379
|
+
const targetRoute = section ? section.route : route;
|
|
2380
|
+
const targetFeaturePath = section ? section.featurePath : featurePath;
|
|
2323
2381
|
|
|
2324
2382
|
if (!targetFeaturePath) {
|
|
2325
|
-
|
|
2326
|
-
targetRoute = section.route;
|
|
2327
|
-
targetFeaturePath = section.featurePath;
|
|
2328
|
-
} else {
|
|
2329
|
-
throw new Error(`Section not found for identifier: ${route}. Please provide both route and featurePath.`);
|
|
2330
|
-
}
|
|
2383
|
+
throw new Error(`Section not found for identifier: ${route}. Please provide both route and featurePath.`);
|
|
2331
2384
|
}
|
|
2332
2385
|
|
|
2333
2386
|
const normalizedRoute = normalizeRoute(targetRoute);
|
|
2334
2387
|
const normalizedFeaturePath = featureToDirectoryPath(targetFeaturePath);
|
|
2335
2388
|
|
|
2336
|
-
const routeExtension = (section && section.extension) || config.naming.routeExtension;
|
|
2337
|
-
const routeFileName = routeToFilePath(normalizedRoute, {
|
|
2338
|
-
extension: routeExtension,
|
|
2339
|
-
mode: config.routing.mode,
|
|
2340
|
-
indexFile: config.routing.indexFile
|
|
2341
|
-
});
|
|
2342
|
-
|
|
2343
2389
|
const pagesRoot = resolvePath(config, 'pages');
|
|
2344
2390
|
const featuresRoot = resolvePath(config, 'features');
|
|
2391
|
+
|
|
2392
|
+
// Find route file in state if possible
|
|
2393
|
+
let routeFilePath = null;
|
|
2394
|
+
const routeRelPath = Object.keys(state.files).find(f => {
|
|
2395
|
+
const data = state.files[f];
|
|
2396
|
+
return data.kind === 'route' && data.owner === normalizedRoute;
|
|
2397
|
+
});
|
|
2398
|
+
|
|
2399
|
+
if (routeRelPath) {
|
|
2400
|
+
routeFilePath = path.resolve(process.cwd(), routeRelPath);
|
|
2401
|
+
} else {
|
|
2402
|
+
const routeExtension = (section && section.extension) || config.naming.routeExtension;
|
|
2403
|
+
const routeFileName = routeToFilePath(normalizedRoute, {
|
|
2404
|
+
extension: routeExtension,
|
|
2405
|
+
mode: config.routing.mode,
|
|
2406
|
+
indexFile: config.routing.indexFile
|
|
2407
|
+
});
|
|
2408
|
+
routeFilePath = secureJoin(pagesRoot, routeFileName);
|
|
2409
|
+
}
|
|
2345
2410
|
|
|
2346
|
-
const routeFilePath = secureJoin(pagesRoot, routeFileName);
|
|
2347
2411
|
const featureDirPath = secureJoin(featuresRoot, normalizedFeaturePath);
|
|
2348
2412
|
|
|
2349
2413
|
const deletedFiles = [];
|
|
@@ -2430,8 +2494,67 @@ async function removeSectionCommand(route, featurePath, options) {
|
|
|
2430
2494
|
}
|
|
2431
2495
|
|
|
2432
2496
|
if (deletedFiles.length === 0 && deletedDirs.length === 0 && skippedFiles.length === 0) {
|
|
2433
|
-
|
|
2497
|
+
if (section) {
|
|
2498
|
+
console.log(`✓ Section ${normalizedRoute} removed from state (files were already missing on disk).`);
|
|
2499
|
+
state.sections = state.sections.filter(s => s.route !== normalizedRoute);
|
|
2500
|
+
await saveState(state);
|
|
2501
|
+
} else {
|
|
2502
|
+
console.log('No files to delete.');
|
|
2503
|
+
}
|
|
2434
2504
|
} else {
|
|
2505
|
+
// Reorganization (Flattening)
|
|
2506
|
+
if (!options.keepRoute && deletedFiles.length > 0 && config.routing.mode === 'flat') {
|
|
2507
|
+
const routeParts = normalizedRoute.split('/').filter(Boolean);
|
|
2508
|
+
if (routeParts.length > 1) {
|
|
2509
|
+
for (let i = routeParts.length - 1; i >= 1; i--) {
|
|
2510
|
+
const parentRoute = '/' + routeParts.slice(0, i).join('/');
|
|
2511
|
+
const parentDirName = routeParts.slice(0, i).join('/');
|
|
2512
|
+
const parentDirPath = secureJoin(pagesRoot, parentDirName);
|
|
2513
|
+
|
|
2514
|
+
if (existsSync(parentDirPath)) {
|
|
2515
|
+
const filesInDir = await readdir(parentDirPath);
|
|
2516
|
+
|
|
2517
|
+
if (filesInDir.length === 1) {
|
|
2518
|
+
const loneFile = filesInDir[0];
|
|
2519
|
+
const ext = path.extname(loneFile);
|
|
2520
|
+
const indexFile = ext === '.astro' ? config.routing.indexFile : `index${ext}`;
|
|
2521
|
+
|
|
2522
|
+
if (loneFile === indexFile) {
|
|
2523
|
+
const loneFilePath = path.join(parentDirPath, loneFile);
|
|
2524
|
+
const oldRelative = path.relative(process.cwd(), loneFilePath).replace(/\\/g, '/');
|
|
2525
|
+
|
|
2526
|
+
if (state.files[oldRelative] && state.files[oldRelative].kind === 'route') {
|
|
2527
|
+
const flatFileName = routeToFilePath(parentRoute, {
|
|
2528
|
+
extension: ext,
|
|
2529
|
+
mode: 'flat'
|
|
2530
|
+
});
|
|
2531
|
+
const flatFilePath = secureJoin(pagesRoot, flatFileName);
|
|
2532
|
+
|
|
2533
|
+
if (!existsSync(flatFilePath)) {
|
|
2534
|
+
await rename(loneFilePath, flatFilePath);
|
|
2535
|
+
|
|
2536
|
+
const newRelative = path.relative(process.cwd(), flatFilePath).replace(/\\/g, '/');
|
|
2537
|
+
|
|
2538
|
+
state.files[newRelative] = { ...state.files[oldRelative] };
|
|
2539
|
+
delete state.files[oldRelative];
|
|
2540
|
+
|
|
2541
|
+
await updateImportsInFile(flatFilePath, loneFilePath, flatFilePath);
|
|
2542
|
+
|
|
2543
|
+
// Update hash in state after import updates
|
|
2544
|
+
const content = await readFile(flatFilePath, 'utf-8');
|
|
2545
|
+
state.files[newRelative].hash = calculateHash(content, config.hashing?.normalization);
|
|
2546
|
+
|
|
2547
|
+
console.log(`✓ Reorganized ${oldRelative} to ${newRelative} (flattened)`);
|
|
2548
|
+
await cleanupEmptyDirs(parentDirPath, pagesRoot);
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2435
2558
|
state.sections = state.sections.filter(s => s.route !== normalizedRoute);
|
|
2436
2559
|
await saveState(state);
|
|
2437
2560
|
}
|
|
@@ -2445,6 +2568,39 @@ async function removeSectionCommand(route, featurePath, options) {
|
|
|
2445
2568
|
}
|
|
2446
2569
|
}
|
|
2447
2570
|
|
|
2571
|
+
async function updateImportsInFile(filePath, oldFilePath, newFilePath) {
|
|
2572
|
+
if (!existsSync(filePath)) return;
|
|
2573
|
+
|
|
2574
|
+
let content = await readFile(filePath, 'utf-8');
|
|
2575
|
+
const oldDir = path.dirname(oldFilePath);
|
|
2576
|
+
const newDir = path.dirname(newFilePath);
|
|
2577
|
+
|
|
2578
|
+
if (oldDir === newDir) return;
|
|
2579
|
+
|
|
2580
|
+
// Find all relative imports
|
|
2581
|
+
const relativeImportRegex = /from\s+['"](\.\.?\/[^'"]+)['"]/g;
|
|
2582
|
+
let match;
|
|
2583
|
+
const replacements = [];
|
|
2584
|
+
|
|
2585
|
+
while ((match = relativeImportRegex.exec(content)) !== null) {
|
|
2586
|
+
const relativePath = match[1];
|
|
2587
|
+
const absoluteTarget = path.resolve(oldDir, relativePath);
|
|
2588
|
+
const newRelativePath = getRelativeImportPath(newFilePath, absoluteTarget);
|
|
2589
|
+
|
|
2590
|
+
replacements.push({
|
|
2591
|
+
full: match[0],
|
|
2592
|
+
oldRel: relativePath,
|
|
2593
|
+
newRel: newRelativePath
|
|
2594
|
+
});
|
|
2595
|
+
}
|
|
2596
|
+
|
|
2597
|
+
for (const repl of replacements) {
|
|
2598
|
+
content = content.replace(repl.full, `from '${repl.newRel}'`);
|
|
2599
|
+
}
|
|
2600
|
+
|
|
2601
|
+
await writeFile(filePath, content, 'utf-8');
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2448
2604
|
/**
|
|
2449
2605
|
* Move a section (route + feature).
|
|
2450
2606
|
*
|
|
@@ -3060,7 +3216,7 @@ async function createComponentCommand(componentName, options) {
|
|
|
3060
3216
|
if (shouldCreateServices) await ensureDir(servicesDirInside);
|
|
3061
3217
|
if (shouldCreateSchemas) await ensureDir(schemasDirInside);
|
|
3062
3218
|
|
|
3063
|
-
const componentContent = generateComponentTemplate(normalizedName, framework);
|
|
3219
|
+
const componentContent = generateComponentTemplate(normalizedName, framework, config.naming.componentExtension);
|
|
3064
3220
|
const signature = getSignature(config, config.naming.componentExtension === '.astro' ? 'astro' : 'tsx');
|
|
3065
3221
|
|
|
3066
3222
|
const componentHash = await writeFileWithSignature(
|
|
@@ -3358,18 +3514,23 @@ async function removeComponentCommand(identifier, options) {
|
|
|
3358
3514
|
owner: identifier
|
|
3359
3515
|
});
|
|
3360
3516
|
|
|
3361
|
-
if (result.deleted) {
|
|
3362
|
-
|
|
3363
|
-
|
|
3517
|
+
if (result.deleted || (result.reason === 'not-found' && component)) {
|
|
3518
|
+
if (result.deleted) {
|
|
3519
|
+
console.log(`✓ Deleted component: ${componentDir}/`);
|
|
3520
|
+
await cleanupEmptyDirs(path.dirname(componentDir), path.join(process.cwd(), config.paths.components));
|
|
3521
|
+
} else {
|
|
3522
|
+
console.log(`✓ Component ${identifier} removed from state (directory was already missing on disk).`);
|
|
3523
|
+
}
|
|
3364
3524
|
|
|
3365
3525
|
// Unregister files
|
|
3366
|
-
const
|
|
3526
|
+
const relComponentPath = path.relative(process.cwd(), componentDir).replace(/\\/g, '/');
|
|
3527
|
+
const dirPrefix = relComponentPath + '/';
|
|
3367
3528
|
for (const f in state.files) {
|
|
3368
3529
|
if (f.startsWith(dirPrefix)) {
|
|
3369
3530
|
delete state.files[f];
|
|
3370
3531
|
}
|
|
3371
3532
|
}
|
|
3372
|
-
state.components = state.components.filter(c => c.name !== identifier && c.path !==
|
|
3533
|
+
state.components = state.components.filter(c => c.name !== identifier && c.path !== relComponentPath);
|
|
3373
3534
|
await saveState(state);
|
|
3374
3535
|
} else if (result.message) {
|
|
3375
3536
|
console.log(`⚠ Skipped: ${componentDir}`);
|
|
@@ -4103,13 +4264,14 @@ program
|
|
|
4103
4264
|
.command('init')
|
|
4104
4265
|
.description('Initialize Textor configuration')
|
|
4105
4266
|
.option('--force', 'Overwrite existing configuration')
|
|
4267
|
+
.option('--quiet', 'Skip printing full default configuration')
|
|
4106
4268
|
.action(initCommand);
|
|
4107
4269
|
|
|
4108
4270
|
program
|
|
4109
4271
|
.command('add-section [route] [featurePath]')
|
|
4110
4272
|
.description('Create a route + feature binding (route optional for standalone features)')
|
|
4111
4273
|
.option('--preset <name>', 'Scaffolding preset (minimal, standard, senior)')
|
|
4112
|
-
.option('--layout <name>', 'Layout component name (use "none" for no layout)'
|
|
4274
|
+
.option('--layout <name>', 'Layout component name (use "none" for no layout)')
|
|
4113
4275
|
.option('--name <name>', 'Section name for state tracking')
|
|
4114
4276
|
.option('--endpoint', 'Create an API endpoint (.ts) instead of an Astro page')
|
|
4115
4277
|
.option('--api', 'Create api directory')
|
package/dist/bin/textor.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"textor.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"textor.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|