@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
package/README.ko.md CHANGED
@@ -55,19 +55,6 @@ if (result.success && result.data) {
55
55
  }
56
56
  ```
57
57
 
58
- ### Lock 파일
59
-
60
- ```typescript
61
- import { writeLock, readLock } from "@mandujs/core";
62
-
63
- // lock 파일 쓰기
64
- const lock = await writeLock(".mandu/spec.lock.json", manifest);
65
- console.log(lock.routesHash);
66
-
67
- // lock 파일 읽기
68
- const existing = await readLock(".mandu/spec.lock.json");
69
- ```
70
-
71
58
  ## Generator 모듈
72
59
 
73
60
  Spec 기반 코드 생성.
@@ -132,7 +119,6 @@ if (!result.passed) {
132
119
 
133
120
  | 규칙 ID | 설명 | 자동 수정 |
134
121
  |---------|------|----------|
135
- | `SPEC_HASH_MISMATCH` | spec과 lock 해시 불일치 | ✅ |
136
122
  | `GENERATED_MANUAL_EDIT` | generated 파일 수동 수정 | ✅ |
137
123
  | `HANDLER_NOT_FOUND` | 핸들러 파일 없음 | ❌ |
138
124
  | `COMPONENT_NOT_FOUND` | 컴포넌트 파일 없음 | ❌ |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mandujs/core",
3
- "version": "0.19.0",
3
+ "version": "0.19.2",
4
4
  "description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
5
5
  "type": "module",
6
6
  "main": "./src/index.ts",
@@ -8,8 +8,11 @@
8
8
  "exports": {
9
9
  ".": "./src/index.ts",
10
10
  "./client": "./src/client/index.ts",
11
+ "./middleware": "./src/middleware/index.ts",
12
+ "./testing": "./src/testing/index.ts",
11
13
  "./plugins": "./src/plugins/index.ts",
12
14
  "./error": "./src/error/index.ts",
15
+ "./bundler/prerender": "./src/bundler/prerender.ts",
13
16
  "./*": "./src/*"
14
17
  },
15
18
  "files": [
@@ -34,7 +34,7 @@ export const DEFAULT_ARCHITECTURE_CONFIG: ArchitectureConfig = {
34
34
  "spec/slots/": {
35
35
  pattern: "spec/slots/**",
36
36
  description: "Slot 파일 전용",
37
- allowedFiles: ["*.slot.ts", "*.client.ts"],
37
+ allowedFiles: ["*.slot.ts", "*.slot.tsx", "*.client.ts", "*.client.tsx"],
38
38
  },
39
39
  "spec/contracts/": {
40
40
  pattern: "spec/contracts/**",
@@ -96,9 +96,9 @@ export const DEFAULT_ARCHITECTURE_CONFIG: ArchitectureConfig = {
96
96
  naming: [
97
97
  {
98
98
  folder: "spec/slots/",
99
- filePattern: "^[a-z][a-z0-9-]*\\.(slot|client)\\.ts$",
100
- description: "Slot 파일은 kebab-case.slot.ts 또는 kebab-case.client.ts",
101
- examples: ["users-list.slot.ts", "counter.client.ts"],
99
+ filePattern: "^[a-z][a-z0-9-]*\\.(slot|client)\\.tsx?$",
100
+ description: "Slot 파일은 kebab-case.slot.ts(x) 또는 kebab-case.client.ts(x)",
101
+ examples: ["users-list.slot.ts", "counter.client.tsx"],
102
102
  },
103
103
  {
104
104
  folder: "spec/contracts/",
@@ -25,7 +25,7 @@ export type ViolationCategory =
25
25
  * Categorize a violation by its rule ID
26
26
  */
27
27
  export function categorizeViolation(ruleId: string): ViolationCategory {
28
- if (ruleId.includes("SPEC") || ruleId === "SPEC_HASH_MISMATCH") {
28
+ if (ruleId.includes("SPEC")) {
29
29
  return "spec";
30
30
  }
31
31
  if (ruleId.includes("GENERATED") || ruleId.includes("FORBIDDEN_IMPORT")) {
@@ -138,16 +138,6 @@ export function generateTemplatePatches(
138
138
 
139
139
  for (const violation of violations) {
140
140
  switch (violation.ruleId) {
141
- case GUARD_RULES.SPEC_HASH_MISMATCH?.id:
142
- patches.push({
143
- file: ".mandu/spec.lock.json",
144
- description: "Spec lock 파일 갱신",
145
- type: "command",
146
- command: "bunx mandu spec-upsert",
147
- confidence: 0.9,
148
- });
149
- break;
150
-
151
141
  case GUARD_RULES.GENERATED_MANUAL_EDIT?.id:
152
142
  patches.push({
153
143
  file: violation.file,
@@ -208,6 +198,23 @@ export function generateTemplatePatches(
208
198
  });
209
199
  break;
210
200
 
201
+ case GUARD_RULES.ISLAND_FIRST_INTEGRITY?.id:
202
+ patches.push({
203
+ file: violation.file,
204
+ description:
205
+ "Create a .island.tsx file in the same app/ directory as page.tsx. " +
206
+ "Do NOT import or re-export the island in page.tsx — island() returns " +
207
+ "a config object, not a React component. Use data-island attributes instead.",
208
+ type: "modify",
209
+ content:
210
+ `// Example: app/my-feature.island.tsx\n` +
211
+ `import { island } from "@mandujs/core/client";\n\n` +
212
+ `export default island("visible", MyComponent);\n\n` +
213
+ `// In page.tsx, reference via: <div data-island="my-feature">...</div>`,
214
+ confidence: 0.9,
215
+ });
216
+ break;
217
+
211
218
  default:
212
219
  // Generic suggestion based on violation.suggestion
213
220
  if (violation.suggestion) {
@@ -307,9 +314,6 @@ export async function analyzeViolations(
307
314
 
308
315
  // Determine recommended next command
309
316
  let nextCommand = "bunx mandu generate";
310
- if (violations.some((v) => v.ruleId === GUARD_RULES.SPEC_HASH_MISMATCH?.id)) {
311
- nextCommand = "bunx mandu spec-upsert";
312
- }
313
317
 
314
318
  // If LLM is not requested or not available, return template analysis
315
319
  if (!useLLM) {
@@ -0,0 +1,127 @@
1
+ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
2
+ import { mkdtemp, mkdir, readFile, rm, writeFile } from "fs/promises";
3
+ import path from "path";
4
+ import { pathToFileURL } from "url";
5
+ import type { RoutesManifest } from "../spec/schema";
6
+ import type { BundleResult } from "./types";
7
+ import { buildClientBundles } from "./build";
8
+
9
+ // 모든 테스트가 하나의 빌드 결과를 공유 — 병렬 Bun.build 충돌 방지
10
+ let rootDir: string;
11
+ let result: BundleResult;
12
+
13
+ async function importBuiltModule(relativePath: string): Promise<Record<string, unknown>> {
14
+ const fileUrl = pathToFileURL(path.join(rootDir, relativePath)).href;
15
+ return import(`${fileUrl}?t=${Date.now()}`);
16
+ }
17
+
18
+ beforeAll(async () => {
19
+ rootDir = await mkdtemp(path.join(import.meta.dir, ".tmp-bundler-"));
20
+
21
+ await mkdir(path.join(rootDir, "app"), { recursive: true });
22
+ await writeFile(
23
+ path.join(rootDir, "package.json"),
24
+ JSON.stringify({ name: "mandu-build-test", type: "module" }, null, 2),
25
+ "utf-8",
26
+ );
27
+ await writeFile(
28
+ path.join(rootDir, "app", "demo.client.tsx"),
29
+ "export default function DemoIsland() { return null; }\n",
30
+ "utf-8",
31
+ );
32
+
33
+ const manifest: RoutesManifest = {
34
+ version: 1,
35
+ routes: [
36
+ {
37
+ id: "demo",
38
+ kind: "page",
39
+ pattern: "/",
40
+ module: "app/page.tsx",
41
+ componentModule: "app/page.tsx",
42
+ clientModule: "app/demo.client.tsx",
43
+ hydration: {
44
+ strategy: "island",
45
+ priority: "visible",
46
+ preload: false,
47
+ },
48
+ },
49
+ ],
50
+ };
51
+
52
+ result = await buildClientBundles(manifest, rootDir, {
53
+ minify: false,
54
+ sourcemap: false,
55
+ splitting: false,
56
+ });
57
+ });
58
+
59
+ afterAll(async () => {
60
+ if (rootDir) {
61
+ await rm(rootDir, { recursive: true, force: true });
62
+ }
63
+ });
64
+
65
+ describe("buildClientBundles vendor shims", () => {
66
+ test("build succeeds", () => {
67
+ if (!result.success) {
68
+ console.error("[build.test] errors:", result.errors);
69
+ }
70
+ expect(result.success).toBe(true);
71
+ });
72
+
73
+ test("re-exports modern React 19 APIs used by islands", async () => {
74
+ const reactShim = await importBuiltModule(".mandu/client/_react.js");
75
+ const requiredExports = [
76
+ "Activity",
77
+ "__COMPILER_RUNTIME",
78
+ "cache",
79
+ "cacheSignal",
80
+ "startTransition",
81
+ "use",
82
+ "useActionState",
83
+ "useEffectEvent",
84
+ "useOptimistic",
85
+ "unstable_useCacheRefresh",
86
+ ];
87
+
88
+ for (const exportName of requiredExports) {
89
+ expect(exportName in reactShim).toBe(true);
90
+ }
91
+ });
92
+
93
+ test("re-exports modern react-dom and react-dom/client APIs", async () => {
94
+ const reactDomShim = await importBuiltModule(".mandu/client/_react-dom.js");
95
+ for (const exportName of [
96
+ "preconnect",
97
+ "prefetchDNS",
98
+ "preinit",
99
+ "preinitModule",
100
+ "preload",
101
+ "preloadModule",
102
+ "requestFormReset",
103
+ "unstable_batchedUpdates",
104
+ "useFormState",
105
+ "useFormStatus",
106
+ ]) {
107
+ expect(exportName in reactDomShim).toBe(true);
108
+ }
109
+
110
+ const reactDomClientShim = await importBuiltModule(".mandu/client/_react-dom-client.js");
111
+ for (const exportName of ["createRoot", "hydrateRoot", "version"]) {
112
+ expect(exportName in reactDomClientShim).toBe(true);
113
+ }
114
+ });
115
+
116
+ test("embeds hydration guards for deferred trigger strategies", async () => {
117
+ const runtimeSource = await readFile(path.join(rootDir, ".mandu", "client", "_runtime.js"), "utf-8");
118
+ expect(runtimeSource).toContain("function resolveHydrationTarget");
119
+ expect(runtimeSource).toContain("function hasHydratableMarkup");
120
+ expect(runtimeSource).toContain("function shouldHydrateCompiledIsland");
121
+ expect(runtimeSource).toContain("onRecoverableError");
122
+ expect(runtimeSource).toContain("data-mandu-hydrating");
123
+ expect(runtimeSource).toContain("data-mandu-render-mode");
124
+ expect(runtimeSource).toContain("data-mandu-recoverable-error");
125
+ expect(runtimeSource).toContain("pointerdown");
126
+ });
127
+ });