@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.
- package/package.json +1 -1
- package/src/bundler/build.ts +91 -73
- package/src/bundler/dev.ts +21 -14
- package/src/client/globals.ts +44 -0
- package/src/client/index.ts +5 -4
- package/src/client/island.ts +8 -13
- package/src/client/router.ts +33 -41
- package/src/client/runtime.ts +23 -51
- package/src/client/window-state.ts +101 -0
- package/src/config/index.ts +1 -0
- package/src/config/mandu.ts +45 -9
- package/src/config/validate.ts +158 -0
- package/src/constants.ts +25 -0
- package/src/contract/client.ts +4 -3
- package/src/contract/define.ts +459 -0
- package/src/devtools/ai/context-builder.ts +375 -0
- package/src/devtools/ai/index.ts +25 -0
- package/src/devtools/ai/mcp-connector.ts +465 -0
- package/src/devtools/client/catchers/error-catcher.ts +327 -0
- package/src/devtools/client/catchers/index.ts +18 -0
- package/src/devtools/client/catchers/network-proxy.ts +363 -0
- package/src/devtools/client/components/index.ts +39 -0
- package/src/devtools/client/components/kitchen-root.tsx +362 -0
- package/src/devtools/client/components/mandu-character.tsx +241 -0
- package/src/devtools/client/components/overlay.tsx +368 -0
- package/src/devtools/client/components/panel/errors-panel.tsx +259 -0
- package/src/devtools/client/components/panel/guard-panel.tsx +244 -0
- package/src/devtools/client/components/panel/index.ts +32 -0
- package/src/devtools/client/components/panel/islands-panel.tsx +304 -0
- package/src/devtools/client/components/panel/network-panel.tsx +292 -0
- package/src/devtools/client/components/panel/panel-container.tsx +259 -0
- package/src/devtools/client/filters/context-filters.ts +282 -0
- package/src/devtools/client/filters/index.ts +16 -0
- package/src/devtools/client/index.ts +63 -0
- package/src/devtools/client/persistence.ts +335 -0
- package/src/devtools/client/state-manager.ts +478 -0
- package/src/devtools/design-tokens.ts +263 -0
- package/src/devtools/hook/create-hook.ts +207 -0
- package/src/devtools/hook/index.ts +13 -0
- package/src/devtools/index.ts +439 -0
- package/src/devtools/init.ts +266 -0
- package/src/devtools/protocol.ts +237 -0
- package/src/devtools/server/index.ts +17 -0
- package/src/devtools/server/source-context.ts +444 -0
- package/src/devtools/types.ts +319 -0
- package/src/devtools/worker/index.ts +25 -0
- package/src/devtools/worker/redaction-worker.ts +222 -0
- package/src/devtools/worker/worker-manager.ts +409 -0
- package/src/error/formatter.ts +28 -24
- package/src/error/index.ts +13 -9
- package/src/error/result.ts +46 -0
- package/src/error/types.ts +6 -4
- package/src/filling/filling.ts +6 -5
- package/src/guard/check.ts +60 -56
- package/src/guard/types.ts +3 -1
- package/src/guard/watcher.ts +10 -1
- package/src/index.ts +81 -0
- package/src/intent/index.ts +310 -0
- package/src/island/index.ts +304 -0
- package/src/router/fs-patterns.ts +7 -0
- package/src/router/fs-routes.ts +20 -8
- package/src/router/fs-scanner.ts +117 -133
- package/src/runtime/server.ts +261 -201
- package/src/runtime/ssr.ts +5 -4
- package/src/runtime/streaming-ssr.ts +5 -4
- package/src/utils/bun.ts +8 -0
- package/src/utils/lru-cache.ts +75 -0
package/src/router/fs-scanner.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* @module router/fs-scanner
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import {
|
|
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
|
-
|
|
22
|
+
segmentsToPattern,
|
|
23
23
|
detectFileType,
|
|
24
24
|
isPrivateFolder,
|
|
25
25
|
generateRouteId,
|
|
26
26
|
validateSegments,
|
|
27
27
|
sortRoutesByPriority,
|
|
28
|
-
|
|
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.
|
|
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
|
|
102
|
-
|
|
101
|
+
private async scanWithGlob(
|
|
102
|
+
rootDir: string,
|
|
103
103
|
routesRoot: string,
|
|
104
104
|
files: ScannedFile[],
|
|
105
105
|
errors: ScanError[]
|
|
106
106
|
): Promise<void> {
|
|
107
|
-
|
|
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
|
-
|
|
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
|
|
115
|
-
filePath:
|
|
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
|
-
|
|
121
|
-
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
134
|
+
foundFiles.sort((a, b) => a.localeCompare(b));
|
|
122
135
|
|
|
123
|
-
for (const
|
|
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 (
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
144
|
-
}
|
|
145
|
-
if (this.isExcluded(relativePath, false)) {
|
|
146
|
-
continue;
|
|
147
|
-
}
|
|
146
|
+
if (this.hasPrivateSegment(relativePath)) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
148
149
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
}
|
|
150
|
+
const pathSegments = relativePath.split("/");
|
|
151
|
+
if (pathSegments.includes("node_modules")) {
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
154
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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 =
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
const
|
|
204
|
-
const
|
|
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
|
-
|
|
212
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
*/
|