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