@oamm/textor 1.0.2 → 1.0.4

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/dist/index.cjs CHANGED
@@ -9,8 +9,10 @@ var util = require('util');
9
9
 
10
10
  const CONFIG_DIR$1 = '.textor';
11
11
  const CONFIG_FILE = 'config.json';
12
+ const CURRENT_CONFIG_VERSION = 2;
12
13
  /**
13
14
  * @typedef {Object} TextorConfig
15
+ * @property {number} configVersion
14
16
  * @property {Object} paths
15
17
  * @property {string} paths.pages
16
18
  * @property {string} paths.features
@@ -47,6 +49,9 @@ const CONFIG_FILE = 'config.json';
47
49
  * @property {boolean} components.createTypes
48
50
  * @property {Object} formatting
49
51
  * @property {string} formatting.tool
52
+ * @property {Object} filePatterns
53
+ * @property {Object} filePatterns.features
54
+ * @property {Object} filePatterns.components
50
55
  * @property {Object} git
51
56
  * @property {boolean} git.requireCleanRepo
52
57
  * @property {boolean} git.stageChanges
@@ -58,6 +63,7 @@ const CONFIG_FILE = 'config.json';
58
63
  * @type {TextorConfig}
59
64
  */
60
65
  const DEFAULT_CONFIG = {
66
+ configVersion: CURRENT_CONFIG_VERSION,
61
67
  paths: {
62
68
  pages: 'src/pages',
63
69
  features: 'src/features',
@@ -117,6 +123,34 @@ const DEFAULT_CONFIG = {
117
123
  formatting: {
118
124
  tool: 'none' // 'prettier' | 'biome' | 'none'
119
125
  },
126
+ filePatterns: {
127
+ features: {
128
+ index: 'index.ts',
129
+ types: 'index.ts',
130
+ api: 'index.ts',
131
+ services: 'index.ts',
132
+ schemas: 'index.ts',
133
+ hook: '{{hookName}}{{hookExtension}}',
134
+ context: '{{componentName}}Context.tsx',
135
+ test: '{{componentName}}{{testExtension}}',
136
+ readme: 'README.md',
137
+ stories: '{{componentName}}.stories.tsx'
138
+ },
139
+ components: {
140
+ index: 'index.ts',
141
+ types: 'index.ts',
142
+ api: 'index.ts',
143
+ services: 'index.ts',
144
+ schemas: 'index.ts',
145
+ hook: '{{hookName}}{{hookExtension}}',
146
+ context: '{{componentName}}Context.tsx',
147
+ test: '{{componentName}}{{testExtension}}',
148
+ config: 'index.ts',
149
+ constants: 'index.ts',
150
+ readme: 'README.md',
151
+ stories: '{{componentName}}.stories.tsx'
152
+ }
153
+ },
120
154
  hashing: {
121
155
  normalization: 'normalizeEOL', // 'none' | 'normalizeEOL' | 'stripGeneratedRegions'
122
156
  useMarkers: false
@@ -218,7 +252,7 @@ async function loadConfig() {
218
252
  try {
219
253
  const content = await promises.readFile(configPath, 'utf-8');
220
254
  const config = JSON.parse(content);
221
- const merged = deepMerge(DEFAULT_CONFIG, config);
255
+ const merged = mergeConfig(DEFAULT_CONFIG, config);
222
256
  validateConfig(merged);
223
257
  return merged;
224
258
  }
@@ -250,6 +284,34 @@ async function saveConfig(config, force = false) {
250
284
  await promises.writeFile(configPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
251
285
  return configPath;
252
286
  }
287
+ function mergeConfig(defaults, config) {
288
+ return deepMerge(defaults, config);
289
+ }
290
+ function normalizeConfigVersion(config) {
291
+ if (!config || typeof config !== 'object')
292
+ return config;
293
+ if (typeof config.configVersion !== 'number') {
294
+ return { ...config, configVersion: 1 };
295
+ }
296
+ return config;
297
+ }
298
+ function applyConfigMigrations(config) {
299
+ let current = normalizeConfigVersion(config);
300
+ let version = current.configVersion || 1;
301
+ let migrated = { ...current };
302
+ while (version < CURRENT_CONFIG_VERSION) {
303
+ if (version === 1) {
304
+ migrated = { ...migrated, configVersion: 2 };
305
+ version = 2;
306
+ continue;
307
+ }
308
+ break;
309
+ }
310
+ if (migrated.configVersion !== CURRENT_CONFIG_VERSION) {
311
+ migrated.configVersion = CURRENT_CONFIG_VERSION;
312
+ }
313
+ return migrated;
314
+ }
253
315
  /**
254
316
  * Deeply merges source object into target object.
255
317
  * @param {Object} target
@@ -279,6 +341,9 @@ function validateConfig(config) {
279
341
  if (!config || typeof config !== 'object') {
280
342
  throw new Error('Invalid configuration: must be an object');
281
343
  }
344
+ if (config.configVersion !== undefined && typeof config.configVersion !== 'number') {
345
+ throw new Error('Invalid configuration: "configVersion" must be a number');
346
+ }
282
347
  const requiredSections = ['paths', 'naming', 'signatures', 'importAliases'];
283
348
  for (const section of requiredSections) {
284
349
  if (!config[section] || typeof config[section] !== 'object') {
@@ -288,6 +353,15 @@ function validateConfig(config) {
288
353
  if (config.kindRules && !Array.isArray(config.kindRules)) {
289
354
  throw new Error('Invalid configuration: "kindRules" must be an array');
290
355
  }
356
+ if (config.filePatterns && typeof config.filePatterns !== 'object') {
357
+ throw new Error('Invalid configuration: "filePatterns" must be an object');
358
+ }
359
+ if (config.filePatterns?.features && typeof config.filePatterns.features !== 'object') {
360
+ throw new Error('Invalid configuration: "filePatterns.features" must be an object');
361
+ }
362
+ if (config.filePatterns?.components && typeof config.filePatterns.components !== 'object') {
363
+ throw new Error('Invalid configuration: "filePatterns.components" must be an object');
364
+ }
291
365
  // Validate paths are strings
292
366
  for (const [key, value] of Object.entries(config.paths)) {
293
367
  if (typeof value !== 'string') {
@@ -799,6 +873,33 @@ var filesystem = /*#__PURE__*/Object.freeze({
799
873
  writeFileWithSignature: writeFileWithSignature
800
874
  });
801
875
 
876
+ function renderNamePattern(pattern, data = {}, label = 'pattern') {
877
+ if (typeof pattern !== 'string')
878
+ return null;
879
+ const trimmed = pattern.trim();
880
+ if (!trimmed)
881
+ return null;
882
+ const missing = new Set();
883
+ const rendered = trimmed.replace(/{{\s*([a-zA-Z0-9_]+)\s*}}/g, (match, key) => {
884
+ if (!Object.prototype.hasOwnProperty.call(data, key)) {
885
+ missing.add(key);
886
+ return '';
887
+ }
888
+ return String(data[key]);
889
+ });
890
+ if (missing.size > 0) {
891
+ throw new Error(`Invalid ${label}: missing values for ${Array.from(missing).join(', ')}`);
892
+ }
893
+ return rendered;
894
+ }
895
+ function resolvePatternedPath(baseDir, pattern, data, fallback, label) {
896
+ const fileName = renderNamePattern(pattern, data, label) || fallback;
897
+ if (!fileName) {
898
+ throw new Error(`Invalid ${label}: resolved to empty file name`);
899
+ }
900
+ return secureJoin(baseDir, fileName);
901
+ }
902
+
802
903
  function getTemplateOverride(templateName, extension, data = {}) {
803
904
  const overridePath = path.join(process.cwd(), '.textor', 'templates', `${templateName}${extension}`);
804
905
  if (fs.existsSync(overridePath)) {
@@ -1190,6 +1291,7 @@ async function loadState() {
1190
1291
  const state = JSON.parse(content);
1191
1292
  if (!state.files)
1192
1293
  state.files = {};
1294
+ normalizeStatePaths(state);
1193
1295
  return state;
1194
1296
  }
1195
1297
  catch (error) {
@@ -1230,23 +1332,67 @@ async function registerFile(filePath, { kind, template, hash, templateVersion =
1230
1332
  }
1231
1333
  async function addSectionToState(section) {
1232
1334
  const state = await loadState();
1335
+ const normalizedSection = { ...section };
1336
+ if (normalizedSection.featurePath) {
1337
+ normalizedSection.featurePath = normalizeStatePath(normalizedSection.featurePath);
1338
+ }
1233
1339
  // Avoid duplicates by route OR by featurePath if route is null
1234
- if (section.route) {
1235
- state.sections = state.sections.filter(s => s.route !== section.route);
1340
+ if (normalizedSection.route) {
1341
+ state.sections = state.sections.filter(s => s.route !== normalizedSection.route);
1236
1342
  }
1237
1343
  else {
1238
- state.sections = state.sections.filter(s => s.featurePath !== section.featurePath || s.route);
1344
+ state.sections = state.sections.filter(s => s.featurePath !== normalizedSection.featurePath || s.route);
1239
1345
  }
1240
- state.sections.push(section);
1346
+ state.sections.push(normalizedSection);
1241
1347
  await saveState(state);
1242
1348
  }
1243
1349
  async function addComponentToState(component) {
1244
1350
  const state = await loadState();
1351
+ const normalizedComponent = { ...component };
1352
+ if (normalizedComponent.path) {
1353
+ normalizedComponent.path = normalizeStatePath(normalizedComponent.path);
1354
+ }
1245
1355
  // Avoid duplicates by name
1246
- state.components = state.components.filter(c => c.name !== component.name);
1247
- state.components.push(component);
1356
+ state.components = state.components.filter(c => c.name !== normalizedComponent.name);
1357
+ state.components.push(normalizedComponent);
1248
1358
  await saveState(state);
1249
1359
  }
1360
+ function normalizeStatePath(filePath) {
1361
+ if (!filePath || typeof filePath !== 'string')
1362
+ return filePath;
1363
+ const relative = path.isAbsolute(filePath)
1364
+ ? path.relative(process.cwd(), filePath)
1365
+ : filePath;
1366
+ return relative.replace(/\\/g, '/');
1367
+ }
1368
+ function normalizeStatePaths(state) {
1369
+ if (!state || typeof state !== 'object')
1370
+ return;
1371
+ if (Array.isArray(state.sections)) {
1372
+ state.sections = state.sections.map(section => {
1373
+ if (!section || typeof section !== 'object')
1374
+ return section;
1375
+ if (!section.featurePath)
1376
+ return section;
1377
+ const normalized = normalizeStatePath(section.featurePath);
1378
+ if (normalized === section.featurePath)
1379
+ return section;
1380
+ return { ...section, featurePath: normalized };
1381
+ });
1382
+ }
1383
+ if (Array.isArray(state.components)) {
1384
+ state.components = state.components.map(component => {
1385
+ if (!component || typeof component !== 'object')
1386
+ return component;
1387
+ if (!component.path)
1388
+ return component;
1389
+ const normalized = normalizeStatePath(component.path);
1390
+ if (normalized === component.path)
1391
+ return component;
1392
+ return { ...component, path: normalized };
1393
+ });
1394
+ }
1395
+ }
1250
1396
  function findSection(state, identifier) {
1251
1397
  return state.sections.find(s => s.route === identifier || s.name === identifier || s.featurePath === identifier);
1252
1398
  }
@@ -1407,16 +1553,25 @@ async function addSectionCommand(route, featurePath, options) {
1407
1553
  const servicesDirInside = secureJoin(featureDirPath, 'services');
1408
1554
  const schemasDirInside = secureJoin(featureDirPath, 'schemas');
1409
1555
  const { framework, createSubComponentsDir: shouldCreateSubComponentsDir, createScriptsDir: shouldCreateScriptsDir, createApi: shouldCreateApi, createServices: shouldCreateServices, createSchemas: shouldCreateSchemas, createHooks: shouldCreateHooks, createContext: shouldCreateContext, createTests: shouldCreateTests, createTypes: shouldCreateTypes, createReadme: shouldCreateReadme, createStories: shouldCreateStories, createIndex: shouldCreateIndex } = effectiveOptions;
1410
- const indexFilePath = path.join(featureDirPath, 'index.ts');
1411
- const contextFilePath = path.join(contextDirInside, `${featureComponentName}Context.tsx`);
1412
- const hookFilePath = path.join(hooksDirInside, getHookFileName(featureComponentName, config.naming.hookExtension));
1413
- const testFilePath = path.join(testsDir, `${featureComponentName}${config.naming.testExtension}`);
1414
- const typesFilePath = path.join(typesDirInside, 'index.ts');
1415
- const apiFilePath = path.join(apiDirInside, 'index.ts');
1416
- const servicesFilePath = path.join(servicesDirInside, 'index.ts');
1417
- const schemasFilePath = path.join(schemasDirInside, 'index.ts');
1418
- const readmeFilePath = path.join(featureDirPath, 'README.md');
1419
- const storiesFilePath = path.join(featureDirPath, `${featureComponentName}.stories.tsx`);
1556
+ const featurePatterns = config.filePatterns?.features || {};
1557
+ const patternData = {
1558
+ componentName: featureComponentName,
1559
+ hookName: getHookFunctionName(featureComponentName),
1560
+ hookExtension: config.naming.hookExtension,
1561
+ testExtension: config.naming.testExtension,
1562
+ featureExtension: config.naming.featureExtension,
1563
+ componentExtension: config.naming.componentExtension
1564
+ };
1565
+ const indexFilePath = resolvePatternedPath(featureDirPath, featurePatterns.index, patternData, 'index.ts', 'filePatterns.features.index');
1566
+ const contextFilePath = resolvePatternedPath(contextDirInside, featurePatterns.context, patternData, `${featureComponentName}Context.tsx`, 'filePatterns.features.context');
1567
+ const hookFilePath = resolvePatternedPath(hooksDirInside, featurePatterns.hook, patternData, getHookFileName(featureComponentName, config.naming.hookExtension), 'filePatterns.features.hook');
1568
+ const testFilePath = resolvePatternedPath(testsDir, featurePatterns.test, patternData, `${featureComponentName}${config.naming.testExtension}`, 'filePatterns.features.test');
1569
+ const typesFilePath = resolvePatternedPath(typesDirInside, featurePatterns.types, patternData, 'index.ts', 'filePatterns.features.types');
1570
+ const apiFilePath = resolvePatternedPath(apiDirInside, featurePatterns.api, patternData, 'index.ts', 'filePatterns.features.api');
1571
+ const servicesFilePath = resolvePatternedPath(servicesDirInside, featurePatterns.services, patternData, 'index.ts', 'filePatterns.features.services');
1572
+ const schemasFilePath = resolvePatternedPath(schemasDirInside, featurePatterns.schemas, patternData, 'index.ts', 'filePatterns.features.schemas');
1573
+ const readmeFilePath = resolvePatternedPath(featureDirPath, featurePatterns.readme, patternData, 'README.md', 'filePatterns.features.readme');
1574
+ const storiesFilePath = resolvePatternedPath(featureDirPath, featurePatterns.stories, patternData, `${featureComponentName}.stories.tsx`, 'filePatterns.features.stories');
1420
1575
  const routeParts = normalizedRoute ? normalizedRoute.split('/').filter(Boolean) : [];
1421
1576
  const reorganizations = [];
1422
1577
  if (normalizedRoute && routeParts.length > 1 && config.routing.mode === 'flat') {
@@ -2304,19 +2459,28 @@ async function createComponentCommand(componentName, options) {
2304
2459
  const servicesDirInside = secureJoin(componentDir, 'services');
2305
2460
  const schemasDirInside = secureJoin(componentDir, 'schemas');
2306
2461
  const { framework, createContext: shouldCreateContext, createHook: shouldCreateHook, createTests: shouldCreateTests, createConfig: shouldCreateConfig, createConstants: shouldCreateConstants, createTypes: shouldCreateTypes, createSubComponentsDir: shouldCreateSubComponentsDir, createApi: shouldCreateApi, createServices: shouldCreateServices, createSchemas: shouldCreateSchemas, createReadme: shouldCreateReadme, createStories: shouldCreateStories } = effectiveOptions;
2462
+ const componentPatterns = config.filePatterns?.components || {};
2463
+ const patternData = {
2464
+ componentName: normalizedName,
2465
+ hookName: getHookFunctionName(normalizedName),
2466
+ hookExtension: config.naming.hookExtension,
2467
+ testExtension: config.naming.testExtension,
2468
+ componentExtension: config.naming.componentExtension,
2469
+ featureExtension: config.naming.featureExtension
2470
+ };
2307
2471
  const componentFilePath = path.join(componentDir, `${normalizedName}${config.naming.componentExtension}`);
2308
- const indexFilePath = path.join(componentDir, 'index.ts');
2309
- const contextFilePath = path.join(contextDirInside, `${normalizedName}Context.tsx`);
2310
- const hookFilePath = path.join(hooksDirInside, getHookFileName(normalizedName, config.naming.hookExtension));
2311
- const testFilePath = path.join(testsDir, `${normalizedName}${config.naming.testExtension}`);
2312
- const configFilePath = path.join(configDirInside, 'index.ts');
2313
- const constantsFilePath = path.join(constantsDirInside, 'index.ts');
2314
- const typesFilePath = path.join(typesDirInside, 'index.ts');
2315
- const apiFilePath = path.join(apiDirInside, 'index.ts');
2316
- const servicesFilePath = path.join(servicesDirInside, 'index.ts');
2317
- const schemasFilePath = path.join(schemasDirInside, 'index.ts');
2318
- const readmeFilePath = path.join(componentDir, 'README.md');
2319
- const storiesFilePath = path.join(componentDir, `${normalizedName}.stories.tsx`);
2472
+ const indexFilePath = resolvePatternedPath(componentDir, componentPatterns.index, patternData, 'index.ts', 'filePatterns.components.index');
2473
+ const contextFilePath = resolvePatternedPath(contextDirInside, componentPatterns.context, patternData, `${normalizedName}Context.tsx`, 'filePatterns.components.context');
2474
+ const hookFilePath = resolvePatternedPath(hooksDirInside, componentPatterns.hook, patternData, getHookFileName(normalizedName, config.naming.hookExtension), 'filePatterns.components.hook');
2475
+ const testFilePath = resolvePatternedPath(testsDir, componentPatterns.test, patternData, `${normalizedName}${config.naming.testExtension}`, 'filePatterns.components.test');
2476
+ const configFilePath = resolvePatternedPath(configDirInside, componentPatterns.config, patternData, 'index.ts', 'filePatterns.components.config');
2477
+ const constantsFilePath = resolvePatternedPath(constantsDirInside, componentPatterns.constants, patternData, 'index.ts', 'filePatterns.components.constants');
2478
+ const typesFilePath = resolvePatternedPath(typesDirInside, componentPatterns.types, patternData, 'index.ts', 'filePatterns.components.types');
2479
+ const apiFilePath = resolvePatternedPath(apiDirInside, componentPatterns.api, patternData, 'index.ts', 'filePatterns.components.api');
2480
+ const servicesFilePath = resolvePatternedPath(servicesDirInside, componentPatterns.services, patternData, 'index.ts', 'filePatterns.components.services');
2481
+ const schemasFilePath = resolvePatternedPath(schemasDirInside, componentPatterns.schemas, patternData, 'index.ts', 'filePatterns.components.schemas');
2482
+ const readmeFilePath = resolvePatternedPath(componentDir, componentPatterns.readme, patternData, 'README.md', 'filePatterns.components.readme');
2483
+ const storiesFilePath = resolvePatternedPath(componentDir, componentPatterns.stories, patternData, `${normalizedName}.stories.tsx`, 'filePatterns.components.stories');
2320
2484
  if (options.dryRun) {
2321
2485
  console.log('Dry run - would create:');
2322
2486
  console.log(` Component: ${componentFilePath}`);
@@ -2593,7 +2757,7 @@ async function removeComponentCommand(identifier, options) {
2593
2757
  const component = findComponent(state, identifier);
2594
2758
  let componentDir;
2595
2759
  if (component) {
2596
- componentDir = component.path;
2760
+ componentDir = path.resolve(process.cwd(), component.path);
2597
2761
  }
2598
2762
  else {
2599
2763
  // Fallback: try to guess path if not in state
@@ -3220,15 +3384,74 @@ async function scanDirectoryOrFile(fullPath, fileSet, state) {
3220
3384
  }
3221
3385
  }
3222
3386
 
3387
+ async function upgradeConfigCommand(options) {
3388
+ try {
3389
+ const configPath = getConfigPath();
3390
+ if (!fs.existsSync(configPath)) {
3391
+ throw new Error(`Textor configuration not found at ${configPath}\n` +
3392
+ `Run 'textor init' to create it.`);
3393
+ }
3394
+ const rawContent = await promises.readFile(configPath, 'utf-8');
3395
+ const rawConfig = JSON.parse(rawContent);
3396
+ const migrated = applyConfigMigrations(rawConfig);
3397
+ const merged = mergeConfig(DEFAULT_CONFIG, migrated);
3398
+ merged.configVersion = CURRENT_CONFIG_VERSION;
3399
+ validateConfig(merged);
3400
+ if (options.dryRun) {
3401
+ console.log('Dry run - upgraded configuration:');
3402
+ console.log(JSON.stringify(merged, null, 2));
3403
+ return;
3404
+ }
3405
+ await promises.writeFile(configPath, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
3406
+ console.log('Configuration upgraded successfully.');
3407
+ console.log(` Version: ${rawConfig.configVersion || 1} -> ${CURRENT_CONFIG_VERSION}`);
3408
+ console.log(` Path: ${configPath}`);
3409
+ }
3410
+ catch (error) {
3411
+ if (error instanceof SyntaxError) {
3412
+ console.error('Error: Failed to parse config: Invalid JSON');
3413
+ }
3414
+ else {
3415
+ console.error('Error:', error.message);
3416
+ }
3417
+ if (typeof process.exit === 'function' && process.env.NODE_ENV !== 'test') {
3418
+ process.exit(1);
3419
+ }
3420
+ throw error;
3421
+ }
3422
+ }
3423
+
3424
+ async function normalizeStateCommand(options) {
3425
+ try {
3426
+ const state = await loadState();
3427
+ if (options.dryRun) {
3428
+ console.log('Dry run - normalized state:');
3429
+ console.log(JSON.stringify(state, null, 2));
3430
+ return;
3431
+ }
3432
+ await saveState(state);
3433
+ console.log('State normalized successfully.');
3434
+ }
3435
+ catch (error) {
3436
+ console.error('Error:', error.message);
3437
+ if (typeof process.exit === 'function' && process.env.NODE_ENV !== 'test') {
3438
+ process.exit(1);
3439
+ }
3440
+ throw error;
3441
+ }
3442
+ }
3443
+
3223
3444
  exports.addSectionCommand = addSectionCommand;
3224
3445
  exports.adoptCommand = adoptCommand;
3225
3446
  exports.createComponentCommand = createComponentCommand;
3226
3447
  exports.initCommand = initCommand;
3227
3448
  exports.listSectionsCommand = listSectionsCommand;
3228
3449
  exports.moveSectionCommand = moveSectionCommand;
3450
+ exports.normalizeStateCommand = normalizeStateCommand;
3229
3451
  exports.removeComponentCommand = removeComponentCommand;
3230
3452
  exports.removeSectionCommand = removeSectionCommand;
3231
3453
  exports.statusCommand = statusCommand;
3232
3454
  exports.syncCommand = syncCommand;
3455
+ exports.upgradeConfigCommand = upgradeConfigCommand;
3233
3456
  exports.validateStateCommand = validateStateCommand;
3234
3457
  //# sourceMappingURL=index.cjs.map