@mandujs/core 0.13.0 → 0.13.2
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.ko.md +4 -4
- package/README.md +653 -653
- package/package.json +1 -1
- package/src/bundler/build.ts +91 -91
- package/src/bundler/css.ts +302 -302
- package/src/client/Link.tsx +227 -227
- package/src/client/globals.ts +44 -44
- package/src/client/hooks.ts +267 -267
- package/src/client/index.ts +5 -5
- package/src/client/island.ts +8 -8
- package/src/client/router.ts +435 -435
- package/src/client/runtime.ts +23 -23
- package/src/client/serialize.ts +404 -404
- package/src/client/window-state.ts +101 -101
- package/src/config/mandu.ts +9 -0
- package/src/config/validate.ts +12 -0
- package/src/config/watcher.ts +311 -311
- package/src/constants.ts +40 -40
- package/src/content/content-layer.ts +314 -314
- package/src/content/content.test.ts +433 -433
- package/src/content/data-store.ts +245 -245
- package/src/content/digest.ts +133 -133
- package/src/content/index.ts +164 -164
- package/src/content/loader-context.ts +172 -172
- package/src/content/loaders/api.ts +216 -216
- package/src/content/loaders/file.ts +169 -169
- package/src/content/loaders/glob.ts +252 -252
- package/src/content/loaders/index.ts +34 -34
- package/src/content/loaders/types.ts +137 -137
- package/src/content/meta-store.ts +209 -209
- package/src/content/types.ts +282 -282
- package/src/content/watcher.ts +135 -135
- package/src/contract/client-safe.test.ts +42 -42
- package/src/contract/client-safe.ts +114 -114
- package/src/contract/client.ts +16 -16
- package/src/contract/define.ts +459 -459
- package/src/contract/handler.ts +10 -10
- package/src/contract/normalize.test.ts +276 -276
- package/src/contract/normalize.ts +404 -404
- package/src/contract/registry.test.ts +206 -206
- package/src/contract/registry.ts +568 -568
- package/src/contract/schema.ts +48 -48
- package/src/contract/types.ts +58 -58
- package/src/contract/validator.ts +32 -32
- package/src/devtools/ai/context-builder.ts +375 -375
- package/src/devtools/ai/index.ts +25 -25
- package/src/devtools/ai/mcp-connector.ts +465 -465
- package/src/devtools/client/catchers/error-catcher.ts +327 -327
- package/src/devtools/client/catchers/index.ts +18 -18
- package/src/devtools/client/catchers/network-proxy.ts +363 -363
- package/src/devtools/client/components/index.ts +39 -39
- package/src/devtools/client/components/kitchen-root.tsx +362 -362
- package/src/devtools/client/components/mandu-character.tsx +241 -241
- package/src/devtools/client/components/overlay.tsx +368 -368
- package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
- package/src/devtools/client/components/panel/guard-panel.tsx +244 -244
- package/src/devtools/client/components/panel/index.ts +32 -32
- package/src/devtools/client/components/panel/islands-panel.tsx +304 -304
- package/src/devtools/client/components/panel/network-panel.tsx +292 -292
- package/src/devtools/client/components/panel/panel-container.tsx +259 -259
- package/src/devtools/client/filters/context-filters.ts +282 -282
- package/src/devtools/client/filters/index.ts +16 -16
- package/src/devtools/client/index.ts +63 -63
- package/src/devtools/client/persistence.ts +335 -335
- package/src/devtools/client/state-manager.ts +478 -478
- package/src/devtools/design-tokens.ts +263 -263
- package/src/devtools/hook/create-hook.ts +207 -207
- package/src/devtools/hook/index.ts +13 -13
- package/src/devtools/index.ts +439 -439
- package/src/devtools/init.ts +266 -266
- package/src/devtools/protocol.ts +237 -237
- package/src/devtools/server/index.ts +17 -17
- package/src/devtools/server/source-context.ts +444 -444
- package/src/devtools/types.ts +319 -319
- package/src/devtools/worker/index.ts +25 -25
- package/src/devtools/worker/redaction-worker.ts +222 -222
- package/src/devtools/worker/worker-manager.ts +409 -409
- package/src/error/domains.ts +265 -265
- package/src/error/result.ts +46 -46
- package/src/error/types.ts +6 -6
- package/src/errors/extractor.ts +409 -409
- package/src/errors/index.ts +19 -19
- package/src/filling/auth.ts +308 -308
- package/src/filling/context.ts +24 -1
- package/src/filling/deps.ts +238 -238
- package/src/filling/index.ts +4 -0
- package/src/filling/sse-catchup.test.ts +56 -0
- package/src/filling/sse-catchup.ts +67 -0
- package/src/filling/sse.test.ts +168 -0
- package/src/filling/sse.ts +162 -0
- package/src/generator/index.ts +3 -3
- package/src/guard/analyzer.ts +360 -360
- package/src/guard/ast-analyzer.ts +806 -806
- package/src/guard/contract-guard.ts +9 -9
- package/src/guard/file-type.test.ts +24 -24
- package/src/guard/presets/atomic.ts +70 -70
- package/src/guard/presets/clean.ts +77 -77
- package/src/guard/presets/fsd.ts +79 -79
- package/src/guard/presets/hexagonal.ts +68 -68
- package/src/guard/presets/index.ts +291 -291
- package/src/guard/reporter.ts +445 -445
- package/src/guard/rules.ts +12 -12
- package/src/guard/statistics.ts +578 -578
- package/src/guard/suggestions.ts +358 -358
- package/src/guard/types.ts +348 -348
- package/src/guard/validator.ts +834 -834
- package/src/guard/watcher.ts +404 -404
- package/src/index.ts +6 -1
- package/src/intent/index.ts +310 -310
- package/src/island/index.ts +304 -304
- package/src/logging/index.ts +22 -22
- package/src/logging/transports.ts +365 -365
- package/src/plugins/index.ts +38 -38
- package/src/plugins/registry.ts +377 -377
- package/src/plugins/types.ts +363 -363
- package/src/report/index.ts +1 -1
- package/src/router/fs-patterns.ts +387 -387
- package/src/router/fs-scanner.ts +497 -497
- package/src/runtime/boundary.tsx +232 -232
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/escape.ts +44 -0
- package/src/runtime/lifecycle.ts +381 -381
- package/src/runtime/logger.test.ts +345 -345
- package/src/runtime/logger.ts +677 -677
- package/src/runtime/router.test.ts +476 -476
- package/src/runtime/router.ts +105 -105
- package/src/runtime/security.ts +155 -155
- package/src/runtime/server.ts +257 -0
- package/src/runtime/session-key.ts +328 -328
- package/src/runtime/ssr.ts +16 -21
- package/src/runtime/streaming-ssr.ts +24 -33
- package/src/runtime/trace.ts +144 -144
- package/src/seo/index.ts +214 -214
- package/src/seo/integration/ssr.ts +307 -307
- package/src/seo/render/basic.ts +427 -427
- package/src/seo/render/index.ts +143 -143
- package/src/seo/render/jsonld.ts +539 -539
- package/src/seo/render/opengraph.ts +191 -191
- package/src/seo/render/robots.ts +116 -116
- package/src/seo/render/sitemap.ts +137 -137
- package/src/seo/render/twitter.ts +126 -126
- package/src/seo/resolve/index.ts +353 -353
- package/src/seo/resolve/opengraph.ts +143 -143
- package/src/seo/resolve/robots.ts +73 -73
- package/src/seo/resolve/title.ts +94 -94
- package/src/seo/resolve/twitter.ts +73 -73
- package/src/seo/resolve/url.ts +97 -97
- package/src/seo/routes/index.ts +290 -290
- package/src/seo/types.ts +575 -575
- package/src/slot/validator.ts +39 -39
- package/src/spec/index.ts +3 -3
- package/src/spec/load.ts +76 -76
- package/src/spec/lock.ts +56 -56
- package/src/utils/bun.ts +8 -8
- package/src/utils/lru-cache.ts +75 -75
- package/src/utils/safe-io.ts +188 -188
- package/src/utils/string-safe.ts +298 -298
package/src/guard/analyzer.ts
CHANGED
|
@@ -1,360 +1,360 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Mandu Guard Analyzer
|
|
3
|
-
*
|
|
4
|
-
* 파일 분석 및 Import 추출
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { safeReadFile } from "../utils/safe-io";
|
|
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
|
-
const marker = `${layer}/`;
|
|
243
|
-
const index = relativePath.indexOf(marker);
|
|
244
|
-
if (index === -1) {
|
|
245
|
-
return undefined;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
const rest = relativePath.slice(index + marker.length);
|
|
249
|
-
const slice = rest.split("/")[0];
|
|
250
|
-
|
|
251
|
-
return slice || undefined;
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
255
|
-
// File Analysis
|
|
256
|
-
// ═══════════════════════════════════════════════════════════════════════════
|
|
257
|
-
|
|
258
|
-
/**
|
|
259
|
-
* 파일 분석
|
|
260
|
-
*/
|
|
261
|
-
export async function analyzeFile(
|
|
262
|
-
filePath: string,
|
|
263
|
-
layers: LayerDefinition[],
|
|
264
|
-
rootDir: string
|
|
265
|
-
): Promise<FileAnalysis> {
|
|
266
|
-
const result = await safeReadFile(filePath);
|
|
267
|
-
if (!result.ok) {
|
|
268
|
-
throw new Error(`파일 분석 실패: ${filePath} - ${result.error.message}`);
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const content = result.value;
|
|
272
|
-
const imports = extractImports(content);
|
|
273
|
-
const layer = resolveFileLayer(filePath, layers, rootDir);
|
|
274
|
-
const slice = layer ? extractSlice(filePath, layer) : undefined;
|
|
275
|
-
|
|
276
|
-
return {
|
|
277
|
-
filePath,
|
|
278
|
-
rootDir,
|
|
279
|
-
layer,
|
|
280
|
-
slice,
|
|
281
|
-
imports,
|
|
282
|
-
analyzedAt: Date.now(),
|
|
283
|
-
};
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
/**
|
|
287
|
-
* 파일이 분석 대상인지 확인
|
|
288
|
-
*/
|
|
289
|
-
export function shouldAnalyzeFile(
|
|
290
|
-
filePath: string,
|
|
291
|
-
config: GuardConfig,
|
|
292
|
-
rootDir?: string
|
|
293
|
-
): boolean {
|
|
294
|
-
const ext = filePath.slice(filePath.lastIndexOf("."));
|
|
295
|
-
|
|
296
|
-
// 확장자 체크
|
|
297
|
-
if (!WATCH_EXTENSIONS.includes(ext)) {
|
|
298
|
-
return false;
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// 제외 패턴 체크
|
|
302
|
-
if (config.exclude) {
|
|
303
|
-
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
304
|
-
const candidates = new Set<string>([normalizedPath]);
|
|
305
|
-
|
|
306
|
-
if (rootDir) {
|
|
307
|
-
const relativeToRoot = relative(rootDir, filePath).replace(/\\/g, "/");
|
|
308
|
-
candidates.add(relativeToRoot);
|
|
309
|
-
|
|
310
|
-
const srcDir = (config.srcDir ?? "src").replace(/\\/g, "/").replace(/\/$/, "");
|
|
311
|
-
if (srcDir && relativeToRoot.startsWith(`${srcDir}/`)) {
|
|
312
|
-
candidates.add(relativeToRoot.slice(srcDir.length + 1));
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
for (const pattern of config.exclude) {
|
|
317
|
-
for (const candidate of candidates) {
|
|
318
|
-
if (minimatch(candidate, pattern)) {
|
|
319
|
-
return false;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
return true;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* Import가 무시 대상인지 확인
|
|
330
|
-
*/
|
|
331
|
-
export function shouldIgnoreImport(
|
|
332
|
-
importPath: string,
|
|
333
|
-
config: GuardConfig
|
|
334
|
-
): boolean {
|
|
335
|
-
const normalizedImportPath = importPath.replace(/\\/g, "/");
|
|
336
|
-
const srcDir = (config.srcDir ?? "src").replace(/\\/g, "/").replace(/\/$/, "");
|
|
337
|
-
const isSrcAbsolute = srcDir.length > 0 && srcDir !== "." &&
|
|
338
|
-
(normalizedImportPath === srcDir || normalizedImportPath.startsWith(`${srcDir}/`));
|
|
339
|
-
|
|
340
|
-
// 외부 모듈 (node_modules)
|
|
341
|
-
if (
|
|
342
|
-
!normalizedImportPath.startsWith(".") &&
|
|
343
|
-
!normalizedImportPath.startsWith("@/") &&
|
|
344
|
-
!normalizedImportPath.startsWith("~/") &&
|
|
345
|
-
!isSrcAbsolute
|
|
346
|
-
) {
|
|
347
|
-
return true;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
// 무시 패턴 체크
|
|
351
|
-
if (config.ignoreImports) {
|
|
352
|
-
for (const pattern of config.ignoreImports) {
|
|
353
|
-
if (minimatch(normalizedImportPath, pattern)) {
|
|
354
|
-
return true;
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
return false;
|
|
360
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Guard Analyzer
|
|
3
|
+
*
|
|
4
|
+
* 파일 분석 및 Import 추출
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { safeReadFile } from "../utils/safe-io";
|
|
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
|
+
const marker = `${layer}/`;
|
|
243
|
+
const index = relativePath.indexOf(marker);
|
|
244
|
+
if (index === -1) {
|
|
245
|
+
return undefined;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const rest = relativePath.slice(index + marker.length);
|
|
249
|
+
const slice = rest.split("/")[0];
|
|
250
|
+
|
|
251
|
+
return slice || undefined;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
255
|
+
// File Analysis
|
|
256
|
+
// ═══════════════════════════════════════════════════════════════════════════
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* 파일 분석
|
|
260
|
+
*/
|
|
261
|
+
export async function analyzeFile(
|
|
262
|
+
filePath: string,
|
|
263
|
+
layers: LayerDefinition[],
|
|
264
|
+
rootDir: string
|
|
265
|
+
): Promise<FileAnalysis> {
|
|
266
|
+
const result = await safeReadFile(filePath);
|
|
267
|
+
if (!result.ok) {
|
|
268
|
+
throw new Error(`파일 분석 실패: ${filePath} - ${result.error.message}`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const content = result.value;
|
|
272
|
+
const imports = extractImports(content);
|
|
273
|
+
const layer = resolveFileLayer(filePath, layers, rootDir);
|
|
274
|
+
const slice = layer ? extractSlice(filePath, layer) : undefined;
|
|
275
|
+
|
|
276
|
+
return {
|
|
277
|
+
filePath,
|
|
278
|
+
rootDir,
|
|
279
|
+
layer,
|
|
280
|
+
slice,
|
|
281
|
+
imports,
|
|
282
|
+
analyzedAt: Date.now(),
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* 파일이 분석 대상인지 확인
|
|
288
|
+
*/
|
|
289
|
+
export function shouldAnalyzeFile(
|
|
290
|
+
filePath: string,
|
|
291
|
+
config: GuardConfig,
|
|
292
|
+
rootDir?: string
|
|
293
|
+
): boolean {
|
|
294
|
+
const ext = filePath.slice(filePath.lastIndexOf("."));
|
|
295
|
+
|
|
296
|
+
// 확장자 체크
|
|
297
|
+
if (!WATCH_EXTENSIONS.includes(ext)) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// 제외 패턴 체크
|
|
302
|
+
if (config.exclude) {
|
|
303
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
304
|
+
const candidates = new Set<string>([normalizedPath]);
|
|
305
|
+
|
|
306
|
+
if (rootDir) {
|
|
307
|
+
const relativeToRoot = relative(rootDir, filePath).replace(/\\/g, "/");
|
|
308
|
+
candidates.add(relativeToRoot);
|
|
309
|
+
|
|
310
|
+
const srcDir = (config.srcDir ?? "src").replace(/\\/g, "/").replace(/\/$/, "");
|
|
311
|
+
if (srcDir && relativeToRoot.startsWith(`${srcDir}/`)) {
|
|
312
|
+
candidates.add(relativeToRoot.slice(srcDir.length + 1));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
for (const pattern of config.exclude) {
|
|
317
|
+
for (const candidate of candidates) {
|
|
318
|
+
if (minimatch(candidate, pattern)) {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return true;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Import가 무시 대상인지 확인
|
|
330
|
+
*/
|
|
331
|
+
export function shouldIgnoreImport(
|
|
332
|
+
importPath: string,
|
|
333
|
+
config: GuardConfig
|
|
334
|
+
): boolean {
|
|
335
|
+
const normalizedImportPath = importPath.replace(/\\/g, "/");
|
|
336
|
+
const srcDir = (config.srcDir ?? "src").replace(/\\/g, "/").replace(/\/$/, "");
|
|
337
|
+
const isSrcAbsolute = srcDir.length > 0 && srcDir !== "." &&
|
|
338
|
+
(normalizedImportPath === srcDir || normalizedImportPath.startsWith(`${srcDir}/`));
|
|
339
|
+
|
|
340
|
+
// 외부 모듈 (node_modules)
|
|
341
|
+
if (
|
|
342
|
+
!normalizedImportPath.startsWith(".") &&
|
|
343
|
+
!normalizedImportPath.startsWith("@/") &&
|
|
344
|
+
!normalizedImportPath.startsWith("~/") &&
|
|
345
|
+
!isSrcAbsolute
|
|
346
|
+
) {
|
|
347
|
+
return true;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// 무시 패턴 체크
|
|
351
|
+
if (config.ignoreImports) {
|
|
352
|
+
for (const pattern of config.ignoreImports) {
|
|
353
|
+
if (minimatch(normalizedImportPath, pattern)) {
|
|
354
|
+
return true;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return false;
|
|
360
|
+
}
|