@mandujs/core 0.19.0 → 0.19.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.
Files changed (90) hide show
  1. package/README.ko.md +0 -14
  2. package/package.json +4 -1
  3. package/src/brain/architecture/analyzer.ts +4 -4
  4. package/src/brain/doctor/analyzer.ts +18 -14
  5. package/src/bundler/build.test.ts +127 -0
  6. package/src/bundler/build.ts +291 -113
  7. package/src/bundler/css.ts +20 -5
  8. package/src/bundler/dev.ts +55 -2
  9. package/src/bundler/prerender.ts +195 -0
  10. package/src/change/snapshot.ts +4 -23
  11. package/src/change/types.ts +2 -3
  12. package/src/client/Form.tsx +105 -0
  13. package/src/client/__tests__/use-sse.test.ts +153 -0
  14. package/src/client/hooks.ts +105 -6
  15. package/src/client/index.ts +35 -6
  16. package/src/client/router.ts +670 -433
  17. package/src/client/rpc.ts +140 -0
  18. package/src/client/runtime.ts +24 -21
  19. package/src/client/use-fetch.ts +239 -0
  20. package/src/client/use-head.ts +197 -0
  21. package/src/client/use-sse.ts +378 -0
  22. package/src/components/Image.tsx +162 -0
  23. package/src/config/mandu.ts +5 -0
  24. package/src/config/validate.ts +34 -0
  25. package/src/content/index.ts +5 -1
  26. package/src/devtools/client/catchers/error-catcher.ts +17 -0
  27. package/src/devtools/client/catchers/network-proxy.ts +390 -367
  28. package/src/devtools/client/components/kitchen-root.tsx +479 -467
  29. package/src/devtools/client/components/panel/diff-viewer.tsx +219 -0
  30. package/src/devtools/client/components/panel/guard-panel.tsx +374 -244
  31. package/src/devtools/client/components/panel/index.ts +45 -32
  32. package/src/devtools/client/components/panel/panel-container.tsx +332 -312
  33. package/src/devtools/client/components/panel/preview-panel.tsx +188 -0
  34. package/src/devtools/client/state-manager.ts +535 -478
  35. package/src/devtools/design-tokens.ts +265 -264
  36. package/src/devtools/types.ts +345 -319
  37. package/src/filling/filling.ts +336 -14
  38. package/src/filling/index.ts +5 -1
  39. package/src/filling/session.ts +216 -0
  40. package/src/filling/ws.ts +78 -0
  41. package/src/generator/generate.ts +2 -2
  42. package/src/guard/auto-correct.ts +0 -29
  43. package/src/guard/check.ts +14 -31
  44. package/src/guard/presets/index.ts +296 -294
  45. package/src/guard/rules.ts +15 -19
  46. package/src/guard/validator.ts +834 -834
  47. package/src/index.ts +5 -1
  48. package/src/island/index.ts +373 -304
  49. package/src/kitchen/api/contract-api.ts +225 -0
  50. package/src/kitchen/api/diff-parser.ts +108 -0
  51. package/src/kitchen/api/file-api.ts +273 -0
  52. package/src/kitchen/api/guard-api.ts +83 -0
  53. package/src/kitchen/api/guard-decisions.ts +100 -0
  54. package/src/kitchen/api/routes-api.ts +50 -0
  55. package/src/kitchen/index.ts +21 -0
  56. package/src/kitchen/kitchen-handler.ts +256 -0
  57. package/src/kitchen/kitchen-ui.ts +1732 -0
  58. package/src/kitchen/stream/activity-sse.ts +145 -0
  59. package/src/kitchen/stream/file-tailer.ts +99 -0
  60. package/src/middleware/compress.ts +62 -0
  61. package/src/middleware/cors.ts +47 -0
  62. package/src/middleware/index.ts +10 -0
  63. package/src/middleware/jwt.ts +134 -0
  64. package/src/middleware/logger.ts +58 -0
  65. package/src/middleware/timeout.ts +55 -0
  66. package/src/paths.ts +0 -4
  67. package/src/plugins/hooks.ts +64 -0
  68. package/src/plugins/index.ts +3 -0
  69. package/src/plugins/types.ts +5 -0
  70. package/src/report/build.ts +0 -6
  71. package/src/resource/__tests__/backward-compat.test.ts +0 -1
  72. package/src/router/fs-patterns.ts +11 -1
  73. package/src/router/fs-routes.ts +78 -14
  74. package/src/router/fs-scanner.ts +2 -2
  75. package/src/router/fs-types.ts +2 -1
  76. package/src/runtime/adapter-bun.ts +62 -0
  77. package/src/runtime/adapter.ts +47 -0
  78. package/src/runtime/cache.ts +310 -0
  79. package/src/runtime/handler.ts +65 -0
  80. package/src/runtime/image-handler.ts +195 -0
  81. package/src/runtime/index.ts +12 -0
  82. package/src/runtime/middleware.ts +263 -0
  83. package/src/runtime/server.ts +662 -83
  84. package/src/runtime/ssr.ts +55 -29
  85. package/src/runtime/streaming-ssr.ts +106 -82
  86. package/src/spec/index.ts +0 -1
  87. package/src/spec/schema.ts +1 -0
  88. package/src/testing/index.ts +144 -0
  89. package/src/watcher/watcher.ts +27 -1
  90. package/src/spec/lock.ts +0 -56
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Mandu WebSocket Handler
3
+ * filling.ws({ open, message, close }) 패턴
4
+ */
5
+
6
+ // ========== Types ==========
7
+
8
+ export interface ManduWebSocket {
9
+ /** 고유 연결 ID */
10
+ readonly id: string;
11
+ /** 연결에 첨부된 데이터 */
12
+ readonly data: Record<string, unknown>;
13
+
14
+ /** 메시지 전송 */
15
+ send(data: string | ArrayBuffer | Uint8Array): void;
16
+ /** 토픽 구독 (pub/sub) */
17
+ subscribe(topic: string): void;
18
+ /** 토픽 구독 해제 */
19
+ unsubscribe(topic: string): void;
20
+ /** 토픽에 브로드캐스트 (자신 제외) */
21
+ publish(topic: string, data: string | ArrayBuffer | Uint8Array): void;
22
+ /** 연결 종료 */
23
+ close(code?: number, reason?: string): void;
24
+ /** JSON 전송 헬퍼 */
25
+ sendJSON(data: unknown): void;
26
+ }
27
+
28
+ export interface WSHandlers {
29
+ /** 연결 시 */
30
+ open?(ws: ManduWebSocket): void;
31
+ /** 메시지 수신 시 */
32
+ message?(ws: ManduWebSocket, message: string | ArrayBuffer): void;
33
+ /** 연결 종료 시 */
34
+ close?(ws: ManduWebSocket, code: number, reason: string): void;
35
+ /** 백프레셔 해소 시 */
36
+ drain?(ws: ManduWebSocket): void;
37
+ }
38
+
39
+ export interface WSUpgradeData {
40
+ routeId: string;
41
+ params: Record<string, string>;
42
+ id: string;
43
+ }
44
+
45
+ // ========== Implementation ==========
46
+
47
+ /**
48
+ * Bun WebSocket을 ManduWebSocket으로 래핑
49
+ */
50
+ export function wrapBunWebSocket(
51
+ bunWs: { send: Function; subscribe: Function; unsubscribe: Function; publish: Function; close: Function; data: unknown }
52
+ ): ManduWebSocket {
53
+ const wsData = bunWs.data as WSUpgradeData;
54
+
55
+ return {
56
+ get id() { return wsData.id; },
57
+ get data() { return wsData as unknown as Record<string, unknown>; },
58
+
59
+ send(data: string | ArrayBuffer | Uint8Array) {
60
+ bunWs.send(data);
61
+ },
62
+ subscribe(topic: string) {
63
+ bunWs.subscribe(topic);
64
+ },
65
+ unsubscribe(topic: string) {
66
+ bunWs.unsubscribe(topic);
67
+ },
68
+ publish(topic: string, data: string | ArrayBuffer | Uint8Array) {
69
+ bunWs.publish(topic, data);
70
+ },
71
+ close(code?: number, reason?: string) {
72
+ bunWs.close(code, reason);
73
+ },
74
+ sendJSON(data: unknown) {
75
+ bunWs.send(JSON.stringify(data));
76
+ },
77
+ };
78
+ }
@@ -1,7 +1,7 @@
1
1
  import type { RoutesManifest, RouteSpec } from "../spec/schema";
2
2
  import { generateApiHandler, generatePageComponent, generateSlotLogic } from "./templates";
3
3
  import { generateContractTypeGlue, generateContractTemplate, generateContractTypesIndex } from "./contract-glue";
4
- import { computeHash } from "../spec/lock";
4
+ import { createHash } from "crypto";
5
5
  import { getWatcher } from "../watcher/watcher";
6
6
  import { resolveGeneratedPaths, GENERATED_RELATIVE_PATHS } from "../paths";
7
7
  import path from "path";
@@ -158,7 +158,7 @@ export async function generateRoutes(
158
158
  generatedAt: new Date().toISOString(),
159
159
  specSource: {
160
160
  path: ".mandu/routes.manifest.json",
161
- hash: computeHash(manifest),
161
+ hash: createHash("sha256").update(JSON.stringify(manifest)).digest("hex"),
162
162
  },
163
163
  files: {},
164
164
  frameworkPaths: [
@@ -2,7 +2,6 @@ import type { RoutesManifest } from "../spec/schema";
2
2
  import type { GuardViolation } from "./rules";
3
3
  import { GUARD_RULES } from "./rules";
4
4
  import { runGuardCheck } from "./check";
5
- import { writeLock } from "../spec/lock";
6
5
  import { generateRoutes } from "../generator/generate";
7
6
  import { beginChange, commitChange, rollbackChange } from "../change";
8
7
  import path from "path";
@@ -27,7 +26,6 @@ export interface AutoCorrectResult {
27
26
 
28
27
  // 자동 수정 가능한 규칙들
29
28
  const AUTO_CORRECTABLE_RULES = new Set([
30
- GUARD_RULES.SPEC_HASH_MISMATCH.id,
31
29
  GUARD_RULES.GENERATED_MANUAL_EDIT.id,
32
30
  GUARD_RULES.SLOT_NOT_FOUND.id,
33
31
  ]);
@@ -148,9 +146,6 @@ async function correctViolation(
148
146
  rootDir: string
149
147
  ): Promise<AutoCorrectStep> {
150
148
  switch (violation.ruleId) {
151
- case GUARD_RULES.SPEC_HASH_MISMATCH.id:
152
- return await correctSpecHashMismatch(manifest, rootDir);
153
-
154
149
  case GUARD_RULES.GENERATED_MANUAL_EDIT.id:
155
150
  return await correctGeneratedManualEdit(manifest, rootDir);
156
151
 
@@ -167,30 +162,6 @@ async function correctViolation(
167
162
  }
168
163
  }
169
164
 
170
- async function correctSpecHashMismatch(
171
- manifest: RoutesManifest,
172
- rootDir: string
173
- ): Promise<AutoCorrectStep> {
174
- try {
175
- const lockPath = path.join(rootDir, ".mandu/spec.lock.json");
176
- await writeLock(lockPath, manifest);
177
-
178
- return {
179
- ruleId: GUARD_RULES.SPEC_HASH_MISMATCH.id,
180
- action: "spec-upsert",
181
- success: true,
182
- message: "spec.lock.json 업데이트 완료",
183
- };
184
- } catch (error) {
185
- return {
186
- ruleId: GUARD_RULES.SPEC_HASH_MISMATCH.id,
187
- action: "spec-upsert",
188
- success: false,
189
- message: `spec.lock.json 업데이트 실패: ${error instanceof Error ? error.message : String(error)}`,
190
- };
191
- }
192
- }
193
-
194
165
  async function correctGeneratedManualEdit(
195
166
  manifest: RoutesManifest,
196
167
  rootDir: string
@@ -1,5 +1,4 @@
1
1
  import { GUARD_RULES, FORBIDDEN_IMPORTS, type GuardViolation } from "./rules";
2
- import { verifyLock, computeHash } from "../spec/lock";
3
2
  import { runContractGuardCheck } from "./contract-guard";
4
3
  import { validateSlotContent } from "../slot/validator";
5
4
  import type { RoutesManifest } from "../spec/schema";
@@ -59,27 +58,6 @@ async function readFileContent(filePath: string): Promise<string | null> {
59
58
  }
60
59
  }
61
60
 
62
- // Rule 1: Spec hash mismatch
63
- export async function checkSpecHashMismatch(
64
- manifest: RoutesManifest,
65
- lockPath: string
66
- ): Promise<GuardViolation | null> {
67
- const result = await verifyLock(lockPath, manifest);
68
-
69
- if (!result.valid) {
70
- return {
71
- ruleId: GUARD_RULES.SPEC_HASH_MISMATCH.id,
72
- file: lockPath,
73
- message: result.lockHash
74
- ? `Spec 해시 불일치: lock(${result.lockHash.slice(0, 8)}...) != current(${result.currentHash.slice(0, 8)}...)`
75
- : "spec.lock.json 파일이 없습니다",
76
- suggestion: "bunx mandu spec-upsert를 실행하여 변경사항을 반영하세요",
77
- };
78
- }
79
-
80
- return null;
81
- }
82
-
83
61
  // Rule 2: Generated file manual edit detection
84
62
  export async function checkGeneratedManualEdit(
85
63
  rootDir: string,
@@ -247,16 +225,25 @@ export async function checkIslandFirstIntegrity(
247
225
  continue;
248
226
  }
249
227
 
250
- // 2. componentModule이 island import하는지 확인
251
- if (route.componentModule) {
228
+ // 2. Island-First integrity: verify that a .island.tsx or .client.tsx file
229
+ // exists alongside the page's componentModule. The page does NOT need to
230
+ // directly import the island - the framework auto-links them via
231
+ // data-island attributes and the manifest's clientModule field.
232
+ if (route.componentModule && route.clientModule) {
252
233
  const componentPath = path.join(rootDir, route.componentModule);
253
234
  const content = await readFileContent(componentPath);
254
- if (content && !content.includes("islandModule") && !content.includes("Island-First")) {
235
+ // Check if the island file actually exists on disk
236
+ const clientPath = path.join(rootDir, route.clientModule);
237
+ const clientExists = await fileExists(clientPath);
238
+ if (content && !clientExists && !content.includes("data-island") && !content.includes("data-mandu-island")) {
255
239
  violations.push({
256
240
  ruleId: "ISLAND_FIRST_INTEGRITY",
257
241
  file: route.componentModule,
258
- message: `componentModule이 island import하지 않습니다 (routeId: ${route.id})`,
259
- suggestion: "mandu generate를 실행하여 Island-First 템플릿으로 재생성하세요",
242
+ message: `No island file found for page route (routeId: ${route.id}). The clientModule '${route.clientModule}' does not exist.`,
243
+ suggestion:
244
+ "Create a .island.tsx file in the same app/ directory as page.tsx. " +
245
+ "The page should reference islands via data-island attributes, NOT by directly importing or re-exporting the island. " +
246
+ "Importing island() return values into page.tsx causes a runtime crash because they are config objects, not React components.",
260
247
  });
261
248
  }
262
249
  }
@@ -370,20 +357,17 @@ export async function runGuardCheck(
370
357
  ): Promise<GuardCheckResult> {
371
358
  const config = await loadManduConfig(rootDir);
372
359
 
373
- const lockPath = path.join(rootDir, ".mandu/spec.lock.json");
374
360
  const mapPath = path.join(rootDir, ".mandu/generated/generated.map.json");
375
361
 
376
362
  // ============================================
377
363
  // Phase 1: 독립적인 검사 병렬 실행
378
364
  // ============================================
379
365
  const [
380
- hashViolation,
381
366
  importViolations,
382
367
  slotViolations,
383
368
  specDirViolations,
384
369
  islandViolations,
385
370
  ] = await Promise.all([
386
- checkSpecHashMismatch(manifest, lockPath),
387
371
  checkInvalidGeneratedImport(rootDir),
388
372
  checkSlotFileExists(manifest, rootDir),
389
373
  checkSpecDirNaming(rootDir),
@@ -391,7 +375,6 @@ export async function runGuardCheck(
391
375
  ]);
392
376
 
393
377
  const violations: GuardViolation[] = [];
394
- if (hashViolation) violations.push(hashViolation);
395
378
  violations.push(...importViolations);
396
379
  violations.push(...slotViolations);
397
380
  violations.push(...specDirViolations);