@oamm/textor 1.0.5 → 1.0.8

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
@@ -341,6 +341,8 @@ The .textor/config.json file allows full control over the tool's behavior.
341
341
  "indexFile": "index.astro"
342
342
  },
343
343
  "importAliases": {
344
+ "layouts": "@/layouts",
345
+ "features": "@/features"
344
346
  },
345
347
  "naming": {
346
348
  "routeExtension": ".astro",
@@ -370,7 +372,8 @@ The .textor/config.json file allows full control over the tool's behavior.
370
372
  "createTypes": false,
371
373
  "createReadme": false,
372
374
  "createStories": false,
373
- "createIndex": false
375
+ "createIndex": false,
376
+ "layout": "Main"
374
377
  },
375
378
  "components": {
376
379
  "framework": "react",
@@ -458,6 +461,31 @@ The .textor/config.json file allows full control over the tool's behavior.
458
461
  ```
459
462
  *Supported formatting tools: prettier, biome, none.*
460
463
 
464
+ ### 7. Layout Parameters
465
+
466
+ You can pass parameters to your layout component by defining `layoutProps` in `.textor/config.json`. These props support variable substitution.
467
+
468
+ ```json
469
+ {
470
+ "features": {
471
+ "layout": "AppLayout",
472
+ "layoutProps": {
473
+ "title": "{{componentName}}",
474
+ "description": "Description for {{componentName}}"
475
+ }
476
+ }
477
+ }
478
+ ```
479
+
480
+ You can also override these props via the CLI using the `--prop` flag:
481
+ ```bash
482
+ pnpm textor add-section /users users/roles --prop title="Custom Title" --prop breadcrumbs='{[{ label: "Users" }]}'
483
+ ```
484
+
485
+ Properties that start and end with curly braces `{}` are passed as JavaScript expressions, others as strings.
486
+
487
+ ---
488
+
461
489
  ## 📝 Template Overrides
462
490
 
463
491
  You can customize the code generated by Textor by providing your own templates. Textor looks for override files in the `.textor/templates/` directory at your project root.
@@ -472,7 +500,7 @@ You can customize the code generated by Textor by providing your own templates.
472
500
 
473
501
  | Template Name | File to create in `.textor/templates/` | Available Variables |
474
502
  | :--- | :--- | :--- |
475
- | **Route** | `route.astro` | `{{layoutName}}`, `{{layoutImportPath}}`, `{{featureImportPath}}`, `{{featureComponentName}}` |
503
+ | **Route** | `route.astro` | `{{layoutName}}`, `{{layoutImportPath}}`, `{{featureImportPath}}`, `{{featureComponentName}}`, plus any `layoutProps` |
476
504
  | **Feature** | `feature.astro` or `feature.tsx` | `{{componentName}}`, `{{scriptImportPath}}` |
477
505
  | **Component** | `component.astro` or `component.tsx` | `{{componentName}}` |
478
506
  | **Hook** | `hook.ts` | `{{componentName}}`, `{{hookName}}` |
@@ -36,10 +36,13 @@ const CURRENT_CONFIG_VERSION = 2;
36
36
  * @property {string} signatures.typescript
37
37
  * @property {string} signatures.javascript
38
38
  * @property {Object} features
39
+ * @property {string} features.framework
39
40
  * @property {string} features.entry
40
41
  * @property {boolean} features.createSubComponentsDir
41
42
  * @property {boolean} features.createScriptsDir
42
43
  * @property {string} features.scriptsIndexFile
44
+ * @property {string} features.layout
45
+ * @property {Object} features.layoutProps
43
46
  * @property {Object} components
44
47
  * @property {boolean} components.createSubComponentsDir
45
48
  * @property {boolean} components.createContext
@@ -105,7 +108,9 @@ const DEFAULT_CONFIG = {
105
108
  createTypes: false,
106
109
  createReadme: false,
107
110
  createStories: false,
108
- createIndex: false
111
+ createIndex: false,
112
+ layout: 'Main',
113
+ layoutProps: {}
109
114
  },
110
115
  components: {
111
116
  framework: 'react',
@@ -1017,27 +1022,6 @@ async function formatFiles(filePaths, tool) {
1017
1022
  }
1018
1023
  }
1019
1024
 
1020
- var filesystem = /*#__PURE__*/Object.freeze({
1021
- __proto__: null,
1022
- calculateHash: calculateHash,
1023
- cleanupEmptyDirs: cleanupEmptyDirs,
1024
- ensureDir: ensureDir,
1025
- ensureNotExists: ensureNotExists,
1026
- formatFiles: formatFiles,
1027
- getSignature: getSignature,
1028
- inferKind: inferKind,
1029
- isEmptyDir: isEmptyDir,
1030
- isTextorGenerated: isTextorGenerated,
1031
- safeDelete: safeDelete,
1032
- safeDeleteDir: safeDeleteDir,
1033
- safeMove: safeMove,
1034
- scanDirectory: scanDirectory,
1035
- secureJoin: secureJoin,
1036
- updateSignature: updateSignature,
1037
- verifyFileIntegrity: verifyFileIntegrity,
1038
- writeFileWithSignature: writeFileWithSignature
1039
- });
1040
-
1041
1025
  function renderNamePattern(pattern, data = {}, label = 'pattern') {
1042
1026
  if (typeof pattern !== 'string') return null;
1043
1027
  const trimmed = pattern.trim();
@@ -1105,13 +1089,15 @@ function getTemplateOverride(templateName, extension, data = {}) {
1105
1089
  * - layoutImportPath: Path to import the layout
1106
1090
  * - featureImportPath: Path to import the feature component
1107
1091
  * - featureComponentName: Name of the feature component
1092
+ * - layoutProps: Optional properties for the layout
1108
1093
  */
1109
- function generateRouteTemplate(layoutName, layoutImportPath, featureImportPath, featureComponentName, extension = '.astro') {
1094
+ function generateRouteTemplate(layoutName, layoutImportPath, featureImportPath, featureComponentName, extension = '.astro', layoutProps = {}) {
1110
1095
  const override = getTemplateOverride('route', extension, {
1111
1096
  layoutName,
1112
1097
  layoutImportPath,
1113
1098
  featureImportPath,
1114
- featureComponentName
1099
+ featureComponentName,
1100
+ ...layoutProps
1115
1101
  });
1116
1102
  if (override) return override;
1117
1103
 
@@ -1124,12 +1110,26 @@ import ${featureComponentName} from '${featureImportPath}';
1124
1110
  `;
1125
1111
  }
1126
1112
 
1113
+ const propsStr = Object.entries(layoutProps)
1114
+ .map(([key, value]) => {
1115
+ if (typeof value === 'string' && value.startsWith('{') && value.endsWith('}')) {
1116
+ return `${key}=${value}`;
1117
+ }
1118
+ if (typeof value === 'string') {
1119
+ return `${key}="${value}"`;
1120
+ }
1121
+ return `${key}={${JSON.stringify(value)}}`;
1122
+ })
1123
+ .join(' ');
1124
+
1125
+ const layoutOpening = propsStr ? `<${layoutName} ${propsStr}>` : `<${layoutName}>`;
1126
+
1127
1127
  return `---
1128
1128
  import ${layoutName} from '${layoutImportPath}';
1129
1129
  import ${featureComponentName} from '${featureImportPath}';
1130
1130
  ---
1131
1131
 
1132
- <${layoutName}>
1132
+ ${layoutOpening}
1133
1133
  <${featureComponentName} />
1134
1134
  </${layoutName}>
1135
1135
  `;
@@ -1340,6 +1340,11 @@ function generateIndexTemplate(componentName, componentExtension) {
1340
1340
  const override = getTemplateOverride('index', '.ts', { componentName, componentExtension });
1341
1341
  if (override) return override;
1342
1342
 
1343
+ if (componentExtension === '.astro') {
1344
+ return `export * from './types';
1345
+ `;
1346
+ }
1347
+
1343
1348
  return `export { default as ${componentName} } from './${componentName}${componentExtension}';
1344
1349
  export * from './types';
1345
1350
  `;
@@ -1794,6 +1799,8 @@ async function addSectionCommand(route, featurePath, options) {
1794
1799
 
1795
1800
  const {
1796
1801
  framework,
1802
+ layout,
1803
+ layoutProps: configLayoutProps,
1797
1804
  createSubComponentsDir: shouldCreateSubComponentsDir,
1798
1805
  createScriptsDir: shouldCreateScriptsDir,
1799
1806
  createApi: shouldCreateApi,
@@ -1965,7 +1972,13 @@ async function addSectionCommand(route, featurePath, options) {
1965
1972
  }
1966
1973
 
1967
1974
  // Update imports in the moved file
1968
- await updateImportsInFile(reorg.to, reorg.from, reorg.to);
1975
+ await updateImportsInFile$2(reorg.to, reorg.from, reorg.to);
1976
+
1977
+ // Update hash in state after import updates
1978
+ if (state.files[newRelative]) {
1979
+ const content = await readFile(reorg.to, 'utf-8');
1980
+ state.files[newRelative].hash = calculateHash(content, config.hashing?.normalization);
1981
+ }
1969
1982
 
1970
1983
  console.log(`✓ Reorganized ${oldRelative} to ${newRelative}`);
1971
1984
  }
@@ -1988,11 +2001,37 @@ async function addSectionCommand(route, featurePath, options) {
1988
2001
  if (shouldCreateScriptsDir) await ensureNotExists(scriptsIndexPath, options.force);
1989
2002
 
1990
2003
  let layoutImportPath = null;
1991
- if (routeFilePath && options.layout !== 'none') {
2004
+ const cliProps = options.prop || {};
2005
+ const rawLayoutProps = { ...configLayoutProps, ...cliProps };
2006
+ const layoutProps = {};
2007
+
2008
+ // Resolve variables in layoutProps
2009
+ const substitutionData = enrichData({
2010
+ componentName: featureComponentName,
2011
+ layoutName: layout,
2012
+ featureComponentName: featureComponentName
2013
+ });
2014
+
2015
+ for (const [key, value] of Object.entries(rawLayoutProps)) {
2016
+ if (typeof value === 'string') {
2017
+ let resolvedValue = value;
2018
+ for (const [varKey, varValue] of Object.entries(substitutionData)) {
2019
+ const regex = new RegExp(`{{${varKey}}}`, 'g');
2020
+ resolvedValue = resolvedValue.replace(regex, varValue);
2021
+ const underscoreRegex = new RegExp(`__${varKey}__`, 'g');
2022
+ resolvedValue = resolvedValue.replace(underscoreRegex, varValue);
2023
+ }
2024
+ layoutProps[key] = resolvedValue;
2025
+ } else {
2026
+ layoutProps[key] = value;
2027
+ }
2028
+ }
2029
+
2030
+ if (routeFilePath && layout !== 'none') {
1992
2031
  if (config.importAliases.layouts) {
1993
- layoutImportPath = `${config.importAliases.layouts}/${options.layout}.astro`;
2032
+ layoutImportPath = `${config.importAliases.layouts}/${layout}.astro`;
1994
2033
  } else {
1995
- const layoutFilePath = secureJoin(layoutsRoot, `${options.layout}.astro`);
2034
+ const layoutFilePath = secureJoin(layoutsRoot, `${layout}.astro`);
1996
2035
  layoutImportPath = getRelativeImportPath(routeFilePath, layoutFilePath);
1997
2036
  }
1998
2037
  }
@@ -2000,7 +2039,7 @@ async function addSectionCommand(route, featurePath, options) {
2000
2039
  let featureImportPath = null;
2001
2040
  if (routeFilePath) {
2002
2041
  if (config.importAliases.features) {
2003
- const entryPart = effectiveOptions.entry === 'index' ? '' : `/${featureComponentName}`;
2042
+ const entryPart = effectiveOptions.entry === 'index' ? '/index' : `/${featureComponentName}`;
2004
2043
  // In Astro, we can often omit the extension for .tsx files, but not for .astro files if using aliases sometimes.
2005
2044
  // However, to be safe, we use the configured extension.
2006
2045
  featureImportPath = `${config.importAliases.features}/${normalizedFeaturePath}${entryPart}${config.naming.featureExtension}`;
@@ -2029,11 +2068,12 @@ async function addSectionCommand(route, featurePath, options) {
2029
2068
  routeSignature = getSignature(config, 'typescript');
2030
2069
  } else {
2031
2070
  routeContent = generateRouteTemplate(
2032
- options.layout,
2071
+ layout,
2033
2072
  layoutImportPath,
2034
2073
  featureImportPath,
2035
2074
  featureComponentName,
2036
- routeExtension
2075
+ routeExtension,
2076
+ layoutProps
2037
2077
  );
2038
2078
  routeSignature = getSignature(config, 'astro');
2039
2079
  }
@@ -2301,7 +2341,7 @@ async function addSectionCommand(route, featurePath, options) {
2301
2341
  name: options.name || featureComponentName,
2302
2342
  route: normalizedRoute,
2303
2343
  featurePath: normalizedFeaturePath,
2304
- layout: options.layout,
2344
+ layout: layout,
2305
2345
  extension: routeExtension
2306
2346
  });
2307
2347
 
@@ -2318,7 +2358,7 @@ async function addSectionCommand(route, featurePath, options) {
2318
2358
  }
2319
2359
  }
2320
2360
 
2321
- async function updateImportsInFile(filePath, oldFilePath, newFilePath) {
2361
+ async function updateImportsInFile$2(filePath, oldFilePath, newFilePath) {
2322
2362
  if (!existsSync(filePath)) return;
2323
2363
 
2324
2364
  let content = await readFile(filePath, 'utf-8');
@@ -2361,33 +2401,43 @@ async function removeSectionCommand(route, featurePath, options) {
2361
2401
 
2362
2402
  const state = await loadState();
2363
2403
 
2364
- let targetRoute = route;
2365
- let targetFeaturePath = featurePath;
2366
2404
  let section = findSection(state, route);
2405
+ if (!section && featurePath) {
2406
+ section = findSection(state, featurePath);
2407
+ }
2408
+
2409
+ const targetRoute = section ? section.route : route;
2410
+ const targetFeaturePath = section ? section.featurePath : featurePath;
2367
2411
 
2368
2412
  if (!targetFeaturePath) {
2369
- if (section) {
2370
- targetRoute = section.route;
2371
- targetFeaturePath = section.featurePath;
2372
- } else {
2373
- throw new Error(`Section not found for identifier: ${route}. Please provide both route and featurePath.`);
2374
- }
2413
+ throw new Error(`Section not found for identifier: ${route}. Please provide both route and featurePath.`);
2375
2414
  }
2376
2415
 
2377
2416
  const normalizedRoute = normalizeRoute(targetRoute);
2378
2417
  const normalizedFeaturePath = featureToDirectoryPath(targetFeaturePath);
2379
2418
 
2380
- const routeExtension = (section && section.extension) || config.naming.routeExtension;
2381
- const routeFileName = routeToFilePath(normalizedRoute, {
2382
- extension: routeExtension,
2383
- mode: config.routing.mode,
2384
- indexFile: config.routing.indexFile
2385
- });
2386
-
2387
2419
  const pagesRoot = resolvePath(config, 'pages');
2388
2420
  const featuresRoot = resolvePath(config, 'features');
2421
+
2422
+ // Find route file in state if possible
2423
+ let routeFilePath = null;
2424
+ const routeRelPath = Object.keys(state.files).find(f => {
2425
+ const data = state.files[f];
2426
+ return data.kind === 'route' && data.owner === normalizedRoute;
2427
+ });
2428
+
2429
+ if (routeRelPath) {
2430
+ routeFilePath = path.resolve(process.cwd(), routeRelPath);
2431
+ } else {
2432
+ const routeExtension = (section && section.extension) || config.naming.routeExtension;
2433
+ const routeFileName = routeToFilePath(normalizedRoute, {
2434
+ extension: routeExtension,
2435
+ mode: config.routing.mode,
2436
+ indexFile: config.routing.indexFile
2437
+ });
2438
+ routeFilePath = secureJoin(pagesRoot, routeFileName);
2439
+ }
2389
2440
 
2390
- const routeFilePath = secureJoin(pagesRoot, routeFileName);
2391
2441
  const featureDirPath = secureJoin(featuresRoot, normalizedFeaturePath);
2392
2442
 
2393
2443
  const deletedFiles = [];
@@ -2474,8 +2524,67 @@ async function removeSectionCommand(route, featurePath, options) {
2474
2524
  }
2475
2525
 
2476
2526
  if (deletedFiles.length === 0 && deletedDirs.length === 0 && skippedFiles.length === 0) {
2477
- console.log('No files to delete.');
2527
+ if (section) {
2528
+ console.log(`✓ Section ${normalizedRoute} removed from state (files were already missing on disk).`);
2529
+ state.sections = state.sections.filter(s => s.route !== normalizedRoute);
2530
+ await saveState(state);
2531
+ } else {
2532
+ console.log('No files to delete.');
2533
+ }
2478
2534
  } else {
2535
+ // Reorganization (Flattening)
2536
+ if (!options.keepRoute && deletedFiles.length > 0 && config.routing.mode === 'flat') {
2537
+ const routeParts = normalizedRoute.split('/').filter(Boolean);
2538
+ if (routeParts.length > 1) {
2539
+ for (let i = routeParts.length - 1; i >= 1; i--) {
2540
+ const parentRoute = '/' + routeParts.slice(0, i).join('/');
2541
+ const parentDirName = routeParts.slice(0, i).join('/');
2542
+ const parentDirPath = secureJoin(pagesRoot, parentDirName);
2543
+
2544
+ if (existsSync(parentDirPath)) {
2545
+ const filesInDir = await readdir(parentDirPath);
2546
+
2547
+ if (filesInDir.length === 1) {
2548
+ const loneFile = filesInDir[0];
2549
+ const ext = path.extname(loneFile);
2550
+ const indexFile = ext === '.astro' ? config.routing.indexFile : `index${ext}`;
2551
+
2552
+ if (loneFile === indexFile) {
2553
+ const loneFilePath = path.join(parentDirPath, loneFile);
2554
+ const oldRelative = path.relative(process.cwd(), loneFilePath).replace(/\\/g, '/');
2555
+
2556
+ if (state.files[oldRelative] && state.files[oldRelative].kind === 'route') {
2557
+ const flatFileName = routeToFilePath(parentRoute, {
2558
+ extension: ext,
2559
+ mode: 'flat'
2560
+ });
2561
+ const flatFilePath = secureJoin(pagesRoot, flatFileName);
2562
+
2563
+ if (!existsSync(flatFilePath)) {
2564
+ await rename(loneFilePath, flatFilePath);
2565
+
2566
+ const newRelative = path.relative(process.cwd(), flatFilePath).replace(/\\/g, '/');
2567
+
2568
+ state.files[newRelative] = { ...state.files[oldRelative] };
2569
+ delete state.files[oldRelative];
2570
+
2571
+ await updateImportsInFile$1(flatFilePath, loneFilePath, flatFilePath);
2572
+
2573
+ // Update hash in state after import updates
2574
+ const content = await readFile(flatFilePath, 'utf-8');
2575
+ state.files[newRelative].hash = calculateHash(content, config.hashing?.normalization);
2576
+
2577
+ console.log(`✓ Reorganized ${oldRelative} to ${newRelative} (flattened)`);
2578
+ await cleanupEmptyDirs(parentDirPath, pagesRoot);
2579
+ }
2580
+ }
2581
+ }
2582
+ }
2583
+ }
2584
+ }
2585
+ }
2586
+ }
2587
+
2479
2588
  state.sections = state.sections.filter(s => s.route !== normalizedRoute);
2480
2589
  await saveState(state);
2481
2590
  }
@@ -2489,6 +2598,39 @@ async function removeSectionCommand(route, featurePath, options) {
2489
2598
  }
2490
2599
  }
2491
2600
 
2601
+ async function updateImportsInFile$1(filePath, oldFilePath, newFilePath) {
2602
+ if (!existsSync(filePath)) return;
2603
+
2604
+ let content = await readFile(filePath, 'utf-8');
2605
+ const oldDir = path.dirname(oldFilePath);
2606
+ const newDir = path.dirname(newFilePath);
2607
+
2608
+ if (oldDir === newDir) return;
2609
+
2610
+ // Find all relative imports
2611
+ const relativeImportRegex = /from\s+['"](\.\.?\/[^'"]+)['"]/g;
2612
+ let match;
2613
+ const replacements = [];
2614
+
2615
+ while ((match = relativeImportRegex.exec(content)) !== null) {
2616
+ const relativePath = match[1];
2617
+ const absoluteTarget = path.resolve(oldDir, relativePath);
2618
+ const newRelativePath = getRelativeImportPath(newFilePath, absoluteTarget);
2619
+
2620
+ replacements.push({
2621
+ full: match[0],
2622
+ oldRel: relativePath,
2623
+ newRel: newRelativePath
2624
+ });
2625
+ }
2626
+
2627
+ for (const repl of replacements) {
2628
+ content = content.replace(repl.full, `from '${repl.newRel}'`);
2629
+ }
2630
+
2631
+ await writeFile(filePath, content, 'utf-8');
2632
+ }
2633
+
2492
2634
  /**
2493
2635
  * Move a section (route + feature).
2494
2636
  *
@@ -2629,9 +2771,14 @@ async function moveSectionCommand(fromRoute, fromFeature, toRoute, toFeature, op
2629
2771
  const fromFeatureComponentName = getFeatureComponentName(normalizedFromFeature);
2630
2772
  const toFeatureComponentName = getFeatureComponentName(targetFeature);
2631
2773
 
2632
- // Update component name in JSX
2774
+ // First, update all relative imports in the file because it moved
2775
+ await updateImportsInFile(toRoutePath, fromRoutePath, toRoutePath);
2776
+
2777
+ let content = await readFile(toRoutePath, 'utf-8');
2778
+ let changed = false;
2779
+
2780
+ // Update component name in JSX tags
2633
2781
  if (fromFeatureComponentName !== toFeatureComponentName) {
2634
- let content = await readFile(toRoutePath, 'utf-8');
2635
2782
  content = content.replace(
2636
2783
  new RegExp(`<${fromFeatureComponentName}`, 'g'),
2637
2784
  `<${toFeatureComponentName}`
@@ -2640,44 +2787,54 @@ async function moveSectionCommand(fromRoute, fromFeature, toRoute, toFeature, op
2640
2787
  new RegExp(`</${fromFeatureComponentName}`, 'g'),
2641
2788
  `</${toFeatureComponentName}`
2642
2789
  );
2643
- await writeFile(toRoutePath, content, 'utf-8');
2790
+ changed = true;
2644
2791
  }
2645
2792
 
2646
2793
  if (config.importAliases.features) {
2647
- if (normalizedFromFeature !== targetFeature) {
2648
- const oldAliasPath = `${config.importAliases.features}/${normalizedFromFeature}`;
2649
- const newAliasPath = `${config.importAliases.features}/${targetFeature}`;
2650
- const ext = config.naming.featureExtension === '.astro' ? '.astro' : '';
2651
-
2652
- // Replace both the path and the component name if they are different
2653
- await updateSignature(toRoutePath,
2654
- `import ${fromFeatureComponentName} from '${oldAliasPath}/${fromFeatureComponentName}${ext}'`,
2655
- `import ${toFeatureComponentName} from '${newAliasPath}/${toFeatureComponentName}${ext}'`
2656
- );
2657
-
2658
- // Fallback for prefix only replacement
2659
- await updateSignature(toRoutePath, oldAliasPath, newAliasPath);
2660
- } else if (fromFeatureComponentName !== toFeatureComponentName) {
2661
- // Name changed but path didn't
2662
- const aliasPath = `${config.importAliases.features}/${targetFeature}`;
2663
- const ext = config.naming.featureExtension === '.astro' ? '.astro' : '';
2664
- await updateSignature(toRoutePath,
2665
- `import ${fromFeatureComponentName} from '${aliasPath}/${fromFeatureComponentName}${ext}'`,
2666
- `import ${toFeatureComponentName} from '${aliasPath}/${toFeatureComponentName}${ext}'`
2667
- );
2794
+ const oldAliasPath = `${config.importAliases.features}/${normalizedFromFeature}`;
2795
+ const newAliasPath = `${config.importAliases.features}/${targetFeature}`;
2796
+
2797
+ // Flexible regex to match import identifier and path with alias
2798
+ const importRegex = new RegExp(`(import\\s+)(${fromFeatureComponentName})(\\s+from\\s+['"])${oldAliasPath}(/[^'"]+)?(['"])`, 'g');
2799
+
2800
+ if (importRegex.test(content)) {
2801
+ content = content.replace(importRegex, (match, p1, p2, p3, subPath, p5) => {
2802
+ let newSubPath = subPath || '';
2803
+ if (subPath && subPath.includes(fromFeatureComponentName)) {
2804
+ newSubPath = subPath.replace(fromFeatureComponentName, toFeatureComponentName);
2805
+ }
2806
+ return `${p1}${toFeatureComponentName}${p3}${newAliasPath}${newSubPath}${p5}`;
2807
+ });
2808
+ changed = true;
2809
+ } else if (content.includes(oldAliasPath)) {
2810
+ // Fallback for path only replacement
2811
+ content = content.replace(new RegExp(oldAliasPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), newAliasPath);
2812
+ changed = true;
2668
2813
  }
2669
2814
  } else {
2670
- const oldRelativeDir = getRelativeImportPath(fromRoutePath, fromFeatureDirPath);
2815
+ const oldRelativeDir = getRelativeImportPath(toRoutePath, fromFeatureDirPath);
2671
2816
  const newRelativeDir = getRelativeImportPath(toRoutePath, toFeatureDirPath);
2672
- const ext = config.naming.featureExtension === '.astro' ? '.astro' : '';
2673
2817
 
2674
- const oldImportPath = `import ${fromFeatureComponentName} from '${oldRelativeDir}/${fromFeatureComponentName}${ext}'`;
2675
- const newImportPath = `import ${toFeatureComponentName} from '${newRelativeDir}/${toFeatureComponentName}${ext}'`;
2818
+ // Flexible regex for relative imports
2819
+ const relImportRegex = new RegExp(`(import\\s+)(${fromFeatureComponentName})(\\s+from\\s+['"])${oldRelativeDir}(/[^'"]+)?(['"])`, 'g');
2676
2820
 
2677
- if (oldImportPath !== newImportPath) {
2678
- await updateSignature(toRoutePath, oldImportPath, newImportPath);
2821
+ if (relImportRegex.test(content)) {
2822
+ content = content.replace(relImportRegex, (match, p1, p2, p3, subPath, p5) => {
2823
+ let newSubPath = subPath || '';
2824
+ if (subPath && subPath.includes(fromFeatureComponentName)) {
2825
+ newSubPath = subPath.replace(fromFeatureComponentName, toFeatureComponentName);
2826
+ }
2827
+ return `${p1}${toFeatureComponentName}${p3}${newRelativeDir}${newSubPath}${p5}`;
2828
+ });
2829
+ changed = true;
2679
2830
  }
2680
2831
  }
2832
+
2833
+ if (changed) {
2834
+ await writeFile(toRoutePath, content, 'utf-8');
2835
+ // Update hash in state after changes
2836
+ state.files[normalizedToRouteRelative].hash = calculateHash(content, config.hashing?.normalization);
2837
+ }
2681
2838
  }
2682
2839
 
2683
2840
  if (!isRouteOnly && normalizedFromFeature && normalizedToFeature && normalizedFromFeature !== normalizedToFeature) {
@@ -2748,7 +2905,6 @@ async function scanAndReplaceImports(config, state, fromInfo, toInfo, options) {
2748
2905
  const { toFeaturePath, toComponentName } = toInfo;
2749
2906
 
2750
2907
  const allFiles = new Set();
2751
- const { scanDirectory, calculateHash } = await Promise.resolve().then(function () { return filesystem; });
2752
2908
  await scanDirectory(process.cwd(), allFiles);
2753
2909
 
2754
2910
  const featuresRoot = resolvePath(config, 'features');
@@ -2889,7 +3045,6 @@ async function moveDirectory(fromPath, toPath, state, config, options = {}) {
2889
3045
  if (hasChanged) {
2890
3046
  await writeFile(toEntryPath, content, 'utf-8');
2891
3047
  // Re-calculate hash after content update
2892
- const { calculateHash } = await Promise.resolve().then(function () { return filesystem; });
2893
3048
  const updatedHash = calculateHash(content, config.hashing?.normalization);
2894
3049
 
2895
3050
  const normalizedToRelative = path.relative(process.cwd(), toEntryPath).replace(/\\/g, '/');
@@ -2922,6 +3077,39 @@ async function moveDirectory(fromPath, toPath, state, config, options = {}) {
2922
3077
  }
2923
3078
  }
2924
3079
 
3080
+ async function updateImportsInFile(filePath, oldFilePath, newFilePath) {
3081
+ if (!existsSync(filePath)) return;
3082
+
3083
+ let content = await readFile(filePath, 'utf-8');
3084
+ const oldDir = path.dirname(oldFilePath);
3085
+ const newDir = path.dirname(newFilePath);
3086
+
3087
+ if (oldDir === newDir) return;
3088
+
3089
+ // Find all relative imports
3090
+ const relativeImportRegex = /from\s+['"](\.\.?\/[^'"]+)['"]/g;
3091
+ let match;
3092
+ const replacements = [];
3093
+
3094
+ while ((match = relativeImportRegex.exec(content)) !== null) {
3095
+ const relativePath = match[1];
3096
+ const absoluteTarget = path.resolve(oldDir, relativePath);
3097
+ const newRelativePath = getRelativeImportPath(newFilePath, absoluteTarget);
3098
+
3099
+ replacements.push({
3100
+ full: match[0],
3101
+ oldRel: relativePath,
3102
+ newRel: newRelativePath
3103
+ });
3104
+ }
3105
+
3106
+ for (const repl of replacements) {
3107
+ content = content.replace(repl.full, `from '${repl.newRel}'`);
3108
+ }
3109
+
3110
+ await writeFile(filePath, content, 'utf-8');
3111
+ }
3112
+
2925
3113
  async function createComponentCommand(componentName, options) {
2926
3114
  try {
2927
3115
  const config = await loadConfig();
@@ -3402,9 +3590,13 @@ async function removeComponentCommand(identifier, options) {
3402
3590
  owner: identifier
3403
3591
  });
3404
3592
 
3405
- if (result.deleted) {
3406
- console.log(`✓ Deleted component: ${componentDir}/`);
3407
- await cleanupEmptyDirs(path.dirname(componentDir), path.join(process.cwd(), config.paths.components));
3593
+ if (result.deleted || (result.reason === 'not-found' && component)) {
3594
+ if (result.deleted) {
3595
+ console.log(`✓ Deleted component: ${componentDir}/`);
3596
+ await cleanupEmptyDirs(path.dirname(componentDir), path.join(process.cwd(), config.paths.components));
3597
+ } else {
3598
+ console.log(`✓ Component ${identifier} removed from state (directory was already missing on disk).`);
3599
+ }
3408
3600
 
3409
3601
  // Unregister files
3410
3602
  const relComponentPath = path.relative(process.cwd(), componentDir).replace(/\\/g, '/');
@@ -4155,7 +4347,7 @@ program
4155
4347
  .command('add-section [route] [featurePath]')
4156
4348
  .description('Create a route + feature binding (route optional for standalone features)')
4157
4349
  .option('--preset <name>', 'Scaffolding preset (minimal, standard, senior)')
4158
- .option('--layout <name>', 'Layout component name (use "none" for no layout)', 'Main')
4350
+ .option('--layout <name>', 'Layout component name (use "none" for no layout)')
4159
4351
  .option('--name <name>', 'Section name for state tracking')
4160
4352
  .option('--endpoint', 'Create an API endpoint (.ts) instead of an Astro page')
4161
4353
  .option('--api', 'Create api directory')
@@ -4170,6 +4362,11 @@ program
4170
4362
  .option('--index', 'Create index.ts')
4171
4363
  .option('--no-sub-components-dir', 'Skip creating sub-components directory')
4172
4364
  .option('--no-scripts-dir', 'Skip creating scripts directory')
4365
+ .option('--prop <key=value>', 'Layout property', (val, memo) => {
4366
+ const [key, ...rest] = val.split('=');
4367
+ memo[key] = rest.join('=');
4368
+ return memo;
4369
+ }, {})
4173
4370
  .option('--dry-run', 'Show what would be created without creating')
4174
4371
  .option('--force', 'Overwrite existing files')
4175
4372
  .action(addSectionCommand);
@@ -1 +1 @@
1
- {"version":3,"file":"textor.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"textor.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}