@mandujs/core 0.9.41 → 0.9.42

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.
Files changed (67) hide show
  1. package/package.json +1 -1
  2. package/src/bundler/build.ts +91 -73
  3. package/src/bundler/dev.ts +21 -14
  4. package/src/client/globals.ts +44 -0
  5. package/src/client/index.ts +5 -4
  6. package/src/client/island.ts +8 -13
  7. package/src/client/router.ts +33 -41
  8. package/src/client/runtime.ts +23 -51
  9. package/src/client/window-state.ts +101 -0
  10. package/src/config/index.ts +1 -0
  11. package/src/config/mandu.ts +45 -9
  12. package/src/config/validate.ts +158 -0
  13. package/src/constants.ts +25 -0
  14. package/src/contract/client.ts +4 -3
  15. package/src/contract/define.ts +459 -0
  16. package/src/devtools/ai/context-builder.ts +375 -0
  17. package/src/devtools/ai/index.ts +25 -0
  18. package/src/devtools/ai/mcp-connector.ts +465 -0
  19. package/src/devtools/client/catchers/error-catcher.ts +327 -0
  20. package/src/devtools/client/catchers/index.ts +18 -0
  21. package/src/devtools/client/catchers/network-proxy.ts +363 -0
  22. package/src/devtools/client/components/index.ts +39 -0
  23. package/src/devtools/client/components/kitchen-root.tsx +362 -0
  24. package/src/devtools/client/components/mandu-character.tsx +241 -0
  25. package/src/devtools/client/components/overlay.tsx +368 -0
  26. package/src/devtools/client/components/panel/errors-panel.tsx +259 -0
  27. package/src/devtools/client/components/panel/guard-panel.tsx +244 -0
  28. package/src/devtools/client/components/panel/index.ts +32 -0
  29. package/src/devtools/client/components/panel/islands-panel.tsx +304 -0
  30. package/src/devtools/client/components/panel/network-panel.tsx +292 -0
  31. package/src/devtools/client/components/panel/panel-container.tsx +259 -0
  32. package/src/devtools/client/filters/context-filters.ts +282 -0
  33. package/src/devtools/client/filters/index.ts +16 -0
  34. package/src/devtools/client/index.ts +63 -0
  35. package/src/devtools/client/persistence.ts +335 -0
  36. package/src/devtools/client/state-manager.ts +478 -0
  37. package/src/devtools/design-tokens.ts +263 -0
  38. package/src/devtools/hook/create-hook.ts +207 -0
  39. package/src/devtools/hook/index.ts +13 -0
  40. package/src/devtools/index.ts +439 -0
  41. package/src/devtools/init.ts +266 -0
  42. package/src/devtools/protocol.ts +237 -0
  43. package/src/devtools/server/index.ts +17 -0
  44. package/src/devtools/server/source-context.ts +444 -0
  45. package/src/devtools/types.ts +319 -0
  46. package/src/devtools/worker/index.ts +25 -0
  47. package/src/devtools/worker/redaction-worker.ts +222 -0
  48. package/src/devtools/worker/worker-manager.ts +409 -0
  49. package/src/error/formatter.ts +28 -24
  50. package/src/error/index.ts +13 -9
  51. package/src/error/result.ts +46 -0
  52. package/src/error/types.ts +6 -4
  53. package/src/filling/filling.ts +6 -5
  54. package/src/guard/check.ts +60 -56
  55. package/src/guard/types.ts +3 -1
  56. package/src/guard/watcher.ts +10 -1
  57. package/src/index.ts +81 -0
  58. package/src/intent/index.ts +310 -0
  59. package/src/island/index.ts +304 -0
  60. package/src/router/fs-patterns.ts +7 -0
  61. package/src/router/fs-routes.ts +20 -8
  62. package/src/router/fs-scanner.ts +117 -133
  63. package/src/runtime/server.ts +261 -201
  64. package/src/runtime/ssr.ts +5 -4
  65. package/src/runtime/streaming-ssr.ts +5 -4
  66. package/src/utils/bun.ts +8 -0
  67. package/src/utils/lru-cache.ts +75 -0
@@ -6,7 +6,7 @@
6
6
  * @module router/fs-scanner
7
7
  */
8
8
 
9
- import { readdir, stat } from "fs/promises";
9
+ import { stat } from "fs/promises";
10
10
  import { join, relative, basename, extname } from "path";
11
11
  import type {
12
12
  ScannedFile,
@@ -19,13 +19,13 @@ import type {
19
19
  import { DEFAULT_SCANNER_CONFIG } from "./fs-types";
20
20
  import {
21
21
  parseSegments,
22
- pathToPattern,
22
+ segmentsToPattern,
23
23
  detectFileType,
24
24
  isPrivateFolder,
25
25
  generateRouteId,
26
26
  validateSegments,
27
27
  sortRoutesByPriority,
28
- patternsConflict,
28
+ getPatternShape,
29
29
  } from "./fs-patterns";
30
30
 
31
31
  // ═══════════════════════════════════════════════════════════════════════════
@@ -77,8 +77,8 @@ export class FSScanner {
77
77
  return this.createEmptyResult([], Date.now() - startTime);
78
78
  }
79
79
 
80
- // 재귀 스캔
81
- await this.scanDirectory(routesDir, routesDir, files, errors);
80
+ // Bun.Glob 기반 스캔
81
+ await this.scanWithGlob(rootDir, routesDir, files, errors);
82
82
 
83
83
  // 라우트 설정 생성
84
84
  const { routes, routeErrors } = this.createRouteConfigs(files, rootDir);
@@ -96,90 +96,91 @@ export class FSScanner {
96
96
  }
97
97
 
98
98
  /**
99
- * 디렉토리 재귀 스캔
99
+ * Bun.Glob 기반 스캔
100
100
  */
101
- private async scanDirectory(
102
- dir: string,
101
+ private async scanWithGlob(
102
+ rootDir: string,
103
103
  routesRoot: string,
104
104
  files: ScannedFile[],
105
105
  errors: ScanError[]
106
106
  ): Promise<void> {
107
- let entries;
107
+ const routesDirPattern = this.config.routesDir.replace(/\\/g, "/").replace(/\/+$/, "");
108
+ const extensions = this.config.extensions
109
+ .map((ext) => ext.replace(/^\./, ""))
110
+ .filter(Boolean)
111
+ .join(",");
112
+
113
+ if (!routesDirPattern || !extensions) {
114
+ return;
115
+ }
116
+
117
+ const pattern = `${routesDirPattern}/**/*.{${extensions}}`;
118
+ const glob = new Bun.Glob(pattern);
119
+ const foundFiles: string[] = [];
108
120
 
109
121
  try {
110
- entries = await readdir(dir, { withFileTypes: true });
122
+ for await (const filePath of glob.scan({ cwd: rootDir, absolute: true })) {
123
+ foundFiles.push(filePath);
124
+ }
111
125
  } catch (error) {
112
126
  errors.push({
113
127
  type: "file_read_error",
114
- message: `Failed to read directory: ${error instanceof Error ? error.message : String(error)}`,
115
- filePath: dir,
128
+ message: `Failed to scan directory: ${error instanceof Error ? error.message : String(error)}`,
129
+ filePath: routesRoot,
116
130
  });
117
131
  return;
118
132
  }
119
133
 
120
- // Deterministic traversal order
121
- entries.sort((a, b) => a.name.localeCompare(b.name));
134
+ foundFiles.sort((a, b) => a.localeCompare(b));
122
135
 
123
- for (const entry of entries) {
124
- const fullPath = join(dir, entry.name);
136
+ for (const fullPath of foundFiles) {
125
137
  const relativePath = relative(routesRoot, fullPath).replace(/\\/g, "/");
138
+ if (relativePath.startsWith("..")) {
139
+ continue;
140
+ }
126
141
 
127
- if (entry.isDirectory()) {
128
- if (this.isExcluded(relativePath, true)) {
129
- continue;
130
- }
131
-
132
- // 비공개 폴더 스킵
133
- if (isPrivateFolder(entry.name)) {
134
- continue;
135
- }
136
-
137
- // node_modules 스킵
138
- if (entry.name === "node_modules") {
139
- continue;
140
- }
142
+ if (this.isExcluded(relativePath, false)) {
143
+ continue;
144
+ }
141
145
 
142
- // 재귀 스캔
143
- await this.scanDirectory(fullPath, routesRoot, files, errors);
144
- } else if (entry.isFile()) {
145
- if (this.isExcluded(relativePath, false)) {
146
- continue;
147
- }
146
+ if (this.hasPrivateSegment(relativePath)) {
147
+ continue;
148
+ }
148
149
 
149
- // 확장자 체크
150
- const ext = extname(entry.name);
151
- if (!this.config.extensions.includes(ext)) {
152
- continue;
153
- }
150
+ const pathSegments = relativePath.split("/");
151
+ if (pathSegments.includes("node_modules")) {
152
+ continue;
153
+ }
154
154
 
155
- // 파일 타입 감지
156
- const fileType = detectFileType(entry.name, this.config.islandSuffix);
157
- if (!fileType) {
158
- continue;
159
- }
155
+ const fileName = basename(fullPath);
156
+ const ext = extname(fileName);
157
+ if (!this.config.extensions.includes(ext)) {
158
+ continue;
159
+ }
160
160
 
161
- // 세그먼트 파싱
162
- const segments = parseSegments(relativePath);
163
-
164
- // 세그먼트 유효성 검사
165
- const validation = validateSegments(segments);
166
- if (!validation.valid) {
167
- errors.push({
168
- type: "invalid_segment",
169
- message: validation.error!,
170
- filePath: fullPath,
171
- });
172
- continue;
173
- }
161
+ const fileType = detectFileType(fileName, this.config.islandSuffix);
162
+ if (!fileType) {
163
+ continue;
164
+ }
174
165
 
175
- files.push({
176
- absolutePath: fullPath,
177
- relativePath, // Windows 경로 정규화 완료
178
- type: fileType,
179
- segments,
180
- extension: ext,
166
+ const segments = parseSegments(relativePath);
167
+ const validation = validateSegments(segments);
168
+ if (!validation.valid) {
169
+ errors.push({
170
+ type: "invalid_segment",
171
+ message: validation.error!,
172
+ filePath: fullPath,
181
173
  });
174
+ continue;
182
175
  }
176
+
177
+ files.push({
178
+ absolutePath: fullPath,
179
+ relativePath,
180
+ type: fileType,
181
+ segments,
182
+ extension: ext,
183
+ });
183
184
  }
184
185
  }
185
186
 
@@ -193,26 +194,52 @@ export class FSScanner {
193
194
  const routes: FSRouteConfig[] = [];
194
195
  const routeErrors: ScanError[] = [];
195
196
 
196
- // 패턴별 라우트 매핑 (중복 감지용)
197
+ // 패턴별 라우트 매핑 (중복/충돌 감지용)
197
198
  const patternMap = new Map<string, FSRouteConfig>();
199
+ const shapeMap = new Map<string, FSRouteConfig>();
198
200
 
199
- // 레이아웃 맵 (경로 → 레이아웃 파일)
200
- const layoutMap = this.buildLayoutMap(files);
201
-
202
- // 로딩/에러
203
- const loadingMap = this.buildSpecialFileMap(files, "loading");
204
- const errorMap = this.buildSpecialFileMap(files, "error");
205
-
206
- // Island 맵 (디렉토리 → Island 파일들)
207
- const islandMap = this.buildIslandMap(files);
201
+ // 파일수집 (single pass)
202
+ const layoutMap = new Map<string, ScannedFile>();
203
+ const loadingMap = new Map<string, ScannedFile>();
204
+ const errorMap = new Map<string, ScannedFile>();
205
+ const islandMap = new Map<string, ScannedFile[]>();
206
+ const routeFiles: ScannedFile[] = [];
208
207
 
209
- // 페이지 및 API 라우트 처리
210
208
  for (const file of files) {
211
- if (file.type !== "page" && file.type !== "route") {
212
- continue;
209
+ const dirPath = this.getDirPath(file.relativePath);
210
+
211
+ switch (file.type) {
212
+ case "layout":
213
+ layoutMap.set(dirPath, file);
214
+ break;
215
+ case "loading":
216
+ loadingMap.set(dirPath, file);
217
+ break;
218
+ case "error":
219
+ errorMap.set(dirPath, file);
220
+ break;
221
+ case "island": {
222
+ const existing = islandMap.get(dirPath);
223
+ if (existing) {
224
+ existing.push(file);
225
+ } else {
226
+ islandMap.set(dirPath, [file]);
227
+ }
228
+ break;
229
+ }
230
+ case "page":
231
+ case "route":
232
+ routeFiles.push(file);
233
+ break;
234
+ default:
235
+ break;
213
236
  }
237
+ }
214
238
 
215
- const pattern = pathToPattern(file.relativePath);
239
+ // 페이지 API 라우트 처리
240
+ for (const file of routeFiles) {
241
+ const pattern = segmentsToPattern(file.segments);
242
+ const patternShape = getPatternShape(pattern);
216
243
  const routeId = generateRouteId(file.relativePath);
217
244
  const modulePath = join(this.config.routesDir, file.relativePath);
218
245
 
@@ -229,7 +256,7 @@ export class FSScanner {
229
256
  }
230
257
 
231
258
  // 패턴 충돌 체크 (파라미터 이름만 다른 경우 등)
232
- const conflictingRoute = routes.find((route) => patternsConflict(route.pattern, pattern));
259
+ const conflictingRoute = shapeMap.get(patternShape);
233
260
  if (conflictingRoute) {
234
261
  routeErrors.push({
235
262
  type: "pattern_conflict",
@@ -275,64 +302,12 @@ export class FSScanner {
275
302
 
276
303
  routes.push(route);
277
304
  patternMap.set(pattern, route);
305
+ shapeMap.set(patternShape, route);
278
306
  }
279
307
 
280
308
  return { routes, routeErrors };
281
309
  }
282
310
 
283
- /**
284
- * 레이아웃 맵 구성
285
- */
286
- private buildLayoutMap(files: ScannedFile[]): Map<string, ScannedFile> {
287
- const layoutMap = new Map<string, ScannedFile>();
288
-
289
- for (const file of files) {
290
- if (file.type === "layout") {
291
- const dirPath = this.getDirPath(file.relativePath);
292
- layoutMap.set(dirPath, file);
293
- }
294
- }
295
-
296
- return layoutMap;
297
- }
298
-
299
- /**
300
- * 특수 파일 맵 구성 (loading, error)
301
- */
302
- private buildSpecialFileMap(
303
- files: ScannedFile[],
304
- type: "loading" | "error" | "not-found"
305
- ): Map<string, ScannedFile> {
306
- const map = new Map<string, ScannedFile>();
307
-
308
- for (const file of files) {
309
- if (file.type === type) {
310
- const dirPath = this.getDirPath(file.relativePath);
311
- map.set(dirPath, file);
312
- }
313
- }
314
-
315
- return map;
316
- }
317
-
318
- /**
319
- * Island 맵 구성
320
- */
321
- private buildIslandMap(files: ScannedFile[]): Map<string, ScannedFile[]> {
322
- const islandMap = new Map<string, ScannedFile[]>();
323
-
324
- for (const file of files) {
325
- if (file.type === "island") {
326
- const dirPath = this.getDirPath(file.relativePath);
327
- const existing = islandMap.get(dirPath) || [];
328
- existing.push(file);
329
- islandMap.set(dirPath, existing);
330
- }
331
- }
332
-
333
- return islandMap;
334
- }
335
-
336
311
  /**
337
312
  * 레이아웃 체인 해결
338
313
  */
@@ -395,6 +370,15 @@ export class FSScanner {
395
370
  return lastSlash === -1 ? "." : normalized.slice(0, lastSlash);
396
371
  }
397
372
 
373
+ /**
374
+ * 경로에 비공개 폴더가 포함되어 있는지 확인
375
+ */
376
+ private hasPrivateSegment(relativePath: string): boolean {
377
+ const normalized = relativePath.replace(/\\/g, "/");
378
+ const segments = normalized.split("/").slice(0, -1);
379
+ return segments.some((segment) => isPrivateFolder(segment));
380
+ }
381
+
398
382
  /**
399
383
  * 제외 패턴 적용 여부
400
384
  */