@mandujs/core 0.9.30 → 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 +21 -7
- 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
|
@@ -21,7 +21,7 @@ export interface DevBundlerOptions {
|
|
|
21
21
|
/**
|
|
22
22
|
* 추가 watch 디렉토리 (공통 컴포넌트 등)
|
|
23
23
|
* 상대 경로 또는 절대 경로 모두 지원
|
|
24
|
-
* 기본값: ["src/components", "components", "src/shared", "shared", "lib"]
|
|
24
|
+
* 기본값: ["src/components", "components", "src/shared", "shared", "src/lib", "lib", "src/hooks", "hooks", "src/utils", "utils"]
|
|
25
25
|
*/
|
|
26
26
|
watchDirs?: string[];
|
|
27
27
|
/**
|
|
@@ -117,15 +117,21 @@ export async function startDevBundler(options: DevBundlerOptions): Promise<DevBu
|
|
|
117
117
|
? customWatchDirs
|
|
118
118
|
: [...DEFAULT_COMMON_DIRS, ...customWatchDirs];
|
|
119
119
|
|
|
120
|
-
|
|
121
|
-
const
|
|
120
|
+
const addCommonDir = async (dir: string): Promise<void> => {
|
|
121
|
+
const absPath = path.isAbsolute(dir) ? dir : path.join(rootDir, dir);
|
|
122
122
|
try {
|
|
123
|
-
await fs.promises.
|
|
124
|
-
|
|
125
|
-
|
|
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);
|
|
126
128
|
} catch {
|
|
127
129
|
// 디렉토리 없으면 무시
|
|
128
130
|
}
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
for (const dir of commonDirsToCheck) {
|
|
134
|
+
await addCommonDir(dir);
|
|
129
135
|
}
|
|
130
136
|
|
|
131
137
|
// 파일 감시 설정
|
|
@@ -306,9 +312,10 @@ export interface HMRServer {
|
|
|
306
312
|
}
|
|
307
313
|
|
|
308
314
|
export interface HMRMessage {
|
|
309
|
-
type: "connected" | "reload" | "island-update" | "error" | "ping";
|
|
315
|
+
type: "connected" | "reload" | "island-update" | "layout-update" | "error" | "ping";
|
|
310
316
|
data?: {
|
|
311
317
|
routeId?: string;
|
|
318
|
+
layoutPath?: string;
|
|
312
319
|
message?: string;
|
|
313
320
|
timestamp?: number;
|
|
314
321
|
};
|
|
@@ -476,6 +483,13 @@ export function generateHMRClientScript(port: number): string {
|
|
|
476
483
|
}
|
|
477
484
|
break;
|
|
478
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
|
+
|
|
479
493
|
case 'error':
|
|
480
494
|
console.error('[Mandu HMR] Build error:', message.data?.message);
|
|
481
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
|
+
}
|