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