@mandujs/core 0.9.31 → 0.9.37
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/package.json +3 -1
- package/src/brain/architecture/analyzer.ts +15 -7
- package/src/bundler/dev.ts +35 -27
- package/src/guard/analyzer.ts +350 -0
- package/src/guard/ast-analyzer.ts +806 -0
- package/src/guard/index.ts +174 -0
- package/src/guard/presets/atomic.ts +70 -0
- package/src/guard/presets/clean.ts +77 -0
- package/src/guard/presets/fsd.ts +79 -0
- package/src/guard/presets/hexagonal.ts +68 -0
- package/src/guard/presets/index.ts +155 -0
- package/src/guard/reporter.ts +445 -0
- package/src/guard/statistics.ts +572 -0
- package/src/guard/suggestions.ts +345 -0
- package/src/guard/types.ts +331 -0
- package/src/guard/validator.ts +683 -0
- package/src/guard/watcher.ts +376 -0
- package/src/index.ts +1 -0
- package/src/router/fs-patterns.ts +380 -0
- package/src/router/fs-routes.ts +389 -0
- package/src/router/fs-scanner.ts +513 -0
- package/src/router/fs-types.ts +278 -0
- package/src/router/index.ts +81 -0
- package/src/runtime/boundary.tsx +232 -0
- package/src/runtime/index.ts +1 -0
- package/src/runtime/router.test.ts +53 -0
- package/src/runtime/router.ts +143 -46
- package/src/runtime/server.ts +233 -2
- package/src/spec/schema.ts +11 -0
- package/src/watcher/rules.ts +4 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mandujs/core",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.37",
|
|
4
4
|
"description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.ts",
|
|
@@ -49,6 +49,8 @@
|
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"chokidar": "^5.0.0",
|
|
52
|
+
"glob": "^13.0.0",
|
|
53
|
+
"minimatch": "^10.1.1",
|
|
52
54
|
"ollama": "^0.6.3"
|
|
53
55
|
}
|
|
54
56
|
}
|
|
@@ -173,9 +173,9 @@ export class ArchitectureAnalyzer {
|
|
|
173
173
|
/**
|
|
174
174
|
* 파일 위치 검증
|
|
175
175
|
*/
|
|
176
|
-
async checkLocation(request: CheckLocationRequest): Promise<CheckLocationResult> {
|
|
177
|
-
const violations: ArchitectureViolation[] = [];
|
|
178
|
-
const normalizedPath = request.path
|
|
176
|
+
async checkLocation(request: CheckLocationRequest): Promise<CheckLocationResult> {
|
|
177
|
+
const violations: ArchitectureViolation[] = [];
|
|
178
|
+
const normalizedPath = this.toRelativePath(request.path);
|
|
179
179
|
|
|
180
180
|
// 1. readonly 폴더 검사
|
|
181
181
|
for (const [key, rule] of Object.entries(this.config.folders || {})) {
|
|
@@ -288,14 +288,14 @@ export class ArchitectureAnalyzer {
|
|
|
288
288
|
/**
|
|
289
289
|
* Import 검증
|
|
290
290
|
*/
|
|
291
|
-
async checkImports(request: CheckImportRequest): Promise<CheckImportResult> {
|
|
291
|
+
async checkImports(request: CheckImportRequest): Promise<CheckImportResult> {
|
|
292
292
|
const violations: Array<{
|
|
293
293
|
import: string;
|
|
294
294
|
reason: string;
|
|
295
295
|
suggestion?: string;
|
|
296
296
|
}> = [];
|
|
297
297
|
|
|
298
|
-
const normalizedSource = request.sourceFile
|
|
298
|
+
const normalizedSource = this.toRelativePath(request.sourceFile);
|
|
299
299
|
|
|
300
300
|
for (const importPath of request.imports) {
|
|
301
301
|
for (const rule of this.config.imports || []) {
|
|
@@ -368,7 +368,7 @@ export class ArchitectureAnalyzer {
|
|
|
368
368
|
/**
|
|
369
369
|
* 코드에서 import 문 추출
|
|
370
370
|
*/
|
|
371
|
-
private extractImports(content: string): string[] {
|
|
371
|
+
private extractImports(content: string): string[] {
|
|
372
372
|
const imports: string[] = [];
|
|
373
373
|
|
|
374
374
|
// ES6 import
|
|
@@ -385,7 +385,15 @@ export class ArchitectureAnalyzer {
|
|
|
385
385
|
}
|
|
386
386
|
|
|
387
387
|
return imports;
|
|
388
|
-
}
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
private toRelativePath(filePath: string): string {
|
|
391
|
+
const normalized = filePath.replace(/\\/g, "/");
|
|
392
|
+
if (path.isAbsolute(normalized)) {
|
|
393
|
+
return path.relative(this.rootDir, normalized).replace(/\\/g, "/");
|
|
394
|
+
}
|
|
395
|
+
return normalized;
|
|
396
|
+
}
|
|
389
397
|
|
|
390
398
|
/**
|
|
391
399
|
* 폴더 스캔
|
package/src/bundler/dev.ts
CHANGED
|
@@ -18,12 +18,12 @@ export interface DevBundlerOptions {
|
|
|
18
18
|
onRebuild?: (result: RebuildResult) => void;
|
|
19
19
|
/** 에러 콜백 */
|
|
20
20
|
onError?: (error: Error, routeId?: string) => void;
|
|
21
|
-
/**
|
|
22
|
-
* 추가 watch 디렉토리 (공통 컴포넌트 등)
|
|
23
|
-
* 상대 경로 또는 절대 경로 모두 지원
|
|
24
|
-
* 기본값: ["src/components", "components", "src/shared", "shared", "src/lib", "lib", "src/hooks", "hooks", "src/utils", "utils"]
|
|
25
|
-
*/
|
|
26
|
-
watchDirs?: string[];
|
|
21
|
+
/**
|
|
22
|
+
* 추가 watch 디렉토리 (공통 컴포넌트 등)
|
|
23
|
+
* 상대 경로 또는 절대 경로 모두 지원
|
|
24
|
+
* 기본값: ["src/components", "components", "src/shared", "shared", "src/lib", "lib", "src/hooks", "hooks", "src/utils", "utils"]
|
|
25
|
+
*/
|
|
26
|
+
watchDirs?: string[];
|
|
27
27
|
/**
|
|
28
28
|
* 기본 watch 디렉토리 비활성화
|
|
29
29
|
* true로 설정하면 watchDirs만 감시
|
|
@@ -113,26 +113,26 @@ export async function startDevBundler(options: DevBundlerOptions): Promise<DevBu
|
|
|
113
113
|
}
|
|
114
114
|
|
|
115
115
|
// 공통 컴포넌트 디렉토리 추가 (기본 + 커스텀)
|
|
116
|
-
const commonDirsToCheck = disableDefaultWatchDirs
|
|
117
|
-
? customWatchDirs
|
|
118
|
-
: [...DEFAULT_COMMON_DIRS, ...customWatchDirs];
|
|
119
|
-
|
|
120
|
-
const addCommonDir = async (dir: string): Promise<void> => {
|
|
121
|
-
const absPath = path.isAbsolute(dir) ? dir : path.join(rootDir, dir);
|
|
122
|
-
try {
|
|
123
|
-
const stat = await fs.promises.stat(absPath);
|
|
124
|
-
const watchPath = stat.isDirectory() ? absPath : path.dirname(absPath);
|
|
125
|
-
await fs.promises.access(watchPath);
|
|
126
|
-
commonWatchDirs.add(watchPath);
|
|
127
|
-
watchDirs.add(watchPath);
|
|
128
|
-
} catch {
|
|
129
|
-
// 디렉토리 없으면 무시
|
|
130
|
-
}
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
for (const dir of commonDirsToCheck) {
|
|
134
|
-
await addCommonDir(dir);
|
|
135
|
-
}
|
|
116
|
+
const commonDirsToCheck = disableDefaultWatchDirs
|
|
117
|
+
? customWatchDirs
|
|
118
|
+
: [...DEFAULT_COMMON_DIRS, ...customWatchDirs];
|
|
119
|
+
|
|
120
|
+
const addCommonDir = async (dir: string): Promise<void> => {
|
|
121
|
+
const absPath = path.isAbsolute(dir) ? dir : path.join(rootDir, dir);
|
|
122
|
+
try {
|
|
123
|
+
const stat = await fs.promises.stat(absPath);
|
|
124
|
+
const watchPath = stat.isDirectory() ? absPath : path.dirname(absPath);
|
|
125
|
+
await fs.promises.access(watchPath);
|
|
126
|
+
commonWatchDirs.add(watchPath);
|
|
127
|
+
watchDirs.add(watchPath);
|
|
128
|
+
} catch {
|
|
129
|
+
// 디렉토리 없으면 무시
|
|
130
|
+
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
for (const dir of commonDirsToCheck) {
|
|
134
|
+
await addCommonDir(dir);
|
|
135
|
+
}
|
|
136
136
|
|
|
137
137
|
// 파일 감시 설정
|
|
138
138
|
const watchers: fs.FSWatcher[] = [];
|
|
@@ -312,9 +312,10 @@ export interface HMRServer {
|
|
|
312
312
|
}
|
|
313
313
|
|
|
314
314
|
export interface HMRMessage {
|
|
315
|
-
type: "connected" | "reload" | "island-update" | "error" | "ping";
|
|
315
|
+
type: "connected" | "reload" | "island-update" | "layout-update" | "error" | "ping";
|
|
316
316
|
data?: {
|
|
317
317
|
routeId?: string;
|
|
318
|
+
layoutPath?: string;
|
|
318
319
|
message?: string;
|
|
319
320
|
timestamp?: number;
|
|
320
321
|
};
|
|
@@ -482,6 +483,13 @@ export function generateHMRClientScript(port: number): string {
|
|
|
482
483
|
}
|
|
483
484
|
break;
|
|
484
485
|
|
|
486
|
+
case 'layout-update':
|
|
487
|
+
const layoutPath = message.data?.layoutPath;
|
|
488
|
+
console.log('[Mandu HMR] Layout updated:', layoutPath);
|
|
489
|
+
// Layout 변경은 항상 전체 리로드
|
|
490
|
+
location.reload();
|
|
491
|
+
break;
|
|
492
|
+
|
|
485
493
|
case 'error':
|
|
486
494
|
console.error('[Mandu HMR] Build error:', message.data?.message);
|
|
487
495
|
showErrorOverlay(message.data?.message);
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Guard Analyzer
|
|
3
|
+
*
|
|
4
|
+
* 파일 분석 및 Import 추출
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { readFile } from "fs/promises";
|
|
8
|
+
import { dirname, isAbsolute, relative, resolve } from "path";
|
|
9
|
+
import { minimatch } from "minimatch";
|
|
10
|
+
import type {
|
|
11
|
+
ImportInfo,
|
|
12
|
+
FileAnalysis,
|
|
13
|
+
LayerDefinition,
|
|
14
|
+
GuardConfig,
|
|
15
|
+
} from "./types";
|
|
16
|
+
import { WATCH_EXTENSIONS } from "./types";
|
|
17
|
+
|
|
18
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
19
|
+
// Import Extraction
|
|
20
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Static import 패턴
|
|
24
|
+
*
|
|
25
|
+
* Examples:
|
|
26
|
+
* - import { X } from 'module'
|
|
27
|
+
* - import X from 'module'
|
|
28
|
+
* - import * as X from 'module'
|
|
29
|
+
* - import 'module'
|
|
30
|
+
*/
|
|
31
|
+
const STATIC_IMPORT_PATTERN = /^import\s+(?:(?:(?:\{[^}]*\}|\*\s+as\s+\w+|\w+)(?:\s*,\s*(?:\{[^}]*\}|\*\s+as\s+\w+|\w+))*)\s+from\s+)?['"]([^'"]+)['"]/gm;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Dynamic import 패턴
|
|
35
|
+
*
|
|
36
|
+
* Examples:
|
|
37
|
+
* - import('module')
|
|
38
|
+
* - await import('module')
|
|
39
|
+
*/
|
|
40
|
+
const DYNAMIC_IMPORT_PATTERN = /(?:await\s+)?import\s*\(\s*['"]([^'"]+)['"]\s*\)/gm;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* CommonJS require 패턴
|
|
44
|
+
*
|
|
45
|
+
* Examples:
|
|
46
|
+
* - require('module')
|
|
47
|
+
* - const X = require('module')
|
|
48
|
+
*/
|
|
49
|
+
const REQUIRE_PATTERN = /require\s*\(\s*['"]([^'"]+)['"]\s*\)/gm;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Named import 추출 패턴
|
|
53
|
+
*/
|
|
54
|
+
const NAMED_IMPORT_PATTERN = /\{\s*([^}]+)\s*\}/;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Default import 추출 패턴
|
|
58
|
+
*/
|
|
59
|
+
const DEFAULT_IMPORT_PATTERN = /^import\s+(\w+)/;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* 파일에서 import 문 추출
|
|
63
|
+
*/
|
|
64
|
+
export function extractImports(content: string): ImportInfo[] {
|
|
65
|
+
const imports: ImportInfo[] = [];
|
|
66
|
+
const lines = content.split("\n");
|
|
67
|
+
|
|
68
|
+
// Static imports
|
|
69
|
+
let match: RegExpExecArray | null;
|
|
70
|
+
const staticPattern = new RegExp(STATIC_IMPORT_PATTERN.source, "gm");
|
|
71
|
+
|
|
72
|
+
while ((match = staticPattern.exec(content)) !== null) {
|
|
73
|
+
const statement = match[0];
|
|
74
|
+
const path = match[1];
|
|
75
|
+
const position = getLineAndColumn(content, match.index);
|
|
76
|
+
|
|
77
|
+
// Named imports 추출
|
|
78
|
+
const namedMatch = statement.match(NAMED_IMPORT_PATTERN);
|
|
79
|
+
const namedImports = namedMatch
|
|
80
|
+
? namedMatch[1].split(",").map((s) => s.trim().split(" as ")[0].trim())
|
|
81
|
+
: undefined;
|
|
82
|
+
|
|
83
|
+
// Default import 추출
|
|
84
|
+
const defaultMatch = statement.match(DEFAULT_IMPORT_PATTERN);
|
|
85
|
+
const defaultImport =
|
|
86
|
+
defaultMatch && !statement.includes("{") && !statement.includes("*")
|
|
87
|
+
? defaultMatch[1]
|
|
88
|
+
: undefined;
|
|
89
|
+
|
|
90
|
+
imports.push({
|
|
91
|
+
statement,
|
|
92
|
+
path,
|
|
93
|
+
line: position.line,
|
|
94
|
+
column: position.column,
|
|
95
|
+
type: "static",
|
|
96
|
+
namedImports,
|
|
97
|
+
defaultImport,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Dynamic imports
|
|
102
|
+
const dynamicPattern = new RegExp(DYNAMIC_IMPORT_PATTERN.source, "gm");
|
|
103
|
+
|
|
104
|
+
while ((match = dynamicPattern.exec(content)) !== null) {
|
|
105
|
+
const position = getLineAndColumn(content, match.index);
|
|
106
|
+
|
|
107
|
+
imports.push({
|
|
108
|
+
statement: match[0],
|
|
109
|
+
path: match[1],
|
|
110
|
+
line: position.line,
|
|
111
|
+
column: position.column,
|
|
112
|
+
type: "dynamic",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// CommonJS requires
|
|
117
|
+
const requirePattern = new RegExp(REQUIRE_PATTERN.source, "gm");
|
|
118
|
+
|
|
119
|
+
while ((match = requirePattern.exec(content)) !== null) {
|
|
120
|
+
const position = getLineAndColumn(content, match.index);
|
|
121
|
+
|
|
122
|
+
imports.push({
|
|
123
|
+
statement: match[0],
|
|
124
|
+
path: match[1],
|
|
125
|
+
line: position.line,
|
|
126
|
+
column: position.column,
|
|
127
|
+
type: "require",
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return imports;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* 인덱스에서 라인과 컬럼 위치 계산
|
|
136
|
+
*/
|
|
137
|
+
function getLineAndColumn(content: string, index: number): { line: number; column: number } {
|
|
138
|
+
const lines = content.slice(0, index).split("\n");
|
|
139
|
+
return {
|
|
140
|
+
line: lines.length,
|
|
141
|
+
column: lines[lines.length - 1].length + 1,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
146
|
+
// Layer Resolution
|
|
147
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* 파일 경로에서 레이어 결정
|
|
151
|
+
*/
|
|
152
|
+
export function resolveFileLayer(
|
|
153
|
+
filePath: string,
|
|
154
|
+
layers: LayerDefinition[],
|
|
155
|
+
rootDir: string
|
|
156
|
+
): string | null {
|
|
157
|
+
const relativePath = relative(rootDir, filePath).replace(/\\/g, "/");
|
|
158
|
+
|
|
159
|
+
for (const layer of layers) {
|
|
160
|
+
if (minimatch(relativePath, layer.pattern)) {
|
|
161
|
+
return layer.name;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Import 경로에서 타겟 레이어 결정
|
|
170
|
+
*/
|
|
171
|
+
export function resolveImportLayer(
|
|
172
|
+
importPath: string,
|
|
173
|
+
layers: LayerDefinition[],
|
|
174
|
+
srcDir: string,
|
|
175
|
+
fromFile?: string,
|
|
176
|
+
rootDir?: string
|
|
177
|
+
): string | null {
|
|
178
|
+
const normalizedImportPath = importPath.replace(/\\/g, "/");
|
|
179
|
+
const normalizedSrcDir = srcDir.replace(/\\/g, "/").replace(/\/$/, "");
|
|
180
|
+
|
|
181
|
+
const isAlias = normalizedImportPath.startsWith("@/") || normalizedImportPath.startsWith("~/");
|
|
182
|
+
const isRelative = normalizedImportPath.startsWith(".");
|
|
183
|
+
const isSrcAbsolute = normalizedSrcDir.length > 0 && normalizedSrcDir !== "." &&
|
|
184
|
+
(normalizedImportPath === normalizedSrcDir || normalizedImportPath.startsWith(`${normalizedSrcDir}/`));
|
|
185
|
+
|
|
186
|
+
// 상대/alias/src 경로가 아닌 경우 (node_modules 등)
|
|
187
|
+
if (!isAlias && !isRelative && !isSrcAbsolute) {
|
|
188
|
+
return null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const candidates: string[] = [];
|
|
192
|
+
|
|
193
|
+
if (isAlias) {
|
|
194
|
+
const aliasPath = normalizedImportPath.slice(2);
|
|
195
|
+
const withSrc = normalizedSrcDir.length > 0 ? `${normalizedSrcDir}/${aliasPath}` : aliasPath;
|
|
196
|
+
candidates.push(withSrc, aliasPath);
|
|
197
|
+
} else if (isSrcAbsolute) {
|
|
198
|
+
const trimmed = normalizedSrcDir.length > 0 && normalizedImportPath.startsWith(`${normalizedSrcDir}/`)
|
|
199
|
+
? normalizedImportPath.slice(normalizedSrcDir.length + 1)
|
|
200
|
+
: normalizedImportPath;
|
|
201
|
+
candidates.push(normalizedImportPath, trimmed);
|
|
202
|
+
} else if (isRelative) {
|
|
203
|
+
if (!fromFile || !rootDir) {
|
|
204
|
+
return null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const absoluteFromFile = isAbsolute(fromFile) ? fromFile : resolve(rootDir, fromFile);
|
|
208
|
+
const resolvedPath = resolve(dirname(absoluteFromFile), normalizedImportPath);
|
|
209
|
+
const relativeToRoot = relative(rootDir, resolvedPath).replace(/\\/g, "/");
|
|
210
|
+
|
|
211
|
+
// 루트 밖이면 무시
|
|
212
|
+
if (relativeToRoot.startsWith("..") || relativeToRoot.startsWith("../")) {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
candidates.push(relativeToRoot);
|
|
217
|
+
|
|
218
|
+
if (normalizedSrcDir.length > 0 && relativeToRoot.startsWith(`${normalizedSrcDir}/`)) {
|
|
219
|
+
candidates.push(relativeToRoot.slice(normalizedSrcDir.length + 1));
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
for (const candidate of candidates) {
|
|
224
|
+
const normalizedCandidate = candidate.replace(/\\/g, "/");
|
|
225
|
+
|
|
226
|
+
for (const layer of layers) {
|
|
227
|
+
if (minimatch(normalizedCandidate, layer.pattern)) {
|
|
228
|
+
return layer.name;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* FSD 슬라이스 이름 추출
|
|
238
|
+
*/
|
|
239
|
+
export function extractSlice(filePath: string, layer: string): string | undefined {
|
|
240
|
+
const relativePath = filePath.replace(/\\/g, "/");
|
|
241
|
+
|
|
242
|
+
// layer/slice/... 형식에서 slice 추출
|
|
243
|
+
const pattern = new RegExp(`${layer}/([^/]+)`);
|
|
244
|
+
const match = relativePath.match(pattern);
|
|
245
|
+
|
|
246
|
+
return match?.[1];
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
250
|
+
// File Analysis
|
|
251
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* 파일 분석
|
|
255
|
+
*/
|
|
256
|
+
export async function analyzeFile(
|
|
257
|
+
filePath: string,
|
|
258
|
+
layers: LayerDefinition[],
|
|
259
|
+
rootDir: string
|
|
260
|
+
): Promise<FileAnalysis> {
|
|
261
|
+
const content = await readFile(filePath, "utf-8");
|
|
262
|
+
const imports = extractImports(content);
|
|
263
|
+
const layer = resolveFileLayer(filePath, layers, rootDir);
|
|
264
|
+
const slice = layer ? extractSlice(filePath, layer) : undefined;
|
|
265
|
+
|
|
266
|
+
return {
|
|
267
|
+
filePath,
|
|
268
|
+
rootDir,
|
|
269
|
+
layer,
|
|
270
|
+
slice,
|
|
271
|
+
imports,
|
|
272
|
+
analyzedAt: Date.now(),
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* 파일이 분석 대상인지 확인
|
|
278
|
+
*/
|
|
279
|
+
export function shouldAnalyzeFile(
|
|
280
|
+
filePath: string,
|
|
281
|
+
config: GuardConfig,
|
|
282
|
+
rootDir?: string
|
|
283
|
+
): boolean {
|
|
284
|
+
const ext = filePath.slice(filePath.lastIndexOf("."));
|
|
285
|
+
|
|
286
|
+
// 확장자 체크
|
|
287
|
+
if (!WATCH_EXTENSIONS.includes(ext)) {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 제외 패턴 체크
|
|
292
|
+
if (config.exclude) {
|
|
293
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
294
|
+
const candidates = new Set<string>([normalizedPath]);
|
|
295
|
+
|
|
296
|
+
if (rootDir) {
|
|
297
|
+
const relativeToRoot = relative(rootDir, filePath).replace(/\\/g, "/");
|
|
298
|
+
candidates.add(relativeToRoot);
|
|
299
|
+
|
|
300
|
+
const srcDir = (config.srcDir ?? "src").replace(/\\/g, "/").replace(/\/$/, "");
|
|
301
|
+
if (srcDir && relativeToRoot.startsWith(`${srcDir}/`)) {
|
|
302
|
+
candidates.add(relativeToRoot.slice(srcDir.length + 1));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
for (const pattern of config.exclude) {
|
|
307
|
+
for (const candidate of candidates) {
|
|
308
|
+
if (minimatch(candidate, pattern)) {
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Import가 무시 대상인지 확인
|
|
320
|
+
*/
|
|
321
|
+
export function shouldIgnoreImport(
|
|
322
|
+
importPath: string,
|
|
323
|
+
config: GuardConfig
|
|
324
|
+
): boolean {
|
|
325
|
+
const normalizedImportPath = importPath.replace(/\\/g, "/");
|
|
326
|
+
const srcDir = (config.srcDir ?? "src").replace(/\\/g, "/").replace(/\/$/, "");
|
|
327
|
+
const isSrcAbsolute = srcDir.length > 0 && srcDir !== "." &&
|
|
328
|
+
(normalizedImportPath === srcDir || normalizedImportPath.startsWith(`${srcDir}/`));
|
|
329
|
+
|
|
330
|
+
// 외부 모듈 (node_modules)
|
|
331
|
+
if (
|
|
332
|
+
!normalizedImportPath.startsWith(".") &&
|
|
333
|
+
!normalizedImportPath.startsWith("@/") &&
|
|
334
|
+
!normalizedImportPath.startsWith("~/") &&
|
|
335
|
+
!isSrcAbsolute
|
|
336
|
+
) {
|
|
337
|
+
return true;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// 무시 패턴 체크
|
|
341
|
+
if (config.ignoreImports) {
|
|
342
|
+
for (const pattern of config.ignoreImports) {
|
|
343
|
+
if (minimatch(normalizedImportPath, pattern)) {
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return false;
|
|
350
|
+
}
|