claudeos-core 1.6.2 → 1.7.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.
@@ -1,254 +1,264 @@
1
- /**
2
- * ClaudeOS-Core — Frontend Structure Scanner
3
- *
4
- * Scans frontend project directory structure to discover domains.
5
- * Supports: Angular, Next.js, React, Vue.
6
- * Includes FSD (Feature-Sliced Design), components scan, and 4-stage fallback.
7
- * Also provides frontend file count statistics.
8
- */
9
-
10
- const path = require("path");
11
- const { glob } = require("glob");
12
-
13
- async function scanFrontendDomains(stack, ROOT) {
14
- const frontendDomains = [];
15
-
16
- // ── Angular ──
17
- if (stack.frontend === "angular") {
18
- // Angular feature modules: src/app/*/ with *.component.ts or *.module.ts (+ monorepo apps/*/)
19
- const angularAppDirs = [
20
- ...await glob("{src/app,app}/*/", { cwd: ROOT }),
21
- ...await glob("{apps,packages}/*/src/app/*/", { cwd: ROOT, ignore: ["**/node_modules/**"] }),
22
- ];
23
- const skipAngularDirs = ["shared", "core", "common", "layout", "layouts", "environments", "assets", "styles", "testing", "utils"];
24
- for (const dir of angularAppDirs) {
25
- const name = path.basename(dir.replace(/\/$/, ""));
26
- if (skipAngularDirs.includes(name) || name.startsWith("_") || name.startsWith(".")) continue;
27
- const files = await glob(`${dir.replace(/\\/g, "/")}**/*.ts`, { cwd: ROOT, ignore: ["**/*.spec.ts", "**/*.test.ts"] });
28
- if (files.length > 0) {
29
- const components = files.filter(f => /\.component\.ts$/.test(f)).length;
30
- const services = files.filter(f => /\.service\.ts$/.test(f)).length;
31
- const modules = files.filter(f => /\.module\.ts$/.test(f)).length;
32
- const pipes = files.filter(f => /\.pipe\.ts$/.test(f)).length;
33
- const directives = files.filter(f => /\.directive\.ts$/.test(f)).length;
34
- const guards = files.filter(f => /\.guard\.ts$/.test(f)).length;
35
- frontendDomains.push({ name, type: "frontend", components, services, modules, pipes, directives, guards, totalFiles: files.length });
36
- }
37
- }
38
-
39
- // Angular fallback: scan **/modules/*/ and **/features/*/ with *.component.ts detection
40
- if (frontendDomains.length === 0) {
41
- const deepAngularDirs = await glob("**/{modules,features,pages,views}/*/", { cwd: ROOT, ignore: ["**/node_modules/**", "**/dist/**", "**/.angular/**"] });
42
- for (const dir of deepAngularDirs) {
43
- const name = path.basename(dir.replace(/\/$/, ""));
44
- if (skipAngularDirs.includes(name) || name.startsWith("_") || name.startsWith(".")) continue;
45
- const files = await glob(`${dir.replace(/\\/g, "/")}**/*.ts`, { cwd: ROOT, ignore: ["**/*.spec.ts", "**/*.test.ts"] });
46
- if (files.length >= 2) {
47
- const components = files.filter(f => /\.component\.ts$/.test(f)).length;
48
- const services = files.filter(f => /\.service\.ts$/.test(f)).length;
49
- frontendDomains.push({ name, type: "frontend", components, services, totalFiles: files.length });
50
- }
51
- }
52
- }
53
- }
54
-
55
- // ── Next.js/React/Vue ──
56
- if (stack.frontend === "nextjs" || stack.frontend === "react" || stack.frontend === "vue") {
57
- // App Router / Pages Router domains (standard + monorepo apps/*/)
58
- const allDirs = [
59
- ...await glob("{app,src/app}/*/", { cwd: ROOT }),
60
- ...await glob("{pages,src/pages}/*/", { cwd: ROOT }),
61
- ...await glob("{apps,packages}/*/app/*/", { cwd: ROOT, ignore: ["**/node_modules/**"] }),
62
- ...await glob("{apps,packages}/*/src/app/*/", { cwd: ROOT, ignore: ["**/node_modules/**"] }),
63
- ...await glob("{apps,packages}/*/pages/*/", { cwd: ROOT, ignore: ["**/node_modules/**"] }),
64
- ...await glob("{apps,packages}/*/src/pages/*/", { cwd: ROOT, ignore: ["**/node_modules/**"] }),
65
- ];
66
- const skipPages = ["api", "_app", "_document", "fonts", "not-found", "error", "loading"];
67
- for (const dir of allDirs) {
68
- const name = path.basename(dir);
69
- if (skipPages.includes(name) || name.startsWith("(") || name.startsWith("[") || name.startsWith("_") || name.startsWith(".")) continue;
70
- const files = await glob(`${dir.replace(/\\/g, "/")}**/*.{tsx,jsx,ts,js,vue}`, { cwd: ROOT });
71
- if (files.length > 0) {
72
- const pages = files.filter(f => /page\.|index\./.test(f)).length;
73
- const layouts = files.filter(f => /layout\./.test(f)).length;
74
- const clientFiles = files.filter(f => /client\./.test(f)).length;
75
- const serverFiles = pages + layouts;
76
- const components = files.filter(f => !/page\.|layout\.|index\.|client\./.test(f)).length;
77
- frontendDomains.push({
78
- name, type: "frontend", pages, layouts, clientFiles, serverFiles, components, totalFiles: files.length,
79
- rscPattern: clientFiles > 0 ? "RSC+Client split" : "default",
80
- });
81
- }
82
- }
83
-
84
- // FSD (Feature-Sliced Design): features/*, widgets/*, entities/*
85
- const fsdLayers = ["features", "widgets", "entities"];
86
- for (const layer of fsdLayers) {
87
- const fsdDirs = await glob(`{${layer},src/${layer}}/*/`, { cwd: ROOT });
88
- for (const dir of fsdDirs) {
89
- const name = path.basename(dir);
90
- if (["ui", "common", "shared", "lib", "config", "index"].includes(name)) continue;
91
- const files = await glob(`${dir.replace(/\\/g, "/")}**/*.{tsx,jsx,ts,js,vue}`, { cwd: ROOT, ignore: ["**/*.spec.*", "**/*.test.*", "**/*.stories.*"] });
92
- if (files.length > 0) {
93
- const uiFiles = files.filter(f => /\bui\b/.test(f)).length;
94
- const modelFiles = files.filter(f => /model|store|hook/.test(f)).length;
95
- frontendDomains.push({ name: `${layer}/${name}`, type: "frontend", components: uiFiles, models: modelFiles, totalFiles: files.length });
96
- }
97
- }
98
- }
99
-
100
- // components/* (existing)
101
- const compDirs = await glob("{src/,}components/*/", { cwd: ROOT });
102
- for (const dir of compDirs) {
103
- const name = path.basename(dir);
104
- if (["ui", "common", "shared", "layout", "icons"].includes(name)) continue;
105
- const files = await glob(`${dir.replace(/\\/g, "/")}**/*.{tsx,jsx,vue}`, { cwd: ROOT });
106
- if (files.length >= 2) {
107
- frontendDomains.push({ name: `comp-${name}`, type: "frontend", components: files.length, totalFiles: files.length });
108
- }
109
- }
110
-
111
- // ── Fallback: extract domains when primary scanners return 0 ──
112
- if (frontendDomains.length === 0) {
113
- // Fallback A: Next.js page.tsx / client.tsx based detection
114
- const pageFiles = await glob("**/page.{tsx,jsx}", { cwd: ROOT, ignore: ["**/node_modules/**", "**/.next/**"] });
115
- const domainSet = {};
116
- const skipNames = ["app", "src", "pages", "api", "_app", "_document"];
117
- for (const f of pageFiles) {
118
- const parts = f.replace(/\\/g, "/").split("/");
119
- const appIdx = parts.indexOf("app");
120
- const pagesIdx = parts.indexOf("pages");
121
- const baseIdx = appIdx >= 0 ? appIdx : pagesIdx;
122
- if (baseIdx >= 0 && baseIdx + 1 < parts.length - 1) {
123
- const domain = parts[baseIdx + 1];
124
- if (!skipNames.includes(domain) && !domain.startsWith("_") && !domain.startsWith("(") && !domain.startsWith("[") && !domain.startsWith(".")) {
125
- if (!domainSet[domain]) domainSet[domain] = { pages: 0, clientFiles: 0, totalFiles: 0 };
126
- domainSet[domain].pages++;
127
- domainSet[domain].totalFiles++;
128
- }
129
- }
130
- }
131
- const clientFiles = await glob("**/client.{tsx,jsx}", { cwd: ROOT, ignore: ["**/node_modules/**", "**/.next/**"] });
132
- for (const f of clientFiles) {
133
- const parts = f.replace(/\\/g, "/").split("/");
134
- const appIdx = parts.indexOf("app");
135
- const baseIdx = appIdx >= 0 ? appIdx : -1;
136
- if (baseIdx >= 0 && baseIdx + 1 < parts.length - 1) {
137
- const domain = parts[baseIdx + 1];
138
- if (domainSet[domain]) {
139
- domainSet[domain].clientFiles++;
140
- domainSet[domain].totalFiles++;
141
- }
142
- }
143
- }
144
- for (const [name, data] of Object.entries(domainSet)) {
145
- frontendDomains.push({
146
- name, type: "frontend", pages: data.pages, clientFiles: data.clientFiles, totalFiles: data.totalFiles,
147
- rscPattern: data.clientFiles > 0 ? "RSC+Client split" : "default",
148
- });
149
- }
150
-
151
- // Fallback B: FSD widgets/features/entities
152
- for (const layer of ["widgets", "features", "entities"]) {
153
- const layerFiles = await glob(`**/${layer}/*/**/*.{tsx,jsx,ts,js,vue}`, { cwd: ROOT, ignore: ["**/node_modules/**", "**/.next/**", "**/*.spec.*", "**/*.test.*"] });
154
- const layerDomains = {};
155
- for (const f of layerFiles) {
156
- const parts = f.replace(/\\/g, "/").split("/");
157
- const layerIdx = parts.indexOf(layer);
158
- if (layerIdx >= 0 && layerIdx + 1 < parts.length) {
159
- const domain = parts[layerIdx + 1];
160
- if (!["ui", "common", "shared", "lib", "config"].includes(domain)) {
161
- if (!layerDomains[domain]) layerDomains[domain] = 0;
162
- layerDomains[domain]++;
163
- }
164
- }
165
- }
166
- for (const [name, count] of Object.entries(layerDomains)) {
167
- frontendDomains.push({ name: `${layer}/${name}`, type: "frontend", totalFiles: count });
168
- }
169
- }
170
-
171
- // Fallback C: Deep components/**/components/*/ detection (React/CRA/Vite projects)
172
- if (frontendDomains.length === 0) {
173
- const deepCompDirs = await glob("**/components/*/", { cwd: ROOT, ignore: ["**/node_modules/**", "**/.next/**", "**/build/**", "**/dist/**", "**/.git/**", "**/vendor/**", "**/__pycache__/**", "**/coverage/**"] });
174
- const deepDomains = {};
175
- const skipDomainNames = ["ui", "common", "shared", "layout", "layouts", "icons", "assets", "config", "utils", "lib", "error", "footer", "header", "inputs", "template"];
176
- for (const dir of deepCompDirs) {
177
- const name = path.basename(dir.replace(/\/$/, ""));
178
- if (skipDomainNames.includes(name) || name.startsWith("_") || name.startsWith(".")) continue;
179
- const files = await glob(`${dir.replace(/\\/g, "/")}**/*.{tsx,jsx,ts,js,vue}`, { cwd: ROOT, ignore: ["**/*.spec.*", "**/*.test.*", "**/*.stories.*"] });
180
- if (files.length >= 2) {
181
- if (!deepDomains[name]) deepDomains[name] = { components: 0, totalFiles: 0 };
182
- deepDomains[name].components += files.length;
183
- deepDomains[name].totalFiles += files.length;
184
- }
185
- }
186
- for (const [name, data] of Object.entries(deepDomains)) {
187
- frontendDomains.push({ name, type: "frontend", components: data.components, totalFiles: data.totalFiles });
188
- }
189
- }
190
-
191
- // Fallback D: views/screens/containers/pages/routes deep detection
192
- if (frontendDomains.length === 0) {
193
- const domainDirPatterns = ["views", "screens", "containers", "pages", "routes", "modules", "domains"];
194
- const deepDirDomains = {};
195
- const skipDirNames = ["api", "auth", "_app", "_document", "index", "ui", "common", "shared",
196
- "layout", "layouts", "lib", "config", "utils", "assets", "hooks", "store", "types",
197
- "constants", "helpers", "services", "middleware", "interceptors", "guards", "decorators"];
198
-
199
- for (const pattern of domainDirPatterns) {
200
- const dirs = await glob(`**/${pattern}/*/`, { cwd: ROOT, ignore: ["**/node_modules/**", "**/.next/**", "**/build/**", "**/dist/**", "**/.git/**", "**/vendor/**", "**/__pycache__/**", "**/coverage/**"] });
201
- for (const dir of dirs) {
202
- const name = path.basename(dir.replace(/\/$/, ""));
203
- if (skipDirNames.includes(name) || name.startsWith("_") || name.startsWith(".") || name.startsWith("(") || name.startsWith("[")) continue;
204
- const files = await glob(`${dir.replace(/\\/g, "/")}**/*.{tsx,jsx,ts,js,vue}`, { cwd: ROOT, ignore: ["**/*.spec.*", "**/*.test.*", "**/*.stories.*"] });
205
- if (files.length >= 2) {
206
- if (!deepDirDomains[name]) deepDirDomains[name] = { components: 0, pages: 0, totalFiles: 0, sources: [] };
207
- const tsx = files.filter(f => /\.(tsx|jsx|vue)$/.test(f)).length;
208
- deepDirDomains[name].components += tsx;
209
- deepDirDomains[name].totalFiles += files.length;
210
- if (!deepDirDomains[name].sources.includes(pattern)) deepDirDomains[name].sources.push(pattern);
211
- }
212
- }
213
- }
214
- for (const [name, data] of Object.entries(deepDirDomains)) {
215
- frontendDomains.push({ name, type: "frontend", components: data.components, totalFiles: data.totalFiles, sources: data.sources });
216
- }
217
- }
218
- }
219
- }
220
-
221
- return { frontendDomains };
222
- }
223
-
224
- // ── Frontend file count statistics ──
225
- async function countFrontendStats(stack, ROOT) {
226
- const frontend = { exists: false, components: 0, pages: 0, hooks: 0 };
227
- if (stack.frontend) {
228
- frontend.exists = true;
229
- if (stack.frontend === "angular") {
230
- frontend.components = (await glob("{src/,}**/*.component.ts", { cwd: ROOT, ignore: ["**/node_modules/**", "**/dist/**"] })).length;
231
- frontend.pages = (await glob("{src/,}**/*.module.ts", { cwd: ROOT, ignore: ["**/node_modules/**", "**/dist/**"] })).length;
232
- frontend.hooks = (await glob("{src/,}**/*.service.ts", { cwd: ROOT, ignore: ["**/node_modules/**", "**/dist/**"] })).length;
233
- } else {
234
- frontend.components = (await glob("{src/,}**/components/**/*.{tsx,jsx,vue}", { cwd: ROOT, ignore: ["**/node_modules/**"] })).length;
235
- frontend.pages = (await glob("{src/,}{app,pages}/**/{page,index}.{tsx,jsx,vue}", { cwd: ROOT, ignore: ["**/node_modules/**"] })).length;
236
- frontend.hooks = (await glob("{src/,}**/hooks/**/*.{ts,js}", { cwd: ROOT, ignore: ["**/node_modules/**"] })).length;
237
- }
238
- }
239
-
240
- // App Router RSC/Client overall stats (for project-analysis.json)
241
- if (stack.frontend === "nextjs") {
242
- const allClientFiles = await glob("{app,src/app}/**/client.{tsx,ts,jsx,js}", { cwd: ROOT });
243
- const allPageFiles = await glob("{app,src/app}/**/page.{tsx,ts,jsx,js}", { cwd: ROOT });
244
- const allLayoutFiles = await glob("{app,src/app}/**/layout.{tsx,ts,jsx,js}", { cwd: ROOT });
245
- frontend.clientComponents = allClientFiles.length;
246
- frontend.serverPages = allPageFiles.length;
247
- frontend.layouts = allLayoutFiles.length;
248
- frontend.rscPattern = allClientFiles.length > 0;
249
- }
250
-
251
- return frontend;
252
- }
253
-
254
- module.exports = { scanFrontendDomains, countFrontendStats };
1
+ /**
2
+ * ClaudeOS-Core — Frontend Structure Scanner
3
+ *
4
+ * Scans frontend project directory structure to discover domains.
5
+ * Supports: Angular, Next.js, React, Vue.
6
+ * Includes FSD (Feature-Sliced Design), components scan, and 4-stage fallback.
7
+ * Also provides frontend file count statistics.
8
+ */
9
+
10
+ const path = require("path");
11
+ const { glob } = require("glob");
12
+
13
+ async function scanFrontendDomains(stack, ROOT) {
14
+ const frontendDomains = [];
15
+
16
+ // ── Angular ──
17
+ if (stack.frontend === "angular") {
18
+ // Angular feature modules: src/app/*/ with *.component.ts or *.module.ts (+ monorepo apps/*/)
19
+ const angularAppDirs = [
20
+ ...await glob("{src/app,app}/*/", { cwd: ROOT }),
21
+ ...await glob("{apps,packages}/*/src/app/*/", { cwd: ROOT, ignore: ["**/node_modules/**"] }),
22
+ ];
23
+ const skipAngularDirs = ["shared", "core", "common", "layout", "layouts", "environments", "assets", "styles", "testing", "utils"];
24
+ for (const dir of angularAppDirs) {
25
+ const name = path.basename(dir.replace(/\/$/, ""));
26
+ if (skipAngularDirs.includes(name) || name.startsWith("_") || name.startsWith(".")) continue;
27
+ const files = await glob(`${dir.replace(/\\/g, "/")}**/*.ts`, { cwd: ROOT, ignore: ["**/*.spec.ts", "**/*.test.ts"] });
28
+ if (files.length > 0) {
29
+ const components = files.filter(f => /\.component\.ts$/.test(f)).length;
30
+ const services = files.filter(f => /\.service\.ts$/.test(f)).length;
31
+ const modules = files.filter(f => /\.module\.ts$/.test(f)).length;
32
+ const pipes = files.filter(f => /\.pipe\.ts$/.test(f)).length;
33
+ const directives = files.filter(f => /\.directive\.ts$/.test(f)).length;
34
+ const guards = files.filter(f => /\.guard\.ts$/.test(f)).length;
35
+ frontendDomains.push({ name, type: "frontend", components, services, modules, pipes, directives, guards, totalFiles: files.length });
36
+ }
37
+ }
38
+
39
+ // Angular fallback: scan **/modules/*/ and **/features/*/ with *.component.ts detection
40
+ if (frontendDomains.length === 0) {
41
+ const deepAngularDirs = await glob("**/{modules,features,pages,views}/*/", { cwd: ROOT, ignore: ["**/node_modules/**", "**/dist/**", "**/.angular/**"] });
42
+ for (const dir of deepAngularDirs) {
43
+ const name = path.basename(dir.replace(/\/$/, ""));
44
+ if (skipAngularDirs.includes(name) || name.startsWith("_") || name.startsWith(".")) continue;
45
+ const files = await glob(`${dir.replace(/\\/g, "/")}**/*.ts`, { cwd: ROOT, ignore: ["**/*.spec.ts", "**/*.test.ts"] });
46
+ if (files.length >= 2) {
47
+ const components = files.filter(f => /\.component\.ts$/.test(f)).length;
48
+ const services = files.filter(f => /\.service\.ts$/.test(f)).length;
49
+ frontendDomains.push({ name, type: "frontend", components, services, totalFiles: files.length });
50
+ }
51
+ }
52
+ }
53
+ }
54
+
55
+ // ── Next.js/React/Vue ──
56
+ if (stack.frontend === "nextjs" || stack.frontend === "react" || stack.frontend === "vue") {
57
+ // App Router / Pages Router domains (standard + monorepo apps/*/ + nested src/*/)
58
+ const allDirs = [
59
+ ...await glob("{app,src/app}/*/", { cwd: ROOT }),
60
+ ...await glob("{pages,src/pages}/*/", { cwd: ROOT }),
61
+ ...await glob("{apps,packages}/*/app/*/", { cwd: ROOT, ignore: ["**/node_modules/**"] }),
62
+ ...await glob("{apps,packages}/*/src/app/*/", { cwd: ROOT, ignore: ["**/node_modules/**"] }),
63
+ ...await glob("{apps,packages}/*/pages/*/", { cwd: ROOT, ignore: ["**/node_modules/**"] }),
64
+ ...await glob("{apps,packages}/*/src/pages/*/", { cwd: ROOT, ignore: ["**/node_modules/**"] }),
65
+ // Non-standard nested page paths (e.g., src/admin/pages/*, src/dashboard/app/*)
66
+ ...await glob("src/*/{app,pages}/*/", { cwd: ROOT, ignore: ["**/node_modules/**"] }),
67
+ ];
68
+ const skipPages = ["api", "_app", "_document", "fonts", "not-found", "error", "loading"];
69
+ for (const dir of allDirs) {
70
+ const name = path.basename(dir);
71
+ if (skipPages.includes(name) || name.startsWith("(") || name.startsWith("[") || name.startsWith("_") || name.startsWith(".")) continue;
72
+ const files = await glob(`${dir.replace(/\\/g, "/")}**/*.{tsx,jsx,ts,js,vue}`, { cwd: ROOT });
73
+ if (files.length > 0) {
74
+ const pages = files.filter(f => /page\.|index\./.test(f)).length;
75
+ const layouts = files.filter(f => /layout\./.test(f)).length;
76
+ const clientFiles = files.filter(f => /client\./.test(f)).length;
77
+ const serverFiles = pages + layouts;
78
+ const components = files.filter(f => !/page\.|layout\.|index\.|client\./.test(f)).length;
79
+ frontendDomains.push({
80
+ name, type: "frontend", pages, layouts, clientFiles, serverFiles, components, totalFiles: files.length,
81
+ rscPattern: clientFiles > 0 ? "RSC+Client split" : "default",
82
+ });
83
+ }
84
+ }
85
+
86
+ // FSD (Feature-Sliced Design): features/*, widgets/*, entities/*
87
+ const fsdLayers = ["features", "widgets", "entities"];
88
+ for (const layer of fsdLayers) {
89
+ const fsdDirs = [...new Set([
90
+ ...await glob(`{${layer},src/${layer}}/*/`, { cwd: ROOT }),
91
+ ...await glob(`src/*/${layer}/*/`, { cwd: ROOT, ignore: ["**/node_modules/**"] }),
92
+ ])];
93
+ for (const dir of fsdDirs) {
94
+ const name = path.basename(dir);
95
+ if (["ui", "common", "shared", "lib", "config", "index"].includes(name)) continue;
96
+ const files = await glob(`${dir.replace(/\\/g, "/")}**/*.{tsx,jsx,ts,js,vue}`, { cwd: ROOT, ignore: ["**/*.spec.*", "**/*.test.*", "**/*.stories.*"] });
97
+ if (files.length > 0) {
98
+ const uiFiles = files.filter(f => /\bui\b/.test(f)).length;
99
+ const modelFiles = files.filter(f => /model|store|hook/.test(f)).length;
100
+ frontendDomains.push({ name: `${layer}/${name}`, type: "frontend", components: uiFiles, models: modelFiles, totalFiles: files.length });
101
+ }
102
+ }
103
+ }
104
+
105
+ // components/* (existing + nested src/*/components/*)
106
+ const compDirSet = new Set([
107
+ ...await glob("{src/,}components/*/", { cwd: ROOT }),
108
+ ...await glob("src/*/components/*/", { cwd: ROOT, ignore: ["**/node_modules/**"] }),
109
+ ]);
110
+ const compDirs = [...compDirSet];
111
+ for (const dir of compDirs) {
112
+ const name = path.basename(dir);
113
+ if (["ui", "common", "shared", "layout", "icons"].includes(name)) continue;
114
+ const files = await glob(`${dir.replace(/\\/g, "/")}**/*.{tsx,jsx,vue}`, { cwd: ROOT });
115
+ if (files.length >= 2) {
116
+ frontendDomains.push({ name: `comp-${name}`, type: "frontend", components: files.length, totalFiles: files.length });
117
+ }
118
+ }
119
+
120
+ // ── Fallback: extract domains when primary scanners return 0 ──
121
+ if (frontendDomains.length === 0) {
122
+ // Fallback A: Next.js page.tsx / client.tsx based detection
123
+ const pageFiles = await glob("**/page.{tsx,jsx}", { cwd: ROOT, ignore: ["**/node_modules/**", "**/.next/**"] });
124
+ const domainSet = {};
125
+ const skipNames = ["app", "src", "pages", "api", "_app", "_document"];
126
+ for (const f of pageFiles) {
127
+ const parts = f.replace(/\\/g, "/").split("/");
128
+ const appIdx = parts.indexOf("app");
129
+ const pagesIdx = parts.indexOf("pages");
130
+ const baseIdx = appIdx >= 0 ? appIdx : pagesIdx;
131
+ if (baseIdx >= 0 && baseIdx + 1 < parts.length - 1) {
132
+ const domain = parts[baseIdx + 1];
133
+ if (!skipNames.includes(domain) && !domain.startsWith("_") && !domain.startsWith("(") && !domain.startsWith("[") && !domain.startsWith(".")) {
134
+ if (!domainSet[domain]) domainSet[domain] = { pages: 0, clientFiles: 0, totalFiles: 0 };
135
+ domainSet[domain].pages++;
136
+ domainSet[domain].totalFiles++;
137
+ }
138
+ }
139
+ }
140
+ const clientFiles = await glob("**/client.{tsx,jsx}", { cwd: ROOT, ignore: ["**/node_modules/**", "**/.next/**"] });
141
+ for (const f of clientFiles) {
142
+ const parts = f.replace(/\\/g, "/").split("/");
143
+ const appIdx = parts.indexOf("app");
144
+ const baseIdx = appIdx >= 0 ? appIdx : -1;
145
+ if (baseIdx >= 0 && baseIdx + 1 < parts.length - 1) {
146
+ const domain = parts[baseIdx + 1];
147
+ if (domainSet[domain]) {
148
+ domainSet[domain].clientFiles++;
149
+ domainSet[domain].totalFiles++;
150
+ }
151
+ }
152
+ }
153
+ for (const [name, data] of Object.entries(domainSet)) {
154
+ frontendDomains.push({
155
+ name, type: "frontend", pages: data.pages, clientFiles: data.clientFiles, totalFiles: data.totalFiles,
156
+ rscPattern: data.clientFiles > 0 ? "RSC+Client split" : "default",
157
+ });
158
+ }
159
+
160
+ // Fallback B: FSD widgets/features/entities
161
+ for (const layer of ["widgets", "features", "entities"]) {
162
+ const layerFiles = await glob(`**/${layer}/*/**/*.{tsx,jsx,ts,js,vue}`, { cwd: ROOT, ignore: ["**/node_modules/**", "**/.next/**", "**/*.spec.*", "**/*.test.*"] });
163
+ const layerDomains = {};
164
+ for (const f of layerFiles) {
165
+ const parts = f.replace(/\\/g, "/").split("/");
166
+ const layerIdx = parts.indexOf(layer);
167
+ if (layerIdx >= 0 && layerIdx + 1 < parts.length) {
168
+ const domain = parts[layerIdx + 1];
169
+ if (!["ui", "common", "shared", "lib", "config"].includes(domain)) {
170
+ if (!layerDomains[domain]) layerDomains[domain] = 0;
171
+ layerDomains[domain]++;
172
+ }
173
+ }
174
+ }
175
+ for (const [name, count] of Object.entries(layerDomains)) {
176
+ frontendDomains.push({ name: `${layer}/${name}`, type: "frontend", totalFiles: count });
177
+ }
178
+ }
179
+
180
+ // Fallback C: Deep components/**/components/*/ detection (React/CRA/Vite projects)
181
+ if (frontendDomains.length === 0) {
182
+ const deepCompDirs = await glob("**/components/*/", { cwd: ROOT, ignore: ["**/node_modules/**", "**/.next/**", "**/build/**", "**/dist/**", "**/.git/**", "**/vendor/**", "**/__pycache__/**", "**/coverage/**"] });
183
+ const deepDomains = {};
184
+ const skipDomainNames = ["ui", "common", "shared", "layout", "layouts", "icons", "assets", "config", "utils", "lib", "error", "footer", "header", "inputs", "template"];
185
+ for (const dir of deepCompDirs) {
186
+ const name = path.basename(dir.replace(/\/$/, ""));
187
+ if (skipDomainNames.includes(name) || name.startsWith("_") || name.startsWith(".")) continue;
188
+ const files = await glob(`${dir.replace(/\\/g, "/")}**/*.{tsx,jsx,ts,js,vue}`, { cwd: ROOT, ignore: ["**/*.spec.*", "**/*.test.*", "**/*.stories.*"] });
189
+ if (files.length >= 2) {
190
+ if (!deepDomains[name]) deepDomains[name] = { components: 0, totalFiles: 0 };
191
+ deepDomains[name].components += files.length;
192
+ deepDomains[name].totalFiles += files.length;
193
+ }
194
+ }
195
+ for (const [name, data] of Object.entries(deepDomains)) {
196
+ frontendDomains.push({ name, type: "frontend", components: data.components, totalFiles: data.totalFiles });
197
+ }
198
+ }
199
+
200
+ // Fallback D: views/screens/containers/pages/routes deep detection
201
+ if (frontendDomains.length === 0) {
202
+ const domainDirPatterns = ["views", "screens", "containers", "pages", "routes", "modules", "domains"];
203
+ const deepDirDomains = {};
204
+ const skipDirNames = ["api", "auth", "_app", "_document", "index", "ui", "common", "shared",
205
+ "layout", "layouts", "lib", "config", "utils", "assets", "hooks", "store", "types",
206
+ "constants", "helpers", "services", "middleware", "interceptors", "guards", "decorators"];
207
+
208
+ for (const pattern of domainDirPatterns) {
209
+ const dirs = await glob(`**/${pattern}/*/`, { cwd: ROOT, ignore: ["**/node_modules/**", "**/.next/**", "**/build/**", "**/dist/**", "**/.git/**", "**/vendor/**", "**/__pycache__/**", "**/coverage/**"] });
210
+ for (const dir of dirs) {
211
+ const name = path.basename(dir.replace(/\/$/, ""));
212
+ if (skipDirNames.includes(name) || name.startsWith("_") || name.startsWith(".") || name.startsWith("(") || name.startsWith("[")) continue;
213
+ const files = await glob(`${dir.replace(/\\/g, "/")}**/*.{tsx,jsx,ts,js,vue}`, { cwd: ROOT, ignore: ["**/*.spec.*", "**/*.test.*", "**/*.stories.*"] });
214
+ if (files.length >= 2) {
215
+ if (!deepDirDomains[name]) deepDirDomains[name] = { components: 0, pages: 0, totalFiles: 0, sources: [] };
216
+ const tsx = files.filter(f => /\.(tsx|jsx|vue)$/.test(f)).length;
217
+ deepDirDomains[name].components += tsx;
218
+ deepDirDomains[name].totalFiles += files.length;
219
+ if (!deepDirDomains[name].sources.includes(pattern)) deepDirDomains[name].sources.push(pattern);
220
+ }
221
+ }
222
+ }
223
+ for (const [name, data] of Object.entries(deepDirDomains)) {
224
+ frontendDomains.push({ name, type: "frontend", components: data.components, totalFiles: data.totalFiles, sources: data.sources });
225
+ }
226
+ }
227
+ }
228
+ }
229
+
230
+ return { frontendDomains };
231
+ }
232
+
233
+ // ── Frontend file count statistics ──
234
+ async function countFrontendStats(stack, ROOT) {
235
+ const frontend = { exists: false, components: 0, pages: 0, hooks: 0 };
236
+ if (stack.frontend) {
237
+ frontend.exists = true;
238
+ if (stack.frontend === "angular") {
239
+ frontend.components = (await glob("{src/,}**/*.component.ts", { cwd: ROOT, ignore: ["**/node_modules/**", "**/dist/**"] })).length;
240
+ frontend.pages = (await glob("{src/,}**/*.module.ts", { cwd: ROOT, ignore: ["**/node_modules/**", "**/dist/**"] })).length;
241
+ frontend.hooks = (await glob("{src/,}**/*.service.ts", { cwd: ROOT, ignore: ["**/node_modules/**", "**/dist/**"] })).length;
242
+ } else {
243
+ frontend.components = (await glob("{src/,}**/components/**/*.{tsx,jsx,vue}", { cwd: ROOT, ignore: ["**/node_modules/**"] })).length;
244
+ frontend.pages = (await glob("{src/,}{app,pages}/**/{page,index}.{tsx,jsx,vue}", { cwd: ROOT, ignore: ["**/node_modules/**"] })).length
245
+ + (await glob("src/*/{app,pages}/**/{page,index}.{tsx,jsx,vue}", { cwd: ROOT, ignore: ["**/node_modules/**"] })).length;
246
+ frontend.hooks = (await glob("{src/,}**/hooks/**/*.{ts,js}", { cwd: ROOT, ignore: ["**/node_modules/**"] })).length;
247
+ }
248
+ }
249
+
250
+ // App Router RSC/Client overall stats (for project-analysis.json)
251
+ if (stack.frontend === "nextjs") {
252
+ const allClientFiles = await glob("{app,src/app}/**/client.{tsx,ts,jsx,js}", { cwd: ROOT });
253
+ const allPageFiles = await glob("{app,src/app}/**/page.{tsx,ts,jsx,js}", { cwd: ROOT });
254
+ const allLayoutFiles = await glob("{app,src/app}/**/layout.{tsx,ts,jsx,js}", { cwd: ROOT });
255
+ frontend.clientComponents = allClientFiles.length;
256
+ frontend.serverPages = allPageFiles.length;
257
+ frontend.layouts = allLayoutFiles.length;
258
+ frontend.rscPattern = allClientFiles.length > 0;
259
+ }
260
+
261
+ return frontend;
262
+ }
263
+
264
+ module.exports = { scanFrontendDomains, countFrontendStats };