@oamm/textor 1.0.9 → 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.
@@ -1136,6 +1136,68 @@ ${layoutOpening}
1136
1136
  `;
1137
1137
  }
1138
1138
 
1139
+ function mergeRouteTemplate(existingContent, featureImportPath, featureComponentName, layoutName) {
1140
+ let content = existingContent;
1141
+
1142
+ // 1. Add import
1143
+ const importLine = `import ${featureComponentName} from '${featureImportPath}';`;
1144
+ if (!content.includes(importLine)) {
1145
+ // Find the second --- which marks the end of frontmatter
1146
+ const lines = content.split('\n');
1147
+ let frontMatterEndLine = -1;
1148
+ let dashCount = 0;
1149
+ for (let i = 0; i < lines.length; i++) {
1150
+ if (lines[i].trim() === '---') {
1151
+ dashCount++;
1152
+ if (dashCount === 2) {
1153
+ frontMatterEndLine = i;
1154
+ break;
1155
+ }
1156
+ }
1157
+ }
1158
+
1159
+ if (frontMatterEndLine !== -1) {
1160
+ lines.splice(frontMatterEndLine, 0, importLine);
1161
+ content = lines.join('\n');
1162
+ } else if (content.includes('---')) {
1163
+ // If only one --- found, maybe it's just the start?
1164
+ // But standard Astro has two.
1165
+ // Fallback: insert at the beginning if no frontmatter end found
1166
+ content = importLine + '\n' + content;
1167
+ }
1168
+ }
1169
+
1170
+ // 2. Add component usage
1171
+ const componentTag = `<${featureComponentName} />`;
1172
+ if (!content.includes(componentTag)) {
1173
+ if (layoutName && layoutName !== 'none') {
1174
+ const layoutEndTag = `</${layoutName}>`;
1175
+ if (content.includes(layoutEndTag)) {
1176
+ const lines = content.split('\n');
1177
+ let layoutEndLine = -1;
1178
+ for (let i = lines.length - 1; i >= 0; i--) {
1179
+ if (lines[i].includes(layoutEndTag)) {
1180
+ layoutEndLine = i;
1181
+ break;
1182
+ }
1183
+ }
1184
+ if (layoutEndLine !== -1) {
1185
+ lines.splice(layoutEndLine, 0, ` ${componentTag}`);
1186
+ content = lines.join('\n');
1187
+ }
1188
+ } else {
1189
+ // Layout might be self-closing or missing end tag?
1190
+ // If it's Textor generated it should have it.
1191
+ content += `\n${componentTag}\n`;
1192
+ }
1193
+ } else {
1194
+ content += `\n${componentTag}\n`;
1195
+ }
1196
+ }
1197
+
1198
+ return content;
1199
+ }
1200
+
1139
1201
  /**
1140
1202
  * Feature Template Variables:
1141
1203
  * - componentName: Name of the feature component
@@ -1561,9 +1623,9 @@ async function addSectionToState(section) {
1561
1623
  if (normalizedSection.featurePath) {
1562
1624
  normalizedSection.featurePath = normalizeStatePath(normalizedSection.featurePath);
1563
1625
  }
1564
- // Avoid duplicates by route OR by featurePath if route is null
1626
+ // Avoid duplicates by route AND by featurePath
1565
1627
  if (normalizedSection.route) {
1566
- state.sections = state.sections.filter(s => s.route !== normalizedSection.route);
1628
+ state.sections = state.sections.filter(s => s.route !== normalizedSection.route || s.featurePath !== normalizedSection.featurePath);
1567
1629
  } else {
1568
1630
  state.sections = state.sections.filter(s => s.featurePath !== normalizedSection.featurePath || s.route);
1569
1631
  }
@@ -1641,6 +1703,10 @@ function reconstructComponents(files, config) {
1641
1703
  path: componentPath
1642
1704
  });
1643
1705
  }
1706
+ // Attribute ownership
1707
+ if (!files[filePath].owner) {
1708
+ files[filePath].owner = componentName;
1709
+ }
1644
1710
  }
1645
1711
  }
1646
1712
  }
@@ -1684,28 +1750,34 @@ function reconstructSections(state, config) {
1684
1750
 
1685
1751
  if (!sections.has(finalRoute)) {
1686
1752
  // Try to find a matching feature by name
1687
- const routeName = path.basename(finalRoute === '/' ? 'index' : finalRoute);
1753
+ const routeName = path.basename(finalRoute === '/' ? 'index' : finalRoute).toLowerCase();
1688
1754
  // Look for a directory in features with same name or similar
1689
1755
  const possibleFeaturePath = Object.keys(files).find(f => {
1690
1756
  const nf = f.replace(/\\/g, '/');
1691
- return nf.startsWith(featuresRoot + '/') && nf.includes('/' + routeName + '/');
1757
+ if (!nf.startsWith(featuresRoot + '/')) return false;
1758
+ const relToFeatures = nf.slice(featuresRoot.length + 1);
1759
+ const segments = relToFeatures.toLowerCase().split('/');
1760
+ return segments.includes(routeName);
1692
1761
  });
1693
1762
 
1694
1763
  if (possibleFeaturePath) {
1695
- const featurePathParts = possibleFeaturePath.replace(/\\/g, '/').split('/');
1696
- const featuresBase = path.basename(featuresRoot);
1697
- const featureIndex = featurePathParts.indexOf(featuresBase) + 1;
1698
-
1699
- if (featureIndex > 0 && featureIndex < featurePathParts.length) {
1700
- const featureName = featurePathParts[featureIndex];
1701
- const featurePath = `${featuresRoot}/${featureName}`;
1702
-
1703
- sections.set(finalRoute, {
1704
- name: featureName,
1705
- route: finalRoute,
1706
- featurePath: featurePath,
1707
- extension: path.extname(filePath)
1708
- });
1764
+ const relToFeatures = path.dirname(path.relative(featuresRoot, possibleFeaturePath)).replace(/\\/g, '/');
1765
+ const featurePath = relToFeatures === '.' ? featuresRoot : `${featuresRoot}/${relToFeatures}`;
1766
+ const featureName = path.basename(featurePath);
1767
+
1768
+ sections.set(finalRoute, {
1769
+ name: featureName,
1770
+ route: finalRoute,
1771
+ featurePath: featurePath,
1772
+ extension: path.extname(filePath)
1773
+ });
1774
+
1775
+ // Attribute ownership to discovered files
1776
+ if (!files[filePath].owner) files[filePath].owner = finalRoute;
1777
+ for (const f in files) {
1778
+ if (f.startsWith(featurePath + '/') || f === featurePath) {
1779
+ if (!files[f].owner) files[f].owner = finalRoute;
1780
+ }
1709
1781
  }
1710
1782
  }
1711
1783
  }
@@ -1986,7 +2058,15 @@ async function addSectionCommand(route, featurePath, options) {
1986
2058
  await saveState(state);
1987
2059
  }
1988
2060
 
1989
- if (routeFilePath) await ensureNotExists(routeFilePath, options.force);
2061
+ if (routeFilePath) {
2062
+ if (existsSync(routeFilePath)) {
2063
+ const isGenerated = await isTextorGenerated(routeFilePath);
2064
+ if (!isGenerated && !options.force) {
2065
+ throw new Error(`File already exists: ${routeFilePath}\nUse --force to overwrite.`);
2066
+ }
2067
+ }
2068
+ }
2069
+
1990
2070
  await ensureNotExists(featureFilePath, options.force);
1991
2071
 
1992
2072
  if (shouldCreateIndex) await ensureNotExists(indexFilePath, options.force);
@@ -2068,15 +2148,42 @@ async function addSectionCommand(route, featurePath, options) {
2068
2148
  routeContent = generateEndpointTemplate(featureComponentName);
2069
2149
  routeSignature = getSignature(config, 'typescript');
2070
2150
  } else {
2071
- routeContent = generateRouteTemplate(
2072
- layout,
2073
- layoutImportPath,
2074
- featureImportPath,
2075
- featureComponentName,
2076
- routeExtension,
2077
- layoutProps
2078
- );
2079
2151
  routeSignature = getSignature(config, 'astro');
2152
+
2153
+ if (existsSync(routeFilePath)) {
2154
+ const existingContent = await readFile(routeFilePath, 'utf-8');
2155
+ // Strip existing signature if present
2156
+ let contentToMerge = existingContent;
2157
+ if (existingContent.startsWith(routeSignature)) {
2158
+ contentToMerge = existingContent.slice(routeSignature.length).trimStart();
2159
+ } else {
2160
+ // Check for generic signature if specific one doesn't match
2161
+ const genericSignature = '@generated by Textor';
2162
+ if (existingContent.includes(genericSignature)) {
2163
+ const lines = existingContent.split('\n');
2164
+ if (lines[0].includes(genericSignature)) {
2165
+ lines.shift();
2166
+ contentToMerge = lines.join('\n').trimStart();
2167
+ }
2168
+ }
2169
+ }
2170
+
2171
+ routeContent = mergeRouteTemplate(
2172
+ contentToMerge,
2173
+ featureImportPath,
2174
+ featureComponentName,
2175
+ layout
2176
+ );
2177
+ } else {
2178
+ routeContent = generateRouteTemplate(
2179
+ layout,
2180
+ layoutImportPath,
2181
+ featureImportPath,
2182
+ featureComponentName,
2183
+ routeExtension,
2184
+ layoutProps
2185
+ );
2186
+ }
2080
2187
  }
2081
2188
  }
2082
2189
 
@@ -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
@@ -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);