@oamm/textor 1.0.8 → 1.0.10

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
@@ -981,6 +981,66 @@ ${layoutOpening}
981
981
  </${layoutName}>
982
982
  `;
983
983
  }
984
+ function mergeRouteTemplate(existingContent, featureImportPath, featureComponentName, layoutName) {
985
+ let content = existingContent;
986
+ // 1. Add import
987
+ const importLine = `import ${featureComponentName} from '${featureImportPath}';`;
988
+ if (!content.includes(importLine)) {
989
+ // Find the second --- which marks the end of frontmatter
990
+ const lines = content.split('\n');
991
+ let frontMatterEndLine = -1;
992
+ let dashCount = 0;
993
+ for (let i = 0; i < lines.length; i++) {
994
+ if (lines[i].trim() === '---') {
995
+ dashCount++;
996
+ if (dashCount === 2) {
997
+ frontMatterEndLine = i;
998
+ break;
999
+ }
1000
+ }
1001
+ }
1002
+ if (frontMatterEndLine !== -1) {
1003
+ lines.splice(frontMatterEndLine, 0, importLine);
1004
+ content = lines.join('\n');
1005
+ }
1006
+ else if (content.includes('---')) {
1007
+ // If only one --- found, maybe it's just the start?
1008
+ // But standard Astro has two.
1009
+ // Fallback: insert at the beginning if no frontmatter end found
1010
+ content = importLine + '\n' + content;
1011
+ }
1012
+ }
1013
+ // 2. Add component usage
1014
+ const componentTag = `<${featureComponentName} />`;
1015
+ if (!content.includes(componentTag)) {
1016
+ if (layoutName && layoutName !== 'none') {
1017
+ const layoutEndTag = `</${layoutName}>`;
1018
+ if (content.includes(layoutEndTag)) {
1019
+ const lines = content.split('\n');
1020
+ let layoutEndLine = -1;
1021
+ for (let i = lines.length - 1; i >= 0; i--) {
1022
+ if (lines[i].includes(layoutEndTag)) {
1023
+ layoutEndLine = i;
1024
+ break;
1025
+ }
1026
+ }
1027
+ if (layoutEndLine !== -1) {
1028
+ lines.splice(layoutEndLine, 0, ` ${componentTag}`);
1029
+ content = lines.join('\n');
1030
+ }
1031
+ }
1032
+ else {
1033
+ // Layout might be self-closing or missing end tag?
1034
+ // If it's Textor generated it should have it.
1035
+ content += `\n${componentTag}\n`;
1036
+ }
1037
+ }
1038
+ else {
1039
+ content += `\n${componentTag}\n`;
1040
+ }
1041
+ }
1042
+ return content;
1043
+ }
984
1044
  /**
985
1045
  * Feature Template Variables:
986
1046
  * - componentName: Name of the feature component
@@ -1376,9 +1436,9 @@ async function addSectionToState(section) {
1376
1436
  if (normalizedSection.featurePath) {
1377
1437
  normalizedSection.featurePath = normalizeStatePath(normalizedSection.featurePath);
1378
1438
  }
1379
- // Avoid duplicates by route OR by featurePath if route is null
1439
+ // Avoid duplicates by route AND by featurePath
1380
1440
  if (normalizedSection.route) {
1381
- state.sections = state.sections.filter(s => s.route !== normalizedSection.route);
1441
+ state.sections = state.sections.filter(s => s.route !== normalizedSection.route || s.featurePath !== normalizedSection.featurePath);
1382
1442
  }
1383
1443
  else {
1384
1444
  state.sections = state.sections.filter(s => s.featurePath !== normalizedSection.featurePath || s.route);
@@ -1458,6 +1518,10 @@ function reconstructComponents(files, config) {
1458
1518
  path: componentPath
1459
1519
  });
1460
1520
  }
1521
+ // Attribute ownership
1522
+ if (!files[filePath].owner) {
1523
+ files[filePath].owner = componentName;
1524
+ }
1461
1525
  }
1462
1526
  }
1463
1527
  }
@@ -1491,25 +1555,34 @@ function reconstructSections(state, config) {
1491
1555
  const finalRoute = route === '' ? '/' : route;
1492
1556
  if (!sections.has(finalRoute)) {
1493
1557
  // Try to find a matching feature by name
1494
- const routeName = path.basename(finalRoute === '/' ? 'index' : finalRoute);
1558
+ const routeName = path.basename(finalRoute === '/' ? 'index' : finalRoute).toLowerCase();
1495
1559
  // Look for a directory in features with same name or similar
1496
1560
  const possibleFeaturePath = Object.keys(files).find(f => {
1497
1561
  const nf = f.replace(/\\/g, '/');
1498
- return nf.startsWith(featuresRoot + '/') && nf.includes('/' + routeName + '/');
1562
+ if (!nf.startsWith(featuresRoot + '/'))
1563
+ return false;
1564
+ const relToFeatures = nf.slice(featuresRoot.length + 1);
1565
+ const segments = relToFeatures.toLowerCase().split('/');
1566
+ return segments.includes(routeName);
1499
1567
  });
1500
1568
  if (possibleFeaturePath) {
1501
- const featurePathParts = possibleFeaturePath.replace(/\\/g, '/').split('/');
1502
- const featuresBase = path.basename(featuresRoot);
1503
- const featureIndex = featurePathParts.indexOf(featuresBase) + 1;
1504
- if (featureIndex > 0 && featureIndex < featurePathParts.length) {
1505
- const featureName = featurePathParts[featureIndex];
1506
- const featurePath = `${featuresRoot}/${featureName}`;
1507
- sections.set(finalRoute, {
1508
- name: featureName,
1509
- route: finalRoute,
1510
- featurePath: featurePath,
1511
- extension: path.extname(filePath)
1512
- });
1569
+ const relToFeatures = path.dirname(path.relative(featuresRoot, possibleFeaturePath)).replace(/\\/g, '/');
1570
+ const featurePath = relToFeatures === '.' ? featuresRoot : `${featuresRoot}/${relToFeatures}`;
1571
+ const featureName = path.basename(featurePath);
1572
+ sections.set(finalRoute, {
1573
+ name: featureName,
1574
+ route: finalRoute,
1575
+ featurePath: featurePath,
1576
+ extension: path.extname(filePath)
1577
+ });
1578
+ // Attribute ownership to discovered files
1579
+ if (!files[filePath].owner)
1580
+ files[filePath].owner = finalRoute;
1581
+ for (const f in files) {
1582
+ if (f.startsWith(featurePath + '/') || f === featurePath) {
1583
+ if (!files[f].owner)
1584
+ files[f].owner = finalRoute;
1585
+ }
1513
1586
  }
1514
1587
  }
1515
1588
  }
@@ -1699,8 +1772,14 @@ async function addSectionCommand(route, featurePath, options) {
1699
1772
  }
1700
1773
  await saveState(state);
1701
1774
  }
1702
- if (routeFilePath)
1703
- await ensureNotExists(routeFilePath, options.force);
1775
+ if (routeFilePath) {
1776
+ if (fs.existsSync(routeFilePath)) {
1777
+ const isGenerated = await isTextorGenerated(routeFilePath);
1778
+ if (!isGenerated && !options.force) {
1779
+ throw new Error(`File already exists: ${routeFilePath}\nUse --force to overwrite.`);
1780
+ }
1781
+ }
1782
+ }
1704
1783
  await ensureNotExists(featureFilePath, options.force);
1705
1784
  if (shouldCreateIndex)
1706
1785
  await ensureNotExists(indexFilePath, options.force);
@@ -1789,8 +1868,30 @@ async function addSectionCommand(route, featurePath, options) {
1789
1868
  routeSignature = getSignature(config, 'typescript');
1790
1869
  }
1791
1870
  else {
1792
- routeContent = generateRouteTemplate(layout, layoutImportPath, featureImportPath, featureComponentName, routeExtension, layoutProps);
1793
1871
  routeSignature = getSignature(config, 'astro');
1872
+ if (fs.existsSync(routeFilePath)) {
1873
+ const existingContent = await promises.readFile(routeFilePath, 'utf-8');
1874
+ // Strip existing signature if present
1875
+ let contentToMerge = existingContent;
1876
+ if (existingContent.startsWith(routeSignature)) {
1877
+ contentToMerge = existingContent.slice(routeSignature.length).trimStart();
1878
+ }
1879
+ else {
1880
+ // Check for generic signature if specific one doesn't match
1881
+ const genericSignature = '@generated by Textor';
1882
+ if (existingContent.includes(genericSignature)) {
1883
+ const lines = existingContent.split('\n');
1884
+ if (lines[0].includes(genericSignature)) {
1885
+ lines.shift();
1886
+ contentToMerge = lines.join('\n').trimStart();
1887
+ }
1888
+ }
1889
+ }
1890
+ routeContent = mergeRouteTemplate(contentToMerge, featureImportPath, featureComponentName, layout);
1891
+ }
1892
+ else {
1893
+ routeContent = generateRouteTemplate(layout, layoutImportPath, featureImportPath, featureComponentName, routeExtension, layoutProps);
1894
+ }
1794
1895
  }
1795
1896
  }
1796
1897
  const featureContent = generateFeatureTemplate(featureComponentName, scriptImportPath, framework, config.naming.featureExtension);
@@ -3208,58 +3309,77 @@ async function validateStateCommand(options) {
3208
3309
  }
3209
3310
  }
3210
3311
 
3312
+ /**
3313
+ * Computes the drift between the state and the actual files on disk.
3314
+ *
3315
+ * @param {import('./config.js').TextorConfig} config
3316
+ * @param {Object} state
3317
+ * @returns {Promise<{
3318
+ * missing: string[],
3319
+ * modified: string[],
3320
+ * untracked: string[],
3321
+ * orphaned: string[],
3322
+ * synced: number
3323
+ * }>}
3324
+ */
3325
+ async function getProjectStatus(config, state) {
3326
+ const results = {
3327
+ missing: [],
3328
+ modified: [],
3329
+ untracked: [], // Has signature, not in state
3330
+ orphaned: [], // No signature, not in state
3331
+ synced: 0
3332
+ };
3333
+ const roots = [
3334
+ resolvePath(config, 'pages'),
3335
+ resolvePath(config, 'features'),
3336
+ resolvePath(config, 'components')
3337
+ ].map(p => path.resolve(p));
3338
+ const diskFiles = new Set();
3339
+ const configSignatures = Object.values(config.signatures || {});
3340
+ for (const root of roots) {
3341
+ if (fs.existsSync(root)) {
3342
+ await scanDirectory(root, diskFiles);
3343
+ }
3344
+ }
3345
+ // 1. Check state files against disk
3346
+ for (const relativePath in state.files) {
3347
+ const fullPath = path.join(process.cwd(), relativePath);
3348
+ if (!fs.existsSync(fullPath)) {
3349
+ results.missing.push(relativePath);
3350
+ continue;
3351
+ }
3352
+ // It exists on disk, so it's not untracked/orphaned
3353
+ diskFiles.delete(relativePath);
3354
+ const content = await promises.readFile(fullPath, 'utf-8');
3355
+ const currentHash = calculateHash(content, config.hashing?.normalization);
3356
+ const fileData = state.files[relativePath];
3357
+ if (currentHash !== fileData.hash) {
3358
+ results.modified.push(relativePath);
3359
+ }
3360
+ else {
3361
+ results.synced++;
3362
+ }
3363
+ }
3364
+ // 2. Check remaining disk files
3365
+ for (const relativePath of diskFiles) {
3366
+ const fullPath = path.join(process.cwd(), relativePath);
3367
+ const isGenerated = await isTextorGenerated(fullPath, configSignatures);
3368
+ if (isGenerated) {
3369
+ results.untracked.push(relativePath);
3370
+ }
3371
+ else {
3372
+ results.orphaned.push(relativePath);
3373
+ }
3374
+ }
3375
+ return results;
3376
+ }
3377
+
3211
3378
  async function statusCommand() {
3212
3379
  try {
3213
3380
  const config = await loadConfig();
3214
3381
  const state = await loadState();
3215
- const results = {
3216
- missing: [],
3217
- modified: [],
3218
- untracked: [], // Has signature, not in state
3219
- orphaned: [], // No signature, not in state
3220
- synced: 0
3221
- };
3222
- const roots = [
3223
- resolvePath(config, 'pages'),
3224
- resolvePath(config, 'features'),
3225
- resolvePath(config, 'components')
3226
- ].map(p => path.resolve(p));
3227
- const diskFiles = new Set();
3228
- const configSignatures = Object.values(config.signatures || {});
3229
- for (const root of roots) {
3230
- if (fs.existsSync(root)) {
3231
- await scanDirectory(root, diskFiles);
3232
- }
3233
- }
3234
- // 1. Check state files against disk
3235
- for (const relativePath in state.files) {
3236
- const fullPath = path.join(process.cwd(), relativePath);
3237
- if (!fs.existsSync(fullPath)) {
3238
- results.missing.push(relativePath);
3239
- continue;
3240
- }
3241
- diskFiles.delete(relativePath);
3242
- const content = await promises.readFile(fullPath, 'utf-8');
3243
- const currentHash = calculateHash(content, config.hashing?.normalization);
3244
- const fileData = state.files[relativePath];
3245
- if (currentHash !== fileData.hash) {
3246
- results.modified.push(relativePath);
3247
- }
3248
- else {
3249
- results.synced++;
3250
- }
3251
- }
3252
- // 2. Check remaining disk files
3253
- for (const relativePath of diskFiles) {
3254
- const fullPath = path.join(process.cwd(), relativePath);
3255
- const isGenerated = await isTextorGenerated(fullPath, configSignatures);
3256
- if (isGenerated) {
3257
- results.untracked.push(relativePath);
3258
- }
3259
- else {
3260
- results.orphaned.push(relativePath);
3261
- }
3262
- }
3382
+ const results = await getProjectStatus(config, state);
3263
3383
  // Reporting
3264
3384
  console.log('Textor Status Report:');
3265
3385
  console.log(` Synced files: ${results.synced}`);