@starodubenko/fsd-gen 1.2.1-0 → 1.4.0-0

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
@@ -45,6 +45,53 @@ Reverse engineer existing code into reusable presets.
45
45
  2. **Analyze**: `fsd-gen reverse:analyze <preset-name>`
46
46
  3. **Build**: `fsd-gen reverse:build <preset-name> [--mode short|ejected]`
47
47
 
48
+ #### Configuration Examples
49
+
50
+ **Single Root:**
51
+ ```typescript
52
+ export default {
53
+ root: 'src/entities/User',
54
+ targetLayer: 'entity'
55
+ };
56
+ ```
57
+
58
+ **Multiple Roots with Conflict Resolution:**
59
+ ```typescript
60
+ export default {
61
+ root: [
62
+ 'src/entities/User',
63
+ 'src/features/User', // Auto-resolved as User1
64
+ 'src/widgets/User' // Auto-resolved as User2
65
+ ],
66
+ targetLayer: 'entity'
67
+ };
68
+ ```
69
+
70
+ **Multiple Layers with Array Support:**
71
+ ```typescript
72
+ export default {
73
+ layers: [
74
+ { root: 'src/entities/User', targetLayer: 'entity' },
75
+ {
76
+ root: ['src/features/Auth', 'src/features/Payment'],
77
+ targetLayer: 'feature'
78
+ }
79
+ ]
80
+ };
81
+ ```
82
+
83
+ **Features:**
84
+ - **Multi-Root Support**: Analyze multiple directories at once
85
+ - **Automatic Conflict Resolution**: Numeric suffixes (User, User1, User2)
86
+ - **Folder Name Normalization**: `user-action` → `UserAction`, `user_profile` → `UserProfile`
87
+
88
+ **Plural/Singular Recognition**: Automatically detects both forms of names:
89
+ - Analyzing "User" will also find "Users"
90
+ - Analyzing "Users" will also find "User"
91
+ - Works with common patterns: Category/Categories, Box/Boxes, etc.
92
+ - **TypeScript Config**: Type-safe `preset.config.ts` with enums
93
+
94
+
48
95
  #### Modes
49
96
  - **short** (default): Generates a thin `preset.ts` that auto-discovers templates at runtime.
50
97
  - **ejected**: Compiles and copies all source files into the preset folder as static templates.
@@ -1 +1 @@
1
- {"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../../../src/lib/reverse/analyze.ts"],"names":[],"mappings":"AAiBA;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,iBA+GlF"}
1
+ {"version":3,"file":"analyze.d.ts","sourceRoot":"","sources":["../../../src/lib/reverse/analyze.ts"],"names":[],"mappings":"AAiBA;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,UAAU,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,iBAiHlF"}
@@ -26,8 +26,10 @@ export async function analyzeReversePreset(presetName, templatesDir) {
26
26
  }
27
27
  console.log(`Analyzing source at: ${rootPath} (Layer: ${layer.targetLayer})`);
28
28
  // Normalize folder name to PascalCase for better token recognition
29
+ // Use resolvedName if provided (for conflict resolution), otherwise use basename
29
30
  // e.g., "user-action" -> "UserAction"
30
- const subjectName = toPascalCase(basename(rootPath));
31
+ const folderName = layer.resolvedName || basename(rootPath);
32
+ const subjectName = toPascalCase(folderName);
31
33
  const variations = generateVariations(subjectName);
32
34
  // Discovery
33
35
  project.addSourceFilesAtPaths(join(rootPath, '**', '*.{ts,tsx,css,scss,less,sass}'));
@@ -78,8 +80,8 @@ export async function analyzeReversePreset(presetName, templatesDir) {
78
80
  // Generate TypeScript content with enum values
79
81
  const filesContent = resultFiles.map(file => {
80
82
  const tokensStr = Object.entries(file.tokens)
81
- .map(([key, value]) => ` "${key}": ${tokenToEnum(value)}`)
82
- .join(',\n');
83
+ .map(([key, value]) => `"${key}": ${tokenToEnum(value)}`)
84
+ .join(',\n ');
83
85
  return ` {
84
86
  "path": "${file.path}",
85
87
  "targetLayer": ${layerToEnum(file.targetLayer)},
@@ -9,7 +9,18 @@
9
9
  */
10
10
  export declare function toPascalCase(str: string): string;
11
11
  /**
12
- * Generates naming variations for a given subject string
12
+ * Simple pluralization - adds 's' or handles common patterns
13
+ * Examples: User -> Users, Category -> Categories, Box -> Boxes
14
+ */
15
+ export declare function pluralize(word: string): string;
16
+ /**
17
+ * Simple singularization - removes 's' or handles common patterns
18
+ * Examples: Users -> User, Categories -> Category, Boxes -> Box
19
+ */
20
+ export declare function singularize(word: string): string;
21
+ /**
22
+ * Generates naming variations for a given subject string.
23
+ * Now includes plural and singular forms.
13
24
  */
14
25
  export declare function generateVariations(subject: string): {
15
26
  pascal: string;
@@ -17,6 +28,16 @@ export declare function generateVariations(subject: string): {
17
28
  lower: string;
18
29
  upper: string;
19
30
  kebab: string;
31
+ plural: string;
32
+ pluralCamel: string;
33
+ pluralLower: string;
34
+ pluralUpper: string;
35
+ pluralKebab: string;
36
+ singular: string;
37
+ singularCamel: string;
38
+ singularLower: string;
39
+ singularUpper: string;
40
+ singularKebab: string;
20
41
  };
21
42
  /**
22
43
  * Identifies potential tokens in content based on variations
@@ -1 +1 @@
1
- {"version":3,"file":"analyzeHelpers.d.ts","sourceRoot":"","sources":["../../../src/lib/reverse/analyzeHelpers.ts"],"names":[],"mappings":"AAKA;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAahD;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM;;;;;;EAUjD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,CAAC,OAAO,kBAAkB,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAyBzH;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,SAAS,GAAE,MAAW,GAAG,MAAM,CAMxG"}
1
+ {"version":3,"file":"analyzeHelpers.d.ts","sourceRoot":"","sources":["../../../src/lib/reverse/analyzeHelpers.ts"],"names":[],"mappings":"AAKA;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAahD;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAiB9C;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAoBhD;AAED;;;GAGG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM;;;;;;;;;;;;;;;;EA4BjD;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,UAAU,EAAE,UAAU,CAAC,OAAO,kBAAkB,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA2DzH;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,CAAC,EAAE,MAAM,EAAE,SAAS,GAAE,MAAW,GAAG,MAAM,CAMxG"}
@@ -22,7 +22,50 @@ export function toPascalCase(str) {
22
22
  return str.charAt(0).toUpperCase() + str.slice(1);
23
23
  }
24
24
  /**
25
- * Generates naming variations for a given subject string
25
+ * Simple pluralization - adds 's' or handles common patterns
26
+ * Examples: User -> Users, Category -> Categories, Box -> Boxes
27
+ */
28
+ export function pluralize(word) {
29
+ if (!word)
30
+ return word;
31
+ // Check for special endings that need 'es' FIRST (like 'ss', 'x', 'ch', 'sh')
32
+ if (word.endsWith('ss') || word.endsWith('x') || word.endsWith('ch') || word.endsWith('sh')) {
33
+ return word + 'es'; // Class -> Classes, Box -> Boxes, Batch -> Batches
34
+ }
35
+ // Already plural (ends with 's' but not 'ss')
36
+ if (word.endsWith('s'))
37
+ return word;
38
+ // Common patterns
39
+ if (word.endsWith('y') && !['a', 'e', 'i', 'o', 'u'].includes(word[word.length - 2]?.toLowerCase())) {
40
+ return word.slice(0, -1) + 'ies'; // Category -> Categories
41
+ }
42
+ return word + 's'; // User -> Users
43
+ }
44
+ /**
45
+ * Simple singularization - removes 's' or handles common patterns
46
+ * Examples: Users -> User, Categories -> Category, Boxes -> Box
47
+ */
48
+ export function singularize(word) {
49
+ if (!word)
50
+ return word;
51
+ // Common patterns
52
+ if (word.endsWith('ies')) {
53
+ if (word.length > 4) {
54
+ return word.slice(0, -3) + 'y'; // Categories -> Category
55
+ }
56
+ // Short words like 'Ties' -> 'Tie' (remove 's') or handle as special
57
+ }
58
+ if (word.endsWith('xes') || word.endsWith('ches') || word.endsWith('shes') || word.endsWith('sses')) {
59
+ return word.slice(0, -2); // Boxes -> Box, Batches -> Batch
60
+ }
61
+ if (word.endsWith('s') && !word.endsWith('ss')) {
62
+ return word.slice(0, -1); // Users -> User
63
+ }
64
+ return word; // Already singular or special case
65
+ }
66
+ /**
67
+ * Generates naming variations for a given subject string.
68
+ * Now includes plural and singular forms.
26
69
  */
27
70
  export function generateVariations(subject) {
28
71
  const pascal = subject.charAt(0).toUpperCase() + subject.slice(1);
@@ -31,7 +74,22 @@ export function generateVariations(subject) {
31
74
  const upper = subject.toUpperCase();
32
75
  // Simple kebab conversion (UserProfile -> user-profile)
33
76
  const kebab = camel.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
34
- return { pascal, camel, lower, upper, kebab };
77
+ // Generate plural and singular forms
78
+ const plural = pluralize(pascal);
79
+ const singular = singularize(pascal);
80
+ const pluralCamel = plural.charAt(0).toLowerCase() + plural.slice(1);
81
+ const pluralLower = plural.toLowerCase();
82
+ const pluralUpper = plural.toUpperCase();
83
+ const pluralKebab = pluralCamel.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
84
+ const singularCamel = singular.charAt(0).toLowerCase() + singular.slice(1);
85
+ const singularLower = singular.toLowerCase();
86
+ const singularUpper = singular.toUpperCase();
87
+ const singularKebab = singularCamel.replace(/[A-Z]/g, m => '-' + m.toLowerCase());
88
+ return {
89
+ pascal, camel, lower, upper, kebab,
90
+ plural, pluralCamel, pluralLower, pluralUpper, pluralKebab,
91
+ singular, singularCamel, singularLower, singularUpper, singularKebab
92
+ };
35
93
  }
36
94
  /**
37
95
  * Identifies potential tokens in content based on variations
@@ -54,6 +112,38 @@ export function identifyTokens(content, variations) {
54
112
  if (variations.kebab !== variations.camel && content.includes(variations.kebab)) {
55
113
  tokens[variations.kebab] = EntityToken.ENTITY_NAME_KEBAB;
56
114
  }
115
+ // Add plural variations if different from original
116
+ if (variations.plural !== variations.pascal && content.includes(variations.plural)) {
117
+ tokens[variations.plural] = EntityToken.ENTITY_NAME;
118
+ }
119
+ if (variations.pluralCamel !== variations.camel && content.includes(variations.pluralCamel)) {
120
+ tokens[variations.pluralCamel] = EntityToken.ENTITY_NAME_CAMEL;
121
+ }
122
+ if (variations.pluralLower !== variations.lower && content.includes(variations.pluralLower)) {
123
+ tokens[variations.pluralLower] = EntityToken.ENTITY_NAME_LOWER;
124
+ }
125
+ if (variations.pluralUpper !== variations.upper && content.includes(variations.pluralUpper)) {
126
+ tokens[variations.pluralUpper] = EntityToken.ENTITY_NAME_UPPER;
127
+ }
128
+ if (variations.pluralKebab !== variations.kebab && content.includes(variations.pluralKebab)) {
129
+ tokens[variations.pluralKebab] = EntityToken.ENTITY_NAME_KEBAB;
130
+ }
131
+ // Add singular variations if different from original and plural
132
+ if (variations.singular !== variations.pascal && variations.singular !== variations.plural && content.includes(variations.singular)) {
133
+ tokens[variations.singular] = EntityToken.ENTITY_NAME;
134
+ }
135
+ if (variations.singularCamel !== variations.camel && variations.singularCamel !== variations.pluralCamel && content.includes(variations.singularCamel)) {
136
+ tokens[variations.singularCamel] = EntityToken.ENTITY_NAME_CAMEL;
137
+ }
138
+ if (variations.singularLower !== variations.lower && variations.singularLower !== variations.pluralLower && content.includes(variations.singularLower)) {
139
+ tokens[variations.singularLower] = EntityToken.ENTITY_NAME_LOWER;
140
+ }
141
+ if (variations.singularUpper !== variations.upper && variations.singularUpper !== variations.pluralUpper && content.includes(variations.singularUpper)) {
142
+ tokens[variations.singularUpper] = EntityToken.ENTITY_NAME_UPPER;
143
+ }
144
+ if (variations.singularKebab !== variations.kebab && variations.singularKebab !== variations.pluralKebab && content.includes(variations.singularKebab)) {
145
+ tokens[variations.singularKebab] = EntityToken.ENTITY_NAME_KEBAB;
146
+ }
57
147
  return tokens;
58
148
  }
59
149
  /**
@@ -1,4 +1,4 @@
1
- import { PresetConfig, PresetConfigFile, PresetConfigTokenMap, PresetSourceConfig, PresetSourceItem } from './types.js';
1
+ import { PresetConfig, PresetConfigFile, PresetConfigTokenMap, PresetSourceConfig, NormalizedPresetSourceItem } from './types.js';
2
2
  /**
3
3
  * Loads the reverse engineering source configuration (preset.source.ts)
4
4
  */
@@ -17,7 +17,7 @@ export declare function loadReverseEnvironment(presetDir: string): Promise<{
17
17
  /**
18
18
  * Normalizes layer definitions from source configuration
19
19
  */
20
- export declare function normalizeLayers(sourceConfig: PresetSourceConfig): PresetSourceItem[];
20
+ export declare function normalizeLayers(sourceConfig: PresetSourceConfig): NormalizedPresetSourceItem[];
21
21
  /**
22
22
  * Identifies the main entity token (mapping to {{name}} or {{entityName}})
23
23
  */
@@ -29,7 +29,7 @@ export declare function applyTokens(content: string, tokens: PresetConfigTokenMa
29
29
  /**
30
30
  * Guesses naming conventions (prefixes/suffixes) from source roots
31
31
  */
32
- export declare function guessConventions(layers: PresetSourceItem[], entityToken: string): Record<string, string>;
32
+ export declare function guessConventions(layers: NormalizedPresetSourceItem[], entityToken: string): Record<string, string>;
33
33
  /**
34
34
  * Generates preset.ts content for short mode
35
35
  */
@@ -37,5 +37,5 @@ export declare function generateShortPresetContent(helpers: Record<string, strin
37
37
  /**
38
38
  * Generates preset.ts content for ejected mode
39
39
  */
40
- export declare function generateEjectedPresetContent(presetName: string, files: PresetConfigFile[], layers: PresetSourceItem[], entityToken: string): string;
40
+ export declare function generateEjectedPresetContent(presetName: string, files: PresetConfigFile[], layers: NormalizedPresetSourceItem[], entityToken: string): string;
41
41
  //# sourceMappingURL=buildHelpers.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"buildHelpers.d.ts","sourceRoot":"","sources":["../../../src/lib/reverse/buildHelpers.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAExH;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAUrF;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAiB/E;AAED;;GAEG;AACH,wBAAsB,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;IACrE,YAAY,EAAE,kBAAkB,CAAC;IACjC,YAAY,EAAE,YAAY,CAAC;CAC9B,CAAC,CAID;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,kBAAkB,GAAG,gBAAgB,EAAE,CAWpF;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,YAAY,GAAG,MAAM,CAcpE;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,oBAAoB,GAAG,MAAM,CAUjF;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,gBAAgB,EAAE,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA2BxG;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAiBlF;AAED;;GAEG;AACH,wBAAgB,4BAA4B,CACxC,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,gBAAgB,EAAE,EACzB,MAAM,EAAE,gBAAgB,EAAE,EAC1B,WAAW,EAAE,MAAM,GACpB,MAAM,CAqCR"}
1
+ {"version":3,"file":"buildHelpers.d.ts","sourceRoot":"","sources":["../../../src/lib/reverse/buildHelpers.ts"],"names":[],"mappings":"AAMA,OAAO,EAAE,YAAY,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,kBAAkB,EAAoB,0BAA0B,EAAE,MAAM,YAAY,CAAC;AAEpJ;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAUrF;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,CAAC,CAiB/E;AAED;;GAEG;AACH,wBAAsB,sBAAsB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC;IACrE,YAAY,EAAE,kBAAkB,CAAC;IACjC,YAAY,EAAE,YAAY,CAAC;CAC9B,CAAC,CAID;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,kBAAkB,GAAG,0BAA0B,EAAE,CAoD9F;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,YAAY,EAAE,YAAY,GAAG,MAAM,CAcpE;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,oBAAoB,GAAG,MAAM,CAUjF;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,0BAA0B,EAAE,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA2BlH;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,MAAM,CAiBlF;AAED;;GAEG;AACH,wBAAgB,4BAA4B,CACxC,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,gBAAgB,EAAE,EACzB,MAAM,EAAE,0BAA0B,EAAE,EACpC,WAAW,EAAE,MAAM,GACpB,MAAM,CAqCR"}
@@ -46,17 +46,61 @@ export async function loadReverseEnvironment(presetDir) {
46
46
  * Normalizes layer definitions from source configuration
47
47
  */
48
48
  export function normalizeLayers(sourceConfig) {
49
- const layers = [];
49
+ const result = [];
50
50
  if (sourceConfig.layers) {
51
- layers.push(...sourceConfig.layers);
51
+ for (const layer of sourceConfig.layers) {
52
+ const roots = Array.isArray(layer.root) ? layer.root : [layer.root];
53
+ if (roots.length === 1) {
54
+ result.push({ root: roots[0], targetLayer: layer.targetLayer });
55
+ }
56
+ else {
57
+ const basenames = roots.map(r => basename(r));
58
+ const nameCount = new Map();
59
+ const nameIndices = new Map();
60
+ basenames.forEach(name => { nameCount.set(name, (nameCount.get(name) || 0) + 1); });
61
+ roots.forEach((root, index) => {
62
+ const name = basenames[index];
63
+ const count = nameCount.get(name) || 1;
64
+ if (count > 1) {
65
+ const currentIndex = nameIndices.get(name) || 0;
66
+ nameIndices.set(name, currentIndex + 1);
67
+ const resolvedName = currentIndex === 0 ? name : `${name}${currentIndex}`;
68
+ result.push({ root, targetLayer: layer.targetLayer, resolvedName });
69
+ }
70
+ else {
71
+ result.push({ root, targetLayer: layer.targetLayer });
72
+ }
73
+ });
74
+ }
75
+ }
52
76
  }
53
77
  else if (sourceConfig.root) {
54
- layers.push({
55
- root: sourceConfig.root,
56
- targetLayer: sourceConfig.targetLayer || 'entity'
57
- });
78
+ const roots = Array.isArray(sourceConfig.root) ? sourceConfig.root : [sourceConfig.root];
79
+ const targetLayer = sourceConfig.targetLayer || 'entity';
80
+ if (roots.length === 1) {
81
+ result.push({ root: roots[0], targetLayer });
82
+ }
83
+ else {
84
+ const basenames = roots.map(r => basename(r));
85
+ const nameCount = new Map();
86
+ const nameIndices = new Map();
87
+ basenames.forEach(name => { nameCount.set(name, (nameCount.get(name) || 0) + 1); });
88
+ roots.forEach((root, index) => {
89
+ const name = basenames[index];
90
+ const count = nameCount.get(name) || 1;
91
+ if (count > 1) {
92
+ const currentIndex = nameIndices.get(name) || 0;
93
+ nameIndices.set(name, currentIndex + 1);
94
+ const resolvedName = currentIndex === 0 ? name : `${name}${currentIndex}`;
95
+ result.push({ root, targetLayer, resolvedName });
96
+ }
97
+ else {
98
+ result.push({ root, targetLayer });
99
+ }
100
+ });
101
+ }
58
102
  }
59
- return layers;
103
+ return result;
60
104
  }
61
105
  /**
62
106
  * Identifies the main entity token (mapping to {{name}} or {{entityName}})
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Normalizes layer definitions from source configuration.
3
+ * Handles conflict resolution when multiple roots have the same folder name.
4
+ */
5
+ export declare function normalizeLayers(sourceConfig: PresetSourceConfig): PresetSourceItem[];
6
+ //# sourceMappingURL=buildHelpers_fixed.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buildHelpers_fixed.d.ts","sourceRoot":"","sources":["../../../src/lib/reverse/buildHelpers_fixed.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,kBAAkB,GAAG,gBAAgB,EAAE,CAwDpF"}
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Normalizes layer definitions from source configuration.
3
+ * Handles conflict resolution when multiple roots have the same folder name.
4
+ */
5
+ export function normalizeLayers(sourceConfig) {
6
+ const layers = [];
7
+ if (sourceConfig.layers) {
8
+ layers.push(...sourceConfig.layers);
9
+ }
10
+ else if (sourceConfig.root) {
11
+ const roots = Array.isArray(sourceConfig.root) ? sourceConfig.root : [sourceConfig.root];
12
+ const targetLayer = sourceConfig.targetLayer || 'entity';
13
+ // If single root, no conflict possible
14
+ if (roots.length === 1) {
15
+ layers.push({
16
+ root: roots[0],
17
+ targetLayer
18
+ });
19
+ }
20
+ else {
21
+ // Multiple roots: detect conflicts and append indices
22
+ const basenames = roots.map(r => basename(r));
23
+ const nameCount = new Map();
24
+ const nameIndices = new Map();
25
+ // Count occurrences of each basename
26
+ basenames.forEach(name => {
27
+ nameCount.set(name, (nameCount.get(name) || 0) + 1);
28
+ });
29
+ // Create layers with conflict-resolved names
30
+ roots.forEach((root, index) => {
31
+ const name = basenames[index];
32
+ const count = nameCount.get(name) || 1;
33
+ // If name appears multiple times, set resolvedName
34
+ if (count > 1) {
35
+ const currentIndex = nameIndices.get(name) || 0;
36
+ nameIndices.set(name, currentIndex + 1);
37
+ // First occurrence keeps original name, subsequent get index suffix
38
+ const resolvedName = currentIndex === 0 ? name : `${name}${currentIndex}`;
39
+ layers.push({
40
+ root,
41
+ targetLayer,
42
+ resolvedName
43
+ });
44
+ }
45
+ else {
46
+ // No conflict, no need for resolvedName
47
+ layers.push({
48
+ root,
49
+ targetLayer
50
+ });
51
+ }
52
+ });
53
+ }
54
+ }
55
+ return layers;
56
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Normalizes layer definitions from source configuration.
3
+ * Expands layers with array roots and handles conflict resolution.
4
+ */
5
+ export declare function normalizeLayers(sourceConfig: PresetSourceConfig): PresetSourceItem[];
6
+ //# sourceMappingURL=buildHelpers_normalizeLayers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buildHelpers_normalizeLayers.d.ts","sourceRoot":"","sources":["../../../src/lib/reverse/buildHelpers_normalizeLayers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,kBAAkB,GAAG,gBAAgB,EAAE,CA8DpF"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Normalizes layer definitions from source configuration.
3
+ * Expands layers with array roots and handles conflict resolution.
4
+ */
5
+ export function normalizeLayers(sourceConfig) {
6
+ const result = [];
7
+ if (sourceConfig.layers) {
8
+ // Expand each layer that might have array roots
9
+ for (const layer of sourceConfig.layers) {
10
+ const roots = Array.isArray(layer.root) ? layer.root : [layer.root];
11
+ if (roots.length === 1) {
12
+ result.push({ root: roots[0], targetLayer: layer.targetLayer });
13
+ }
14
+ else {
15
+ // Multiple roots in this layer: detect conflicts and append indices
16
+ const basenames = roots.map(r => basename(r));
17
+ const nameCount = new Map();
18
+ const nameIndices = new Map();
19
+ basenames.forEach(name => {
20
+ nameCount.set(name, (nameCount.get(name) || 0) + 1);
21
+ });
22
+ roots.forEach((root, index) => {
23
+ const name = basenames[index];
24
+ const count = nameCount.get(name) || 1;
25
+ if (count > 1) {
26
+ const currentIndex = nameIndices.get(name) || 0;
27
+ nameIndices.set(name, currentIndex + 1);
28
+ const resolvedName = currentIndex === 0 ? name : `${name}${currentIndex}`;
29
+ result.push({ root, targetLayer: layer.targetLayer, resolvedName });
30
+ }
31
+ else {
32
+ result.push({ root, targetLayer: layer.targetLayer });
33
+ }
34
+ });
35
+ }
36
+ }
37
+ }
38
+ else if (sourceConfig.root) {
39
+ const roots = Array.isArray(sourceConfig.root) ? sourceConfig.root : [sourceConfig.root];
40
+ const targetLayer = sourceConfig.targetLayer || 'entity';
41
+ if (roots.length === 1) {
42
+ result.push({ root: roots[0], targetLayer });
43
+ }
44
+ else {
45
+ const basenames = roots.map(r => basename(r));
46
+ const nameCount = new Map();
47
+ const nameIndices = new Map();
48
+ basenames.forEach(name => { nameCount.set(name, (nameCount.get(name) || 0) + 1); });
49
+ roots.forEach((root, index) => {
50
+ const name = basenames[index];
51
+ const count = nameCount.get(name) || 1;
52
+ if (count > 1) {
53
+ const currentIndex = nameIndices.get(name) || 0;
54
+ nameIndices.set(name, currentIndex + 1);
55
+ const resolvedName = currentIndex === 0 ? name : `${name}${currentIndex}`;
56
+ result.push({ root, targetLayer, resolvedName });
57
+ }
58
+ else {
59
+ result.push({ root, targetLayer });
60
+ }
61
+ });
62
+ }
63
+ }
64
+ return result;
65
+ }
@@ -1,7 +1,26 @@
1
1
  import { EntityTokenValue, FsdLayerValue } from './constants.js';
2
2
  export interface PresetSourceItem {
3
+ /**
4
+ * Root directory or array of root directories to analyze.
5
+ * If array is provided and folder names conflict, indices will be appended.
6
+ * Example: ['src/entities/User', 'src/features/User'] -> User, User1
7
+ */
8
+ root: string | string[];
9
+ targetLayer: FsdLayerValue;
10
+ /**
11
+ * Resolved name for this source (used when multiple roots have conflicting names)
12
+ * If not set, basename of root is used
13
+ */
14
+ resolvedName?: string;
15
+ }
16
+ /**
17
+ * Normalized version of PresetSourceItem where root is always a string.
18
+ * This is returned by normalizeLayers() after expanding array roots.
19
+ */
20
+ export interface NormalizedPresetSourceItem {
3
21
  root: string;
4
22
  targetLayer: FsdLayerValue;
23
+ resolvedName?: string;
5
24
  }
6
25
  export interface PresetSourceConfig {
7
26
  /**
@@ -11,10 +30,13 @@ export interface PresetSourceConfig {
11
30
  globalRoot?: string;
12
31
  mode?: 'short' | 'ejected';
13
32
  /**
14
- * The root path of the reference code (etalon)
33
+ * The root path of the reference code (etalon).
34
+ * Can be a single path or array of paths.
35
+ * If array is provided and folder names conflict, indices will be appended.
36
+ * Example: ['src/entities/User', 'src/features/User'] -> User, User1
15
37
  * @deprecated Use layers for multiple sources or simpler specific config
16
38
  */
17
- root?: string;
39
+ root?: string | string[];
18
40
  /**
19
41
  * The target layer for the preset (default: 'entity')
20
42
  */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/lib/reverse/types.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEjE,MAAM,WAAW,gBAAgB;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,aAAa,CAAC;CAC9B;AAED,MAAM,WAAW,kBAAkB;IAC/B;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAE3B;;;OAGG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;OAEG;IACH,WAAW,CAAC,EAAE,aAAa,CAAC;IAE5B;;OAEG;IACH,MAAM,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAE5B;;OAEG;IACH,OAAO,CAAC,EAAE;QACN,QAAQ,CAAC,EAAE,YAAY,GAAG,YAAY,CAAC;QACvC;;WAEG;QACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;KACrB,CAAC;CACL;AAED;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACjC,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,GAAG,MAAM,CAAC;CACjD;AAED,MAAM,WAAW,gBAAgB;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,aAAa,GAAG,MAAM,CAAC;IACpC,MAAM,EAAE,oBAAoB,CAAC;CAEhC;AAED,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,gBAAgB,EAAE,CAAC;CAC7B"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/lib/reverse/types.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,gBAAgB,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAEjE,MAAM,WAAW,gBAAgB;IAC7B;;;;OAIG;IACH,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IACxB,WAAW,EAAE,aAAa,CAAC;IAC3B;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAED;;;GAGG;AACH,MAAM,WAAW,0BAA0B;IACvC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,aAAa,CAAC;IAC3B,YAAY,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,kBAAkB;IAC/B;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAE3B;;;;;;OAMG;IACH,IAAI,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAEzB;;OAEG;IACH,WAAW,CAAC,EAAE,aAAa,CAAC;IAE5B;;OAEG;IACH,MAAM,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAE5B;;OAEG;IACH,OAAO,CAAC,EAAE;QACN,QAAQ,CAAC,EAAE,YAAY,GAAG,YAAY,CAAC;QACvC;;WAEG;QACH,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;KACrB,CAAC;CACL;AAED;;;GAGG;AACH,MAAM,WAAW,oBAAoB;IACjC,CAAC,QAAQ,EAAE,MAAM,GAAG,gBAAgB,GAAG,MAAM,CAAC;CACjD;AAED,MAAM,WAAW,gBAAgB;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,aAAa,GAAG,MAAM,CAAC;IACpC,MAAM,EAAE,oBAAoB,CAAC;CAEhC;AAED,MAAM,WAAW,YAAY;IACzB,KAAK,EAAE,gBAAgB,EAAE,CAAC;CAC7B"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starodubenko/fsd-gen",
3
- "version": "1.2.1-0",
3
+ "version": "1.4.0-0",
4
4
  "description": "A powerful CLI tool for scaffolding Feature-Sliced Design (FSD) components, slices, and layers.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",