@oamm/textor 1.0.4 → 1.0.5

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 CHANGED
@@ -466,7 +466,7 @@ You can customize the code generated by Textor by providing your own templates.
466
466
 
467
467
  1. Create the `.textor/templates/` directory if it doesn't exist.
468
468
  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.
469
+ 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
470
 
471
471
  ### Supported Templates
472
472
 
@@ -495,15 +495,38 @@ You can customize the code generated by Textor by providing your own templates.
495
495
  ### Variables Description
496
496
 
497
497
  - `{{componentName}}`: The PascalCase name of the feature or component (e.g., `UserCatalog`).
498
+ - `{{componentNameCamel}}`: camelCase version of the name (e.g., `userCatalog`).
499
+ - `{{componentNameKebab}}`: kebab-case version of the name (e.g., `user-catalog`).
500
+ - `{{componentNameSnake}}`: snake_case version of the name (e.g., `user_catalog`).
501
+ - `{{componentNameUpper}}`: SCREAMING_SNAKE_CASE version of the name (e.g., `USER_CATALOG`).
498
502
  - `{{hookName}}`: The camelCase name of the generated hook (e.g., `useUserCatalog`).
499
503
  - `{{componentPath}}`: Relative path to the component file (useful for imports in tests or stories).
500
504
  - `{{featureComponentName}}`: The name of the feature component as imported in a route.
501
- - `{{featureImportPath}}`: The import path for the feature component.
505
+ - `{{featureComponentNameCamel}}`, `{{featureComponentNameKebab}}`, `{{featureComponentNameSnake}}`, `{{featureComponentNameUpper}}`, `{{featureComponentNamePascal}}`: Case variations for the feature component name.
502
506
  - `{{layoutName}}`: The name of the layout component being used.
507
+ - `{{layoutNameCamel}}`, `{{layoutNameKebab}}`, `{{layoutNameSnake}}`, `{{layoutNameUpper}}`, `{{layoutNamePascal}}`: Case variations for the layout name.
503
508
  - `{{layoutImportPath}}`: The import path for the layout component.
504
509
  - `{{scriptImportPath}}`: Relative path to the client-side script entry point.
505
510
  - `{{componentExtension}}`: The file extension of the component (e.g., `.astro` or `.tsx`).
506
511
 
512
+ ### Example: Custom Route Template (`.textor/templates/route.ts`)
513
+
514
+ If you are using a custom routing library and want your templates to be valid TypeScript:
515
+
516
+ ```typescript
517
+ /**
518
+ * @generated by Textor
519
+ * Route: {{featureComponentName}}
520
+ */
521
+ import { defineRoute } from "my-router";
522
+ import __featureComponentName__ from "{{featureImportPath}}";
523
+
524
+ export const __featureComponentName__Route = defineRoute({
525
+ path: "/__featureComponentNameKebab__",
526
+ component: __featureComponentName__
527
+ });
528
+ ```
529
+
507
530
  ### Example: Custom Feature Template (`.textor/templates/feature.astro`)
508
531
 
509
532
  ```astro
@@ -454,12 +454,15 @@ function getEffectiveOptions(cmdOptions, config, type) {
454
454
  async function initCommand(options) {
455
455
  try {
456
456
  const configPath = await saveConfig(DEFAULT_CONFIG, options.force);
457
-
458
- console.log('✓ Textor configuration created at:', configPath);
459
- console.log('\nDefault configuration:');
460
- console.log(JSON.stringify(DEFAULT_CONFIG, null, 2));
461
- console.log('\nYou can now use Textor commands like:');
462
- console.log(' textor add-section /users users/catalog --layout Main');
457
+ const quiet = options?.quiet || process.env.NODE_ENV === 'test' || process.env.TEXTOR_QUIET === '1';
458
+
459
+ if (!quiet) {
460
+ console.log('Textor configuration created at:', configPath);
461
+ console.log('\nDefault configuration:');
462
+ console.log(JSON.stringify(DEFAULT_CONFIG, null, 2));
463
+ console.log('\nYou can now use Textor commands like:');
464
+ console.log(' textor add-section /users users/catalog --layout Main');
465
+ }
463
466
  } catch (error) {
464
467
  console.error('Error:', error.message);
465
468
  process.exit(1);
@@ -479,6 +482,28 @@ function toPascalCase(input) {
479
482
  .join('');
480
483
  }
481
484
 
485
+ function toCamelCase(input) {
486
+ const pascal = toPascalCase(input);
487
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
488
+ }
489
+
490
+ function toKebabCase(input) {
491
+ return input
492
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
493
+ .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
494
+ .replace(/[_\s/\\-]+/g, '-')
495
+ .toLowerCase()
496
+ .replace(/^-+|-+$/g, '');
497
+ }
498
+
499
+ function toSnakeCase(input) {
500
+ return toKebabCase(input).replace(/-/g, '_');
501
+ }
502
+
503
+ function toScreamingSnakeCase(input) {
504
+ return toSnakeCase(input).toUpperCase();
505
+ }
506
+
482
507
  function getFeatureComponentName(featurePath) {
483
508
  return toPascalCase(featurePath);
484
509
  }
@@ -1044,12 +1069,30 @@ function resolvePatternedPath(baseDir, pattern, data, fallback, label) {
1044
1069
  return secureJoin(baseDir, fileName);
1045
1070
  }
1046
1071
 
1072
+ function enrichData(data) {
1073
+ const enriched = { ...data };
1074
+ const nameKeys = ['componentName', 'featureComponentName', 'layoutName'];
1075
+ for (const key of nameKeys) {
1076
+ if (data[key] && typeof data[key] === 'string') {
1077
+ enriched[`${key}Camel`] = toCamelCase(data[key]);
1078
+ enriched[`${key}Kebab`] = toKebabCase(data[key]);
1079
+ enriched[`${key}Snake`] = toSnakeCase(data[key]);
1080
+ enriched[`${key}Upper`] = toScreamingSnakeCase(data[key]);
1081
+ enriched[`${key}Pascal`] = toPascalCase(data[key]);
1082
+ }
1083
+ }
1084
+ return enriched;
1085
+ }
1086
+
1047
1087
  function getTemplateOverride(templateName, extension, data = {}) {
1048
1088
  const overridePath = path.join(process.cwd(), '.textor', 'templates', `${templateName}${extension}`);
1049
1089
  if (existsSync(overridePath)) {
1050
1090
  let content = readFileSync(overridePath, 'utf-8');
1051
- for (const [key, value] of Object.entries(data)) {
1052
- content = content.replace(new RegExp(`{{${key}}}`, 'g'), () => value || '');
1091
+ const finalData = enrichData(data);
1092
+ for (const [key, value] of Object.entries(finalData)) {
1093
+ const replacement = () => value || '';
1094
+ content = content.replace(new RegExp(`{{${key}}}`, 'g'), replacement);
1095
+ content = content.replace(new RegExp(`__${key}__`, 'g'), replacement);
1053
1096
  }
1054
1097
  return content;
1055
1098
  }
@@ -1063,8 +1106,8 @@ function getTemplateOverride(templateName, extension, data = {}) {
1063
1106
  * - featureImportPath: Path to import the feature component
1064
1107
  * - featureComponentName: Name of the feature component
1065
1108
  */
1066
- function generateRouteTemplate(layoutName, layoutImportPath, featureImportPath, featureComponentName) {
1067
- const override = getTemplateOverride('route', '.astro', {
1109
+ function generateRouteTemplate(layoutName, layoutImportPath, featureImportPath, featureComponentName, extension = '.astro') {
1110
+ const override = getTemplateOverride('route', extension, {
1068
1111
  layoutName,
1069
1112
  layoutImportPath,
1070
1113
  featureImportPath,
@@ -1097,9 +1140,9 @@ import ${featureComponentName} from '${featureImportPath}';
1097
1140
  * - componentName: Name of the feature component
1098
1141
  * - scriptImportPath: Path to the feature's client-side script
1099
1142
  */
1100
- function generateFeatureTemplate(componentName, scriptImportPath, framework = 'astro') {
1101
- const extension = framework === 'astro' ? '.astro' : '.tsx';
1102
- const override = getTemplateOverride('feature', extension, { componentName, scriptImportPath });
1143
+ function generateFeatureTemplate(componentName, scriptImportPath, framework = 'astro', extension) {
1144
+ const templateExtension = extension || (framework === 'astro' ? '.astro' : '.tsx');
1145
+ const override = getTemplateOverride('feature', templateExtension, { componentName, scriptImportPath });
1103
1146
  if (override) return override;
1104
1147
 
1105
1148
  if (framework === 'react') {
@@ -1144,9 +1187,9 @@ function generateScriptsIndexTemplate() {
1144
1187
  * Component Template Variables:
1145
1188
  * - componentName: Name of the component
1146
1189
  */
1147
- function generateComponentTemplate(componentName, framework = 'react') {
1148
- const extension = framework === 'astro' ? '.astro' : '.tsx';
1149
- const override = getTemplateOverride('component', extension, { componentName });
1190
+ function generateComponentTemplate(componentName, framework = 'react', extension) {
1191
+ const templateExtension = extension || (framework === 'astro' ? '.astro' : '.tsx');
1192
+ const override = getTemplateOverride('component', templateExtension, { componentName });
1150
1193
  if (override) return override;
1151
1194
 
1152
1195
  if (framework === 'react') {
@@ -1989,13 +2032,14 @@ async function addSectionCommand(route, featurePath, options) {
1989
2032
  options.layout,
1990
2033
  layoutImportPath,
1991
2034
  featureImportPath,
1992
- featureComponentName
2035
+ featureComponentName,
2036
+ routeExtension
1993
2037
  );
1994
2038
  routeSignature = getSignature(config, 'astro');
1995
2039
  }
1996
2040
  }
1997
2041
 
1998
- const featureContent = generateFeatureTemplate(featureComponentName, scriptImportPath, framework);
2042
+ const featureContent = generateFeatureTemplate(featureComponentName, scriptImportPath, framework, config.naming.featureExtension);
1999
2043
 
2000
2044
  const writtenFiles = [];
2001
2045
 
@@ -3060,7 +3104,7 @@ async function createComponentCommand(componentName, options) {
3060
3104
  if (shouldCreateServices) await ensureDir(servicesDirInside);
3061
3105
  if (shouldCreateSchemas) await ensureDir(schemasDirInside);
3062
3106
 
3063
- const componentContent = generateComponentTemplate(normalizedName, framework);
3107
+ const componentContent = generateComponentTemplate(normalizedName, framework, config.naming.componentExtension);
3064
3108
  const signature = getSignature(config, config.naming.componentExtension === '.astro' ? 'astro' : 'tsx');
3065
3109
 
3066
3110
  const componentHash = await writeFileWithSignature(
@@ -3363,13 +3407,14 @@ async function removeComponentCommand(identifier, options) {
3363
3407
  await cleanupEmptyDirs(path.dirname(componentDir), path.join(process.cwd(), config.paths.components));
3364
3408
 
3365
3409
  // Unregister files
3366
- const dirPrefix = path.relative(process.cwd(), componentDir).replace(/\\/g, '/') + '/';
3410
+ const relComponentPath = path.relative(process.cwd(), componentDir).replace(/\\/g, '/');
3411
+ const dirPrefix = relComponentPath + '/';
3367
3412
  for (const f in state.files) {
3368
3413
  if (f.startsWith(dirPrefix)) {
3369
3414
  delete state.files[f];
3370
3415
  }
3371
3416
  }
3372
- state.components = state.components.filter(c => c.name !== identifier && c.path !== componentDir);
3417
+ state.components = state.components.filter(c => c.name !== identifier && c.path !== relComponentPath);
3373
3418
  await saveState(state);
3374
3419
  } else if (result.message) {
3375
3420
  console.log(`⚠ Skipped: ${componentDir}`);
@@ -4103,6 +4148,7 @@ program
4103
4148
  .command('init')
4104
4149
  .description('Initialize Textor configuration')
4105
4150
  .option('--force', 'Overwrite existing configuration')
4151
+ .option('--quiet', 'Skip printing full default configuration')
4106
4152
  .action(initCommand);
4107
4153
 
4108
4154
  program
@@ -1 +1 @@
1
- {"version":3,"file":"textor.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"textor.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
package/dist/index.cjs CHANGED
@@ -421,11 +421,14 @@ function getEffectiveOptions(cmdOptions, config, type) {
421
421
  async function initCommand(options) {
422
422
  try {
423
423
  const configPath = await saveConfig(DEFAULT_CONFIG, options.force);
424
- console.log(' Textor configuration created at:', configPath);
425
- console.log('\nDefault configuration:');
426
- console.log(JSON.stringify(DEFAULT_CONFIG, null, 2));
427
- console.log('\nYou can now use Textor commands like:');
428
- console.log(' textor add-section /users users/catalog --layout Main');
424
+ const quiet = options?.quiet || process.env.NODE_ENV === 'test' || process.env.TEXTOR_QUIET === '1';
425
+ if (!quiet) {
426
+ console.log('Textor configuration created at:', configPath);
427
+ console.log('\nDefault configuration:');
428
+ console.log(JSON.stringify(DEFAULT_CONFIG, null, 2));
429
+ console.log('\nYou can now use Textor commands like:');
430
+ console.log(' textor add-section /users users/catalog --layout Main');
431
+ }
429
432
  }
430
433
  catch (error) {
431
434
  console.error('Error:', error.message);
@@ -445,6 +448,24 @@ function toPascalCase(input) {
445
448
  })
446
449
  .join('');
447
450
  }
451
+ function toCamelCase(input) {
452
+ const pascal = toPascalCase(input);
453
+ return pascal.charAt(0).toLowerCase() + pascal.slice(1);
454
+ }
455
+ function toKebabCase(input) {
456
+ return input
457
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
458
+ .replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
459
+ .replace(/[_\s/\\-]+/g, '-')
460
+ .toLowerCase()
461
+ .replace(/^-+|-+$/g, '');
462
+ }
463
+ function toSnakeCase(input) {
464
+ return toKebabCase(input).replace(/-/g, '_');
465
+ }
466
+ function toScreamingSnakeCase(input) {
467
+ return toSnakeCase(input).toUpperCase();
468
+ }
448
469
  function getFeatureComponentName(featurePath) {
449
470
  return toPascalCase(featurePath);
450
471
  }
@@ -900,12 +921,29 @@ function resolvePatternedPath(baseDir, pattern, data, fallback, label) {
900
921
  return secureJoin(baseDir, fileName);
901
922
  }
902
923
 
924
+ function enrichData(data) {
925
+ const enriched = { ...data };
926
+ const nameKeys = ['componentName', 'featureComponentName', 'layoutName'];
927
+ for (const key of nameKeys) {
928
+ if (data[key] && typeof data[key] === 'string') {
929
+ enriched[`${key}Camel`] = toCamelCase(data[key]);
930
+ enriched[`${key}Kebab`] = toKebabCase(data[key]);
931
+ enriched[`${key}Snake`] = toSnakeCase(data[key]);
932
+ enriched[`${key}Upper`] = toScreamingSnakeCase(data[key]);
933
+ enriched[`${key}Pascal`] = toPascalCase(data[key]);
934
+ }
935
+ }
936
+ return enriched;
937
+ }
903
938
  function getTemplateOverride(templateName, extension, data = {}) {
904
939
  const overridePath = path.join(process.cwd(), '.textor', 'templates', `${templateName}${extension}`);
905
940
  if (fs.existsSync(overridePath)) {
906
941
  let content = fs.readFileSync(overridePath, 'utf-8');
907
- for (const [key, value] of Object.entries(data)) {
908
- content = content.replace(new RegExp(`{{${key}}}`, 'g'), () => value || '');
942
+ const finalData = enrichData(data);
943
+ for (const [key, value] of Object.entries(finalData)) {
944
+ const replacement = () => value || '';
945
+ content = content.replace(new RegExp(`{{${key}}}`, 'g'), replacement);
946
+ content = content.replace(new RegExp(`__${key}__`, 'g'), replacement);
909
947
  }
910
948
  return content;
911
949
  }
@@ -918,8 +956,8 @@ function getTemplateOverride(templateName, extension, data = {}) {
918
956
  * - featureImportPath: Path to import the feature component
919
957
  * - featureComponentName: Name of the feature component
920
958
  */
921
- function generateRouteTemplate(layoutName, layoutImportPath, featureImportPath, featureComponentName) {
922
- const override = getTemplateOverride('route', '.astro', {
959
+ function generateRouteTemplate(layoutName, layoutImportPath, featureImportPath, featureComponentName, extension = '.astro') {
960
+ const override = getTemplateOverride('route', extension, {
923
961
  layoutName,
924
962
  layoutImportPath,
925
963
  featureImportPath,
@@ -950,9 +988,9 @@ import ${featureComponentName} from '${featureImportPath}';
950
988
  * - componentName: Name of the feature component
951
989
  * - scriptImportPath: Path to the feature's client-side script
952
990
  */
953
- function generateFeatureTemplate(componentName, scriptImportPath, framework = 'astro') {
954
- const extension = framework === 'astro' ? '.astro' : '.tsx';
955
- const override = getTemplateOverride('feature', extension, { componentName, scriptImportPath });
991
+ function generateFeatureTemplate(componentName, scriptImportPath, framework = 'astro', extension) {
992
+ const templateExtension = extension || (framework === 'astro' ? '.astro' : '.tsx');
993
+ const override = getTemplateOverride('feature', templateExtension, { componentName, scriptImportPath });
956
994
  if (override)
957
995
  return override;
958
996
  if (framework === 'react') {
@@ -993,9 +1031,9 @@ function generateScriptsIndexTemplate() {
993
1031
  * Component Template Variables:
994
1032
  * - componentName: Name of the component
995
1033
  */
996
- function generateComponentTemplate(componentName, framework = 'react') {
997
- const extension = framework === 'astro' ? '.astro' : '.tsx';
998
- const override = getTemplateOverride('component', extension, { componentName });
1034
+ function generateComponentTemplate(componentName, framework = 'react', extension) {
1035
+ const templateExtension = extension || (framework === 'astro' ? '.astro' : '.tsx');
1036
+ const override = getTemplateOverride('component', templateExtension, { componentName });
999
1037
  if (override)
1000
1038
  return override;
1001
1039
  if (framework === 'react') {
@@ -1720,11 +1758,11 @@ async function addSectionCommand(route, featurePath, options) {
1720
1758
  routeSignature = getSignature(config, 'typescript');
1721
1759
  }
1722
1760
  else {
1723
- routeContent = generateRouteTemplate(options.layout, layoutImportPath, featureImportPath, featureComponentName);
1761
+ routeContent = generateRouteTemplate(options.layout, layoutImportPath, featureImportPath, featureComponentName, routeExtension);
1724
1762
  routeSignature = getSignature(config, 'astro');
1725
1763
  }
1726
1764
  }
1727
- const featureContent = generateFeatureTemplate(featureComponentName, scriptImportPath, framework);
1765
+ const featureContent = generateFeatureTemplate(featureComponentName, scriptImportPath, framework, config.naming.featureExtension);
1728
1766
  const writtenFiles = [];
1729
1767
  if (routeFilePath) {
1730
1768
  const routeHash = await writeFileWithSignature(routeFilePath, routeContent, routeSignature, config.hashing?.normalization);
@@ -2556,7 +2594,7 @@ async function createComponentCommand(componentName, options) {
2556
2594
  await ensureDir(servicesDirInside);
2557
2595
  if (shouldCreateSchemas)
2558
2596
  await ensureDir(schemasDirInside);
2559
- const componentContent = generateComponentTemplate(normalizedName, framework);
2597
+ const componentContent = generateComponentTemplate(normalizedName, framework, config.naming.componentExtension);
2560
2598
  const signature = getSignature(config, config.naming.componentExtension === '.astro' ? 'astro' : 'tsx');
2561
2599
  const componentHash = await writeFileWithSignature(componentFilePath, componentContent, signature, config.hashing?.normalization);
2562
2600
  await registerFile(componentFilePath, {
@@ -2780,13 +2818,14 @@ async function removeComponentCommand(identifier, options) {
2780
2818
  console.log(`✓ Deleted component: ${componentDir}/`);
2781
2819
  await cleanupEmptyDirs(path.dirname(componentDir), path.join(process.cwd(), config.paths.components));
2782
2820
  // Unregister files
2783
- const dirPrefix = path.relative(process.cwd(), componentDir).replace(/\\/g, '/') + '/';
2821
+ const relComponentPath = path.relative(process.cwd(), componentDir).replace(/\\/g, '/');
2822
+ const dirPrefix = relComponentPath + '/';
2784
2823
  for (const f in state.files) {
2785
2824
  if (f.startsWith(dirPrefix)) {
2786
2825
  delete state.files[f];
2787
2826
  }
2788
2827
  }
2789
- state.components = state.components.filter(c => c.name !== identifier && c.path !== componentDir);
2828
+ state.components = state.components.filter(c => c.name !== identifier && c.path !== relComponentPath);
2790
2829
  await saveState(state);
2791
2830
  }
2792
2831
  else if (result.message) {