@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 +30 -2
- package/dist/bin/textor.js +284 -87
- package/dist/bin/textor.js.map +1 -1
- package/dist/index.cjs +241 -78
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +241 -78
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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}}` |
|
package/dist/bin/textor.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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}/${
|
|
2032
|
+
layoutImportPath = `${config.importAliases.layouts}/${layout}.astro`;
|
|
1994
2033
|
} else {
|
|
1995
|
-
const layoutFilePath = secureJoin(layoutsRoot, `${
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
2790
|
+
changed = true;
|
|
2644
2791
|
}
|
|
2645
2792
|
|
|
2646
2793
|
if (config.importAliases.features) {
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
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(
|
|
2815
|
+
const oldRelativeDir = getRelativeImportPath(toRoutePath, fromFeatureDirPath);
|
|
2671
2816
|
const newRelativeDir = getRelativeImportPath(toRoutePath, toFeatureDirPath);
|
|
2672
|
-
const ext = config.naming.featureExtension === '.astro' ? '.astro' : '';
|
|
2673
2817
|
|
|
2674
|
-
|
|
2675
|
-
const
|
|
2818
|
+
// Flexible regex for relative imports
|
|
2819
|
+
const relImportRegex = new RegExp(`(import\\s+)(${fromFeatureComponentName})(\\s+from\\s+['"])${oldRelativeDir}(/[^'"]+)?(['"])`, 'g');
|
|
2676
2820
|
|
|
2677
|
-
if (
|
|
2678
|
-
|
|
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
|
-
|
|
3407
|
-
|
|
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)'
|
|
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);
|
package/dist/bin/textor.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"textor.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"textor.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
|