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