@oamm/textor 1.0.1 → 1.0.2

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
@@ -114,10 +114,21 @@ If you have manually edited a Textor-generated file and wish to remove or move i
114
114
  ## 🛠️ Commands
115
115
 
116
116
  ### add-section
117
- Create a route + feature binding.
117
+ Create a route + feature binding, or a standalone feature.
118
118
 
119
119
  ```bash
120
- pnpm textor add-section <route> <featurePath> [options]
120
+ pnpm textor add-section [route] <featurePath> [options]
121
+ ```
122
+
123
+ If `route` is provided, Textor creates both a route adapter (e.g., in `src/pages`) and a feature module. If `route` is omitted, Textor scaffolds only the feature module. This is useful for features that are shared across multiple pages or used as sub-parts of other features.
124
+
125
+ **Examples:**
126
+ ```bash
127
+ # Create a section with a route
128
+ pnpm textor add-section /users users/catalog
129
+
130
+ # Create a standalone feature (no route file)
131
+ pnpm textor add-section auth/login
121
132
  ```
122
133
 
123
134
  **Options:**
@@ -149,6 +160,14 @@ pnpm textor move-section /old /new
149
160
  ### remove-section / remove-component
150
161
  Safely remove Textor-managed modules.
151
162
 
163
+ ```bash
164
+ # Remove by route
165
+ pnpm textor remove-section /users
166
+
167
+ # Remove a standalone feature by its name or path
168
+ pnpm textor remove-section auth/login
169
+ ```
170
+
152
171
  ### list-sections
153
172
  List all Textor-managed modules, including their architectural capabilities (API, Hooks, etc.).
154
173
 
@@ -415,6 +415,7 @@ function normalizeComponentName(name) {
415
415
  }
416
416
 
417
417
  function normalizeRoute(route) {
418
+ if (!route) return null;
418
419
  let normalized = route.trim();
419
420
 
420
421
  if (!normalized.startsWith('/')) {
@@ -1394,8 +1395,12 @@ async function registerFile(filePath, { kind, template, hash, templateVersion =
1394
1395
 
1395
1396
  async function addSectionToState(section) {
1396
1397
  const state = await loadState();
1397
- // Avoid duplicates by route
1398
- state.sections = state.sections.filter(s => s.route !== section.route);
1398
+ // Avoid duplicates by route OR by featurePath if route is null
1399
+ if (section.route) {
1400
+ state.sections = state.sections.filter(s => s.route !== section.route);
1401
+ } else {
1402
+ state.sections = state.sections.filter(s => s.featurePath !== section.featurePath || s.route);
1403
+ }
1399
1404
  state.sections.push(section);
1400
1405
  await saveState(state);
1401
1406
  }
@@ -1535,6 +1540,14 @@ async function stageFiles(filePaths) {
1535
1540
  async function addSectionCommand(route, featurePath, options) {
1536
1541
  try {
1537
1542
  const config = await loadConfig();
1543
+
1544
+ // Handle optional route
1545
+ if (typeof featurePath === 'object' || featurePath === undefined) {
1546
+ options = featurePath || options || {};
1547
+ featurePath = route;
1548
+ route = null;
1549
+ }
1550
+
1538
1551
  const effectiveOptions = getEffectiveOptions(options, config, 'features');
1539
1552
 
1540
1553
  const normalizedRoute = normalizeRoute(route);
@@ -1549,7 +1562,7 @@ async function addSectionCommand(route, featurePath, options) {
1549
1562
  // Check if we should use nested mode even if config says flat
1550
1563
  // (because the directory already exists, suggesting it should be an index file)
1551
1564
  let effectiveRoutingMode = config.routing.mode;
1552
- if (effectiveRoutingMode === 'flat') {
1565
+ if (normalizedRoute && effectiveRoutingMode === 'flat') {
1553
1566
  const routeDirName = routeToFilePath(normalizedRoute, {
1554
1567
  extension: '',
1555
1568
  mode: 'flat'
@@ -1560,11 +1573,11 @@ async function addSectionCommand(route, featurePath, options) {
1560
1573
  }
1561
1574
  }
1562
1575
 
1563
- const routeFileName = routeToFilePath(normalizedRoute, {
1576
+ const routeFileName = normalizedRoute ? routeToFilePath(normalizedRoute, {
1564
1577
  extension: routeExtension,
1565
1578
  mode: effectiveRoutingMode,
1566
1579
  indexFile: config.routing.indexFile
1567
- });
1580
+ }) : null;
1568
1581
 
1569
1582
  const featureComponentName = getFeatureComponentName(normalizedFeaturePath);
1570
1583
  const featureFileName = getFeatureFileName(normalizedFeaturePath, {
@@ -1572,7 +1585,7 @@ async function addSectionCommand(route, featurePath, options) {
1572
1585
  strategy: effectiveOptions.entry
1573
1586
  });
1574
1587
 
1575
- const routeFilePath = secureJoin(pagesRoot, routeFileName);
1588
+ const routeFilePath = routeFileName ? secureJoin(pagesRoot, routeFileName) : null;
1576
1589
  const featureDirPath = secureJoin(featuresRoot, normalizedFeaturePath);
1577
1590
  const featureFilePath = secureJoin(featureDirPath, featureFileName);
1578
1591
  const scriptsIndexPath = secureJoin(featureDirPath, config.features.scriptsIndexFile);
@@ -1612,10 +1625,10 @@ async function addSectionCommand(route, featurePath, options) {
1612
1625
  const readmeFilePath = path.join(featureDirPath, 'README.md');
1613
1626
  const storiesFilePath = path.join(featureDirPath, `${featureComponentName}.stories.tsx`);
1614
1627
 
1615
- const routeParts = normalizedRoute.split('/').filter(Boolean);
1628
+ const routeParts = normalizedRoute ? normalizedRoute.split('/').filter(Boolean) : [];
1616
1629
  const reorganizations = [];
1617
1630
 
1618
- if (routeParts.length > 1 && config.routing.mode === 'flat') {
1631
+ if (normalizedRoute && routeParts.length > 1 && config.routing.mode === 'flat') {
1619
1632
  const possibleExtensions = ['.astro', '.ts', '.js', '.md', '.mdx', '.html'];
1620
1633
  for (let i = 1; i < routeParts.length; i++) {
1621
1634
  const parentRoute = '/' + routeParts.slice(0, i).join('/');
@@ -1650,7 +1663,7 @@ async function addSectionCommand(route, featurePath, options) {
1650
1663
 
1651
1664
  if (options.dryRun) {
1652
1665
  console.log('Dry run - would create:');
1653
- console.log(` Route: ${routeFilePath}`);
1666
+ if (routeFilePath) console.log(` Route: ${routeFilePath}`);
1654
1667
  console.log(` Feature: ${featureFilePath}`);
1655
1668
 
1656
1669
  for (const reorg of reorganizations) {
@@ -1695,7 +1708,7 @@ async function addSectionCommand(route, featurePath, options) {
1695
1708
  await saveState(state);
1696
1709
  }
1697
1710
 
1698
- await ensureNotExists(routeFilePath, options.force);
1711
+ if (routeFilePath) await ensureNotExists(routeFilePath, options.force);
1699
1712
  await ensureNotExists(featureFilePath, options.force);
1700
1713
 
1701
1714
  if (shouldCreateIndex) await ensureNotExists(indexFilePath, options.force);
@@ -1711,7 +1724,7 @@ async function addSectionCommand(route, featurePath, options) {
1711
1724
  if (shouldCreateScriptsDir) await ensureNotExists(scriptsIndexPath, options.force);
1712
1725
 
1713
1726
  let layoutImportPath = null;
1714
- if (options.layout !== 'none') {
1727
+ if (routeFilePath && options.layout !== 'none') {
1715
1728
  if (config.importAliases.layouts) {
1716
1729
  layoutImportPath = `${config.importAliases.layouts}/${options.layout}.astro`;
1717
1730
  } else {
@@ -1720,19 +1733,21 @@ async function addSectionCommand(route, featurePath, options) {
1720
1733
  }
1721
1734
  }
1722
1735
 
1723
- let featureImportPath;
1724
- if (config.importAliases.features) {
1725
- const entryPart = effectiveOptions.entry === 'index' ? '' : `/${featureComponentName}`;
1726
- // In Astro, we can often omit the extension for .tsx files, but not for .astro files if using aliases sometimes.
1727
- // However, to be safe, we use the configured extension.
1728
- featureImportPath = `${config.importAliases.features}/${normalizedFeaturePath}${entryPart}${config.naming.featureExtension}`;
1729
- } else {
1730
- const relativeFeatureFile = getRelativeImportPath(routeFilePath, featureFilePath);
1731
- // Remove extension for import if it's not an .astro file
1732
- if (config.naming.featureExtension === '.astro') {
1733
- featureImportPath = relativeFeatureFile;
1736
+ let featureImportPath = null;
1737
+ if (routeFilePath) {
1738
+ if (config.importAliases.features) {
1739
+ const entryPart = effectiveOptions.entry === 'index' ? '' : `/${featureComponentName}`;
1740
+ // In Astro, we can often omit the extension for .tsx files, but not for .astro files if using aliases sometimes.
1741
+ // However, to be safe, we use the configured extension.
1742
+ featureImportPath = `${config.importAliases.features}/${normalizedFeaturePath}${entryPart}${config.naming.featureExtension}`;
1734
1743
  } else {
1735
- featureImportPath = relativeFeatureFile.replace(/\.[^/.]+$/, '');
1744
+ const relativeFeatureFile = getRelativeImportPath(routeFilePath, featureFilePath);
1745
+ // Remove extension for import if it's not an .astro file
1746
+ if (config.naming.featureExtension === '.astro') {
1747
+ featureImportPath = relativeFeatureFile;
1748
+ } else {
1749
+ featureImportPath = relativeFeatureFile.replace(/\.[^/.]+$/, '');
1750
+ }
1736
1751
  }
1737
1752
  }
1738
1753
 
@@ -1744,35 +1759,40 @@ async function addSectionCommand(route, featurePath, options) {
1744
1759
  let routeContent;
1745
1760
  let routeSignature;
1746
1761
 
1747
- if (options.endpoint) {
1748
- routeContent = generateEndpointTemplate(featureComponentName);
1749
- routeSignature = getSignature(config, 'typescript');
1750
- } else {
1751
- routeContent = generateRouteTemplate(
1752
- options.layout,
1753
- layoutImportPath,
1754
- featureImportPath,
1755
- featureComponentName
1756
- );
1757
- routeSignature = getSignature(config, 'astro');
1762
+ if (routeFilePath) {
1763
+ if (options.endpoint) {
1764
+ routeContent = generateEndpointTemplate(featureComponentName);
1765
+ routeSignature = getSignature(config, 'typescript');
1766
+ } else {
1767
+ routeContent = generateRouteTemplate(
1768
+ options.layout,
1769
+ layoutImportPath,
1770
+ featureImportPath,
1771
+ featureComponentName
1772
+ );
1773
+ routeSignature = getSignature(config, 'astro');
1774
+ }
1758
1775
  }
1759
1776
 
1760
1777
  const featureContent = generateFeatureTemplate(featureComponentName, scriptImportPath, framework);
1761
1778
 
1762
- const routeHash = await writeFileWithSignature(
1763
- routeFilePath,
1764
- routeContent,
1765
- routeSignature,
1766
- config.hashing?.normalization
1767
- );
1768
- await registerFile(routeFilePath, {
1769
- kind: 'route',
1770
- template: options.endpoint ? 'endpoint' : 'route',
1771
- hash: routeHash,
1772
- owner: normalizedRoute
1773
- });
1774
-
1775
- const writtenFiles = [routeFilePath];
1779
+ const writtenFiles = [];
1780
+
1781
+ if (routeFilePath) {
1782
+ const routeHash = await writeFileWithSignature(
1783
+ routeFilePath,
1784
+ routeContent,
1785
+ routeSignature,
1786
+ config.hashing?.normalization
1787
+ );
1788
+ await registerFile(routeFilePath, {
1789
+ kind: 'route',
1790
+ template: options.endpoint ? 'endpoint' : 'route',
1791
+ hash: routeHash,
1792
+ owner: normalizedRoute
1793
+ });
1794
+ writtenFiles.push(routeFilePath);
1795
+ }
1776
1796
 
1777
1797
  await ensureDir(featureDirPath);
1778
1798
 
@@ -1996,7 +2016,7 @@ async function addSectionCommand(route, featurePath, options) {
1996
2016
  }
1997
2017
 
1998
2018
  console.log('✓ Section created successfully:');
1999
- console.log(` Route: ${routeFilePath}`);
2019
+ if (routeFilePath) console.log(` Route: ${routeFilePath}`);
2000
2020
  console.log(` Feature: ${featureFilePath}`);
2001
2021
 
2002
2022
  if (shouldCreateIndex) console.log(` Index: ${indexFilePath}`);
@@ -3719,8 +3739,8 @@ program
3719
3739
  .action(initCommand);
3720
3740
 
3721
3741
  program
3722
- .command('add-section <route> <featurePath>')
3723
- .description('Create a route + feature binding')
3742
+ .command('add-section [route] [featurePath]')
3743
+ .description('Create a route + feature binding (route optional for standalone features)')
3724
3744
  .option('--preset <name>', 'Scaffolding preset (minimal, standard, senior)')
3725
3745
  .option('--layout <name>', 'Layout component name (use "none" for no layout)', 'Main')
3726
3746
  .option('--name <name>', 'Section name for state tracking')
@@ -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
@@ -384,6 +384,8 @@ function normalizeComponentName(name) {
384
384
  return toPascalCase(name);
385
385
  }
386
386
  function normalizeRoute(route) {
387
+ if (!route)
388
+ return null;
387
389
  let normalized = route.trim();
388
390
  if (!normalized.startsWith('/')) {
389
391
  normalized = '/' + normalized;
@@ -1228,8 +1230,13 @@ async function registerFile(filePath, { kind, template, hash, templateVersion =
1228
1230
  }
1229
1231
  async function addSectionToState(section) {
1230
1232
  const state = await loadState();
1231
- // Avoid duplicates by route
1232
- state.sections = state.sections.filter(s => s.route !== section.route);
1233
+ // 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);
1236
+ }
1237
+ else {
1238
+ state.sections = state.sections.filter(s => s.featurePath !== section.featurePath || s.route);
1239
+ }
1233
1240
  state.sections.push(section);
1234
1241
  await saveState(state);
1235
1242
  }
@@ -1351,6 +1358,12 @@ async function stageFiles(filePaths) {
1351
1358
  async function addSectionCommand(route, featurePath, options) {
1352
1359
  try {
1353
1360
  const config = await loadConfig();
1361
+ // Handle optional route
1362
+ if (typeof featurePath === 'object' || featurePath === undefined) {
1363
+ options = featurePath || options || {};
1364
+ featurePath = route;
1365
+ route = null;
1366
+ }
1354
1367
  const effectiveOptions = getEffectiveOptions(options, config, 'features');
1355
1368
  const normalizedRoute = normalizeRoute(route);
1356
1369
  const normalizedFeaturePath = featureToDirectoryPath(featurePath);
@@ -1361,7 +1374,7 @@ async function addSectionCommand(route, featurePath, options) {
1361
1374
  // Check if we should use nested mode even if config says flat
1362
1375
  // (because the directory already exists, suggesting it should be an index file)
1363
1376
  let effectiveRoutingMode = config.routing.mode;
1364
- if (effectiveRoutingMode === 'flat') {
1377
+ if (normalizedRoute && effectiveRoutingMode === 'flat') {
1365
1378
  const routeDirName = routeToFilePath(normalizedRoute, {
1366
1379
  extension: '',
1367
1380
  mode: 'flat'
@@ -1371,17 +1384,17 @@ async function addSectionCommand(route, featurePath, options) {
1371
1384
  effectiveRoutingMode = 'nested';
1372
1385
  }
1373
1386
  }
1374
- const routeFileName = routeToFilePath(normalizedRoute, {
1387
+ const routeFileName = normalizedRoute ? routeToFilePath(normalizedRoute, {
1375
1388
  extension: routeExtension,
1376
1389
  mode: effectiveRoutingMode,
1377
1390
  indexFile: config.routing.indexFile
1378
- });
1391
+ }) : null;
1379
1392
  const featureComponentName = getFeatureComponentName(normalizedFeaturePath);
1380
1393
  const featureFileName = getFeatureFileName(normalizedFeaturePath, {
1381
1394
  extension: config.naming.featureExtension,
1382
1395
  strategy: effectiveOptions.entry
1383
1396
  });
1384
- const routeFilePath = secureJoin(pagesRoot, routeFileName);
1397
+ const routeFilePath = routeFileName ? secureJoin(pagesRoot, routeFileName) : null;
1385
1398
  const featureDirPath = secureJoin(featuresRoot, normalizedFeaturePath);
1386
1399
  const featureFilePath = secureJoin(featureDirPath, featureFileName);
1387
1400
  const scriptsIndexPath = secureJoin(featureDirPath, config.features.scriptsIndexFile);
@@ -1404,9 +1417,9 @@ async function addSectionCommand(route, featurePath, options) {
1404
1417
  const schemasFilePath = path.join(schemasDirInside, 'index.ts');
1405
1418
  const readmeFilePath = path.join(featureDirPath, 'README.md');
1406
1419
  const storiesFilePath = path.join(featureDirPath, `${featureComponentName}.stories.tsx`);
1407
- const routeParts = normalizedRoute.split('/').filter(Boolean);
1420
+ const routeParts = normalizedRoute ? normalizedRoute.split('/').filter(Boolean) : [];
1408
1421
  const reorganizations = [];
1409
- if (routeParts.length > 1 && config.routing.mode === 'flat') {
1422
+ if (normalizedRoute && routeParts.length > 1 && config.routing.mode === 'flat') {
1410
1423
  const possibleExtensions = ['.astro', '.ts', '.js', '.md', '.mdx', '.html'];
1411
1424
  for (let i = 1; i < routeParts.length; i++) {
1412
1425
  const parentRoute = '/' + routeParts.slice(0, i).join('/');
@@ -1437,7 +1450,8 @@ async function addSectionCommand(route, featurePath, options) {
1437
1450
  }
1438
1451
  if (options.dryRun) {
1439
1452
  console.log('Dry run - would create:');
1440
- console.log(` Route: ${routeFilePath}`);
1453
+ if (routeFilePath)
1454
+ console.log(` Route: ${routeFilePath}`);
1441
1455
  console.log(` Feature: ${featureFilePath}`);
1442
1456
  for (const reorg of reorganizations) {
1443
1457
  console.log(` Reorganize: ${reorg.from} -> ${reorg.to}`);
@@ -1485,7 +1499,8 @@ async function addSectionCommand(route, featurePath, options) {
1485
1499
  }
1486
1500
  await saveState(state);
1487
1501
  }
1488
- await ensureNotExists(routeFilePath, options.force);
1502
+ if (routeFilePath)
1503
+ await ensureNotExists(routeFilePath, options.force);
1489
1504
  await ensureNotExists(featureFilePath, options.force);
1490
1505
  if (shouldCreateIndex)
1491
1506
  await ensureNotExists(indexFilePath, options.force);
@@ -1510,7 +1525,7 @@ async function addSectionCommand(route, featurePath, options) {
1510
1525
  if (shouldCreateScriptsDir)
1511
1526
  await ensureNotExists(scriptsIndexPath, options.force);
1512
1527
  let layoutImportPath = null;
1513
- if (options.layout !== 'none') {
1528
+ if (routeFilePath && options.layout !== 'none') {
1514
1529
  if (config.importAliases.layouts) {
1515
1530
  layoutImportPath = `${config.importAliases.layouts}/${options.layout}.astro`;
1516
1531
  }
@@ -1519,21 +1534,23 @@ async function addSectionCommand(route, featurePath, options) {
1519
1534
  layoutImportPath = getRelativeImportPath(routeFilePath, layoutFilePath);
1520
1535
  }
1521
1536
  }
1522
- let featureImportPath;
1523
- if (config.importAliases.features) {
1524
- const entryPart = effectiveOptions.entry === 'index' ? '' : `/${featureComponentName}`;
1525
- // In Astro, we can often omit the extension for .tsx files, but not for .astro files if using aliases sometimes.
1526
- // However, to be safe, we use the configured extension.
1527
- featureImportPath = `${config.importAliases.features}/${normalizedFeaturePath}${entryPart}${config.naming.featureExtension}`;
1528
- }
1529
- else {
1530
- const relativeFeatureFile = getRelativeImportPath(routeFilePath, featureFilePath);
1531
- // Remove extension for import if it's not an .astro file
1532
- if (config.naming.featureExtension === '.astro') {
1533
- featureImportPath = relativeFeatureFile;
1537
+ let featureImportPath = null;
1538
+ if (routeFilePath) {
1539
+ if (config.importAliases.features) {
1540
+ const entryPart = effectiveOptions.entry === 'index' ? '' : `/${featureComponentName}`;
1541
+ // In Astro, we can often omit the extension for .tsx files, but not for .astro files if using aliases sometimes.
1542
+ // However, to be safe, we use the configured extension.
1543
+ featureImportPath = `${config.importAliases.features}/${normalizedFeaturePath}${entryPart}${config.naming.featureExtension}`;
1534
1544
  }
1535
1545
  else {
1536
- featureImportPath = relativeFeatureFile.replace(/\.[^/.]+$/, '');
1546
+ const relativeFeatureFile = getRelativeImportPath(routeFilePath, featureFilePath);
1547
+ // Remove extension for import if it's not an .astro file
1548
+ if (config.naming.featureExtension === '.astro') {
1549
+ featureImportPath = relativeFeatureFile;
1550
+ }
1551
+ else {
1552
+ featureImportPath = relativeFeatureFile.replace(/\.[^/.]+$/, '');
1553
+ }
1537
1554
  }
1538
1555
  }
1539
1556
  let scriptImportPath;
@@ -1542,23 +1559,28 @@ async function addSectionCommand(route, featurePath, options) {
1542
1559
  }
1543
1560
  let routeContent;
1544
1561
  let routeSignature;
1545
- if (options.endpoint) {
1546
- routeContent = generateEndpointTemplate(featureComponentName);
1547
- routeSignature = getSignature(config, 'typescript');
1548
- }
1549
- else {
1550
- routeContent = generateRouteTemplate(options.layout, layoutImportPath, featureImportPath, featureComponentName);
1551
- routeSignature = getSignature(config, 'astro');
1562
+ if (routeFilePath) {
1563
+ if (options.endpoint) {
1564
+ routeContent = generateEndpointTemplate(featureComponentName);
1565
+ routeSignature = getSignature(config, 'typescript');
1566
+ }
1567
+ else {
1568
+ routeContent = generateRouteTemplate(options.layout, layoutImportPath, featureImportPath, featureComponentName);
1569
+ routeSignature = getSignature(config, 'astro');
1570
+ }
1552
1571
  }
1553
1572
  const featureContent = generateFeatureTemplate(featureComponentName, scriptImportPath, framework);
1554
- const routeHash = await writeFileWithSignature(routeFilePath, routeContent, routeSignature, config.hashing?.normalization);
1555
- await registerFile(routeFilePath, {
1556
- kind: 'route',
1557
- template: options.endpoint ? 'endpoint' : 'route',
1558
- hash: routeHash,
1559
- owner: normalizedRoute
1560
- });
1561
- const writtenFiles = [routeFilePath];
1573
+ const writtenFiles = [];
1574
+ if (routeFilePath) {
1575
+ const routeHash = await writeFileWithSignature(routeFilePath, routeContent, routeSignature, config.hashing?.normalization);
1576
+ await registerFile(routeFilePath, {
1577
+ kind: 'route',
1578
+ template: options.endpoint ? 'endpoint' : 'route',
1579
+ hash: routeHash,
1580
+ owner: normalizedRoute
1581
+ });
1582
+ writtenFiles.push(routeFilePath);
1583
+ }
1562
1584
  await ensureDir(featureDirPath);
1563
1585
  if (shouldCreateSubComponentsDir)
1564
1586
  await ensureDir(subComponentsDir);
@@ -1713,7 +1735,8 @@ async function addSectionCommand(route, featurePath, options) {
1713
1735
  await formatFiles(writtenFiles, config.formatting.tool);
1714
1736
  }
1715
1737
  console.log('✓ Section created successfully:');
1716
- console.log(` Route: ${routeFilePath}`);
1738
+ if (routeFilePath)
1739
+ console.log(` Route: ${routeFilePath}`);
1717
1740
  console.log(` Feature: ${featureFilePath}`);
1718
1741
  if (shouldCreateIndex)
1719
1742
  console.log(` Index: ${indexFilePath}`);