@starodubenko/fsd-gen 1.3.0-0 → 1.4.1-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
@@ -84,6 +84,11 @@ export default {
84
84
  - **Multi-Root Support**: Analyze multiple directories at once
85
85
  - **Automatic Conflict Resolution**: Numeric suffixes (User, User1, User2)
86
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.
87
92
  - **TypeScript Config**: Type-safe `preset.config.ts` with enums
88
93
 
89
94
 
@@ -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
@@ -25,5 +46,5 @@ export declare function identifyTokens(content: string, variations: ReturnType<t
25
46
  /**
26
47
  * Resolves the absolute source root path
27
48
  */
28
- export declare function resolveSourceRoot(presetDir: string, globalRoot?: string, layerRoot?: string): string;
49
+ export declare function resolveSourceRoot(presetDir: string, globalRoot?: string | string[], layerRoot?: string): string;
29
50
  //# sourceMappingURL=analyzeHelpers.d.ts.map
@@ -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,GAAG,MAAM,EAAE,EAAE,SAAS,GAAE,MAAW,GAAG,MAAM,CASnH"}
@@ -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
  /**
@@ -62,7 +152,10 @@ export function identifyTokens(content, variations) {
62
152
  export function resolveSourceRoot(presetDir, globalRoot, layerRoot = '') {
63
153
  let basePath = presetDir;
64
154
  if (globalRoot) {
65
- basePath = resolve(presetDir, globalRoot);
155
+ // Handle array if user accidentally passed one
156
+ const root = Array.isArray(globalRoot) ? String(globalRoot[0]) : String(globalRoot);
157
+ basePath = resolve(presetDir, root);
66
158
  }
67
- return resolve(basePath, layerRoot);
159
+ // layerRoot should already be normalized to string by normalizeLayers, but we cast just in case
160
+ return resolve(basePath, String(layerRoot));
68
161
  }
@@ -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,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"}
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,CAiE9F;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"}
@@ -47,18 +47,24 @@ export async function loadReverseEnvironment(presetDir) {
47
47
  */
48
48
  export function normalizeLayers(sourceConfig) {
49
49
  const result = [];
50
- if (sourceConfig.layers) {
50
+ // Helper to ensure we have a string root
51
+ const toS = (v) => String(v);
52
+ if (sourceConfig.layers && Array.isArray(sourceConfig.layers)) {
51
53
  for (const layer of sourceConfig.layers) {
52
- const roots = Array.isArray(layer.root) ? layer.root : [layer.root];
54
+ if (!layer || !layer.root)
55
+ continue;
56
+ // Flatten deeply nested arrays explicitly
57
+ const roots = (Array.isArray(layer.root) ? layer.root : [layer.root]).flat(Infinity);
53
58
  if (roots.length === 1) {
54
- result.push({ root: roots[0], targetLayer: layer.targetLayer });
59
+ result.push({ root: toS(roots[0]), targetLayer: layer.targetLayer });
55
60
  }
56
- else {
57
- const basenames = roots.map(r => basename(r));
61
+ else if (roots.length > 1) {
62
+ const basenames = roots.map(r => basename(toS(r)));
58
63
  const nameCount = new Map();
59
64
  const nameIndices = new Map();
60
65
  basenames.forEach(name => { nameCount.set(name, (nameCount.get(name) || 0) + 1); });
61
- roots.forEach((root, index) => {
66
+ roots.forEach((rawRoot, index) => {
67
+ const root = toS(rawRoot);
62
68
  const name = basenames[index];
63
69
  const count = nameCount.get(name) || 1;
64
70
  if (count > 1) {
@@ -75,17 +81,19 @@ export function normalizeLayers(sourceConfig) {
75
81
  }
76
82
  }
77
83
  else if (sourceConfig.root) {
78
- const roots = Array.isArray(sourceConfig.root) ? sourceConfig.root : [sourceConfig.root];
84
+ // Flatten deeply nested arrays explicitly
85
+ const roots = (Array.isArray(sourceConfig.root) ? sourceConfig.root : [sourceConfig.root]).flat(Infinity);
79
86
  const targetLayer = sourceConfig.targetLayer || 'entity';
80
87
  if (roots.length === 1) {
81
- result.push({ root: roots[0], targetLayer });
88
+ result.push({ root: toS(roots[0]), targetLayer });
82
89
  }
83
- else {
84
- const basenames = roots.map(r => basename(r));
90
+ else if (roots.length > 1) {
91
+ const basenames = roots.map(r => basename(toS(r)));
85
92
  const nameCount = new Map();
86
93
  const nameIndices = new Map();
87
94
  basenames.forEach(name => { nameCount.set(name, (nameCount.get(name) || 0) + 1); });
88
- roots.forEach((root, index) => {
95
+ roots.forEach((rawRoot, index) => {
96
+ const root = toS(rawRoot);
89
97
  const name = basenames[index];
90
98
  const count = nameCount.get(name) || 1;
91
99
  if (count > 1) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@starodubenko/fsd-gen",
3
- "version": "1.3.0-0",
3
+ "version": "1.4.1-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",