@mandujs/core 0.20.10 → 0.22.0

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 (127) hide show
  1. package/README.md +2 -1
  2. package/package.json +28 -3
  3. package/src/auth/__tests__/login.test.ts +419 -0
  4. package/src/auth/__tests__/password.test.ts +122 -0
  5. package/src/auth/__tests__/reset.test.ts +296 -0
  6. package/src/auth/__tests__/tokens.test.ts +274 -0
  7. package/src/auth/__tests__/verification.test.ts +274 -0
  8. package/src/auth/index.ts +76 -0
  9. package/src/auth/login.ts +225 -0
  10. package/src/auth/password.ts +120 -0
  11. package/src/auth/reset.ts +243 -0
  12. package/src/auth/tokens.ts +612 -0
  13. package/src/auth/verification.ts +253 -0
  14. package/src/bundler/__tests__/cli-bench-utils.test.ts +149 -0
  15. package/src/bundler/__tests__/cold-start.test.ts +504 -0
  16. package/src/bundler/__tests__/csp-nonce.test.ts +278 -0
  17. package/src/bundler/__tests__/dev-reliability.test.ts +619 -0
  18. package/src/bundler/__tests__/extended-watch.test.ts +710 -0
  19. package/src/bundler/__tests__/fast-refresh.test.ts +596 -0
  20. package/src/bundler/__tests__/hdr.test.ts +353 -0
  21. package/src/bundler/__tests__/hmr-client.test.ts +532 -0
  22. package/src/bundler/__tests__/manifest-schema.test.ts +266 -0
  23. package/src/bundler/__tests__/prod-smoke.test.ts +138 -0
  24. package/src/bundler/__tests__/slot-dispatch.test.ts +573 -0
  25. package/src/bundler/__tests__/url-cap-and-slot-regex.test.ts +286 -0
  26. package/src/bundler/__tests__/vendor-cache.test.ts +455 -0
  27. package/src/bundler/build.test.ts +8 -1
  28. package/src/bundler/build.ts +495 -37
  29. package/src/bundler/css.ts +326 -323
  30. package/src/bundler/dev.ts +1671 -80
  31. package/src/bundler/fast-refresh-plugin.ts +307 -0
  32. package/src/bundler/hmr-types.ts +252 -0
  33. package/src/bundler/manifest-schema.ts +301 -0
  34. package/src/bundler/safe-build.test.ts +128 -0
  35. package/src/bundler/safe-build.ts +77 -0
  36. package/src/bundler/scenario-matrix.ts +229 -0
  37. package/src/bundler/types.ts +19 -0
  38. package/src/bundler/vendor-cache-types.ts +130 -0
  39. package/src/bundler/vendor-cache.ts +526 -0
  40. package/src/client/router.ts +214 -56
  41. package/src/config/validate.ts +1 -0
  42. package/src/db/__tests__/db.test.ts +485 -0
  43. package/src/db/index.ts +513 -0
  44. package/src/db/migrations/__tests__/runner.test.ts +661 -0
  45. package/src/db/migrations/history-table.ts +345 -0
  46. package/src/db/migrations/lock.ts +269 -0
  47. package/src/db/migrations/runner.ts +633 -0
  48. package/src/desktop/__tests__/smoke.test.ts +100 -0
  49. package/src/desktop/__tests__/window.test.ts +172 -0
  50. package/src/desktop/__tests__/worker.test.ts +266 -0
  51. package/src/desktop/index.ts +43 -0
  52. package/src/desktop/types.ts +158 -0
  53. package/src/desktop/window.ts +492 -0
  54. package/src/desktop/worker.ts +180 -0
  55. package/src/devtools/ai/mcp-connector.ts +18 -16
  56. package/src/devtools/client/components/mandu-character.tsx +4 -1
  57. package/src/devtools/client/components/panel/panel-container.tsx +20 -5
  58. package/src/email/__tests__/email.test.ts +355 -0
  59. package/src/email/index.ts +282 -0
  60. package/src/email/resend.ts +163 -0
  61. package/src/email/smtp.ts +64 -0
  62. package/src/filling/__tests__/session-sqlite.test.ts +454 -0
  63. package/src/filling/context.ts +72 -78
  64. package/src/filling/cookie-codec.ts +299 -0
  65. package/src/filling/deps.ts +25 -1
  66. package/src/filling/filling.ts +28 -3
  67. package/src/filling/session-sqlite.ts +617 -0
  68. package/src/filling/session.ts +265 -216
  69. package/src/guard/decision-memory.test.ts +52 -22
  70. package/src/id/__tests__/id.test.ts +120 -0
  71. package/src/id/index.ts +105 -0
  72. package/src/kitchen/index.ts +2 -2
  73. package/src/kitchen/kitchen-handler.ts +86 -0
  74. package/src/kitchen/stream/activity-sse.ts +2 -1
  75. package/src/middleware/csrf.ts +328 -0
  76. package/src/middleware/index.ts +40 -0
  77. package/src/middleware/oauth/__tests__/oauth.test.ts +574 -0
  78. package/src/middleware/oauth/index.ts +505 -0
  79. package/src/middleware/oauth/providers.ts +115 -0
  80. package/src/middleware/rate-limit/__tests__/rate-limit.test.ts +642 -0
  81. package/src/middleware/rate-limit/index.ts +522 -0
  82. package/src/middleware/rate-limit/sqlite-store.ts +382 -0
  83. package/src/middleware/secure/__tests__/secure.test.ts +360 -0
  84. package/src/middleware/secure/csp.ts +193 -0
  85. package/src/middleware/secure/index.ts +417 -0
  86. package/src/middleware/session.ts +174 -0
  87. package/src/observability/event-bus.ts +81 -79
  88. package/src/paths.ts +37 -0
  89. package/src/perf/hmr-markers.ts +215 -0
  90. package/src/perf/index.ts +104 -0
  91. package/src/resource/__tests__/generator.test.ts +603 -2
  92. package/src/resource/ddl/__tests__/diff.test.ts +639 -0
  93. package/src/resource/ddl/__tests__/emit.test.ts +799 -0
  94. package/src/resource/ddl/__tests__/snapshot.test.ts +499 -0
  95. package/src/resource/ddl/diff.ts +392 -0
  96. package/src/resource/ddl/emit.ts +548 -0
  97. package/src/resource/ddl/persistence-types.ts +218 -0
  98. package/src/resource/ddl/snapshot.ts +447 -0
  99. package/src/resource/ddl/type-map.ts +223 -0
  100. package/src/resource/ddl/types.ts +232 -0
  101. package/src/resource/generator-repo.ts +610 -0
  102. package/src/resource/generator-schema.ts +476 -0
  103. package/src/resource/generator.ts +117 -1
  104. package/src/resource/index.ts +17 -1
  105. package/src/resource/schema.ts +30 -0
  106. package/src/router/fs-scanner.ts +3 -0
  107. package/src/runtime/__tests__/error-boundary-redaction.test.ts +141 -0
  108. package/src/runtime/__tests__/hdr-client.test.ts +223 -0
  109. package/src/runtime/__tests__/http-errors.test.ts +117 -0
  110. package/src/runtime/__tests__/not-found.test.ts +152 -0
  111. package/src/runtime/boundary.tsx +21 -1
  112. package/src/runtime/fast-refresh-runtime.ts +322 -0
  113. package/src/runtime/fast-refresh-types.ts +128 -0
  114. package/src/runtime/hmr-client.ts +409 -0
  115. package/src/runtime/http-errors.ts +113 -0
  116. package/src/runtime/index.ts +6 -0
  117. package/src/runtime/logger.ts +678 -677
  118. package/src/runtime/not-found.ts +93 -0
  119. package/src/runtime/redirect.ts +133 -0
  120. package/src/runtime/server.ts +679 -23
  121. package/src/runtime/ssr.ts +340 -10
  122. package/src/runtime/streaming-ssr.ts +222 -19
  123. package/src/scheduler/__tests__/scheduler.test.ts +514 -0
  124. package/src/scheduler/index.ts +343 -0
  125. package/src/storage/s3/__tests__/s3.test.ts +479 -0
  126. package/src/storage/s3/index.ts +412 -0
  127. package/src/testing/index.ts +247 -189
@@ -1,189 +1,247 @@
1
- /**
2
- * Mandu Testing Utilities
3
- * 서버 없이 라우트/filling 단위 테스트
4
- */
5
-
6
- import { ManduContext } from "../filling/context";
7
- import type { ManduFilling } from "../filling/filling";
8
- import type { RouteSpec, RoutesManifest } from "../spec/schema";
9
-
10
- // ========== Types ==========
11
-
12
- export interface TestRequestOptions {
13
- method?: string;
14
- query?: Record<string, string>;
15
- body?: unknown;
16
- headers?: Record<string, string>;
17
- params?: Record<string, string>;
18
- /** Action 이름 — 자동으로 _action을 body에 삽입하고 ManduAction 헤더를 추가 */
19
- action?: string;
20
- }
21
-
22
- // ========== testFilling ==========
23
-
24
- /**
25
- * Filling 단위 테스트 — 서버 없이 직접 실행
26
- *
27
- * @example
28
- * ```typescript
29
- * import { testFilling } from "@mandujs/core/testing";
30
- * import todoRoute from "./app/api/todos/route";
31
- *
32
- * const res = await testFilling(todoRoute, {
33
- * method: "GET",
34
- * query: { page: "2" },
35
- * });
36
- * expect(res.status).toBe(200);
37
- *
38
- * const data = await res.json();
39
- * expect(data.todos).toHaveLength(10);
40
- * ```
41
- */
42
- export async function testFilling(
43
- filling: ManduFilling,
44
- options: TestRequestOptions = {}
45
- ): Promise<Response> {
46
- const {
47
- method: rawMethod,
48
- query,
49
- body: rawBody,
50
- headers: rawHeaders = {},
51
- params = {},
52
- action,
53
- } = options;
54
-
55
- // action 지정 시 자동으로 POST + _action body + ManduAction 헤더
56
- const method = rawMethod ?? (action ? "POST" : "GET");
57
- const headers = { ...rawHeaders };
58
- let body = rawBody;
59
-
60
- if (action) {
61
- headers["X-Requested-With"] = "ManduAction";
62
- headers["Accept"] = "application/json";
63
- if (body && typeof body === "object" && !(body instanceof FormData)) {
64
- body = { _action: action, ...(body as Record<string, unknown>) };
65
- } else if (!body) {
66
- body = { _action: action };
67
- }
68
- }
69
-
70
- const url = new URL("http://localhost/test");
71
- if (query) {
72
- for (const [key, value] of Object.entries(query)) {
73
- url.searchParams.set(key, value);
74
- }
75
- }
76
-
77
- const requestInit: RequestInit = {
78
- method,
79
- headers,
80
- };
81
-
82
- if (body !== undefined && method !== "GET" && method !== "HEAD") {
83
- if (body instanceof FormData) {
84
- requestInit.body = body;
85
- } else {
86
- requestInit.body = JSON.stringify(body);
87
- (requestInit.headers as Record<string, string>)["Content-Type"] = "application/json";
88
- }
89
- }
90
-
91
- const request = new Request(url.toString(), requestInit);
92
- return filling.handle(request, params);
93
- }
94
-
95
- /**
96
- * 간단한 Request 생성 헬퍼
97
- *
98
- * @example
99
- * ```typescript
100
- * const req = createTestRequest("/api/todos", { method: "POST", body: { title: "test" } });
101
- * ```
102
- */
103
- export function createTestRequest(
104
- path: string,
105
- options: TestRequestOptions = {}
106
- ): Request {
107
- const { method = "GET", query, body, headers = {} } = options;
108
-
109
- const url = new URL(`http://localhost${path}`);
110
- if (query) {
111
- for (const [key, value] of Object.entries(query)) {
112
- url.searchParams.set(key, value);
113
- }
114
- }
115
-
116
- const requestInit: RequestInit = { method, headers: { ...headers } };
117
-
118
- if (body !== undefined && method !== "GET" && method !== "HEAD") {
119
- if (body instanceof FormData) {
120
- requestInit.body = body;
121
- } else {
122
- requestInit.body = JSON.stringify(body);
123
- (requestInit.headers as Record<string, string>)["Content-Type"] = "application/json";
124
- }
125
- }
126
-
127
- return new Request(url.toString(), requestInit);
128
- }
129
-
130
- /**
131
- * ManduContext 테스트용 생성 헬퍼
132
- *
133
- * @example
134
- * ```typescript
135
- * const ctx = createTestContext("/api/users/123", { params: { id: "123" } });
136
- * expect(ctx.params.id).toBe("123");
137
- * ```
138
- */
139
- export function createTestContext(
140
- path: string,
141
- options: TestRequestOptions = {}
142
- ): ManduContext {
143
- const request = createTestRequest(path, options);
144
- return new ManduContext(request, options.params);
145
- }
146
-
147
- // ========== Test Factories ==========
148
-
149
- /**
150
- * Create a RoutesManifest from partial route definitions.
151
- * Fills in sensible defaults so tests only specify the fields they care about.
152
- *
153
- * @example
154
- * ```typescript
155
- * const manifest = createTestManifest([
156
- * { id: "home", kind: "page", pattern: "/" },
157
- * { id: "api-users", kind: "api", pattern: "/api/users" },
158
- * ]);
159
- * ```
160
- */
161
- export function createTestManifest(routes: Partial<RouteSpec>[]): RoutesManifest {
162
- return {
163
- version: 1,
164
- routes: routes.map((r, i) => ({
165
- id: r.id ?? `test-route-${i}`,
166
- kind: r.kind ?? "page",
167
- pattern: r.pattern ?? `/test-${i}`,
168
- module: r.module ?? `app/test-${i}/page.tsx`,
169
- componentModule:
170
- (r.kind ?? "page") === "page"
171
- ? (r.componentModule ?? r.module ?? `app/test-${i}/page.tsx`)
172
- : undefined,
173
- ...r,
174
- })) as RouteSpec[],
175
- };
176
- }
177
-
178
- /**
179
- * Create a minimal island descriptor for testing hydration logic.
180
- *
181
- * @example
182
- * ```typescript
183
- * const island = createTestIsland("counter", "interaction");
184
- * expect(island.__hydrate).toBe("interaction");
185
- * ```
186
- */
187
- export function createTestIsland(name: string, strategy: string = "visible") {
188
- return { __island: true, __hydrate: strategy, __name: name };
189
- }
1
+ /**
2
+ * Mandu Testing Utilities
3
+ * 서버 없이 라우트/filling 단위 테스트
4
+ */
5
+
6
+ import path from "path";
7
+ import os from "os";
8
+ import { ManduContext } from "../filling/context";
9
+ import type { ManduFilling } from "../filling/filling";
10
+ import type { RouteSpec, RoutesManifest } from "../spec/schema";
11
+
12
+ // ========== Types ==========
13
+
14
+ export interface TestRequestOptions {
15
+ method?: string;
16
+ query?: Record<string, string>;
17
+ body?: unknown;
18
+ headers?: Record<string, string>;
19
+ params?: Record<string, string>;
20
+ /** Action 이름 — 자동으로 _action을 body에 삽입하고 ManduAction 헤더를 추가 */
21
+ action?: string;
22
+ }
23
+
24
+ // ========== testFilling ==========
25
+
26
+ /**
27
+ * Filling 단위 테스트 — 서버 없이 직접 실행
28
+ *
29
+ * @example
30
+ * ```typescript
31
+ * import { testFilling } from "@mandujs/core/testing";
32
+ * import todoRoute from "./app/api/todos/route";
33
+ *
34
+ * const res = await testFilling(todoRoute, {
35
+ * method: "GET",
36
+ * query: { page: "2" },
37
+ * });
38
+ * expect(res.status).toBe(200);
39
+ *
40
+ * const data = await res.json();
41
+ * expect(data.todos).toHaveLength(10);
42
+ * ```
43
+ */
44
+ export async function testFilling(
45
+ filling: ManduFilling,
46
+ options: TestRequestOptions = {}
47
+ ): Promise<Response> {
48
+ const {
49
+ method: rawMethod,
50
+ query,
51
+ body: rawBody,
52
+ headers: rawHeaders = {},
53
+ params = {},
54
+ action,
55
+ } = options;
56
+
57
+ // action 지정 자동으로 POST + _action body + ManduAction 헤더
58
+ const method = rawMethod ?? (action ? "POST" : "GET");
59
+ const headers = { ...rawHeaders };
60
+ let body = rawBody;
61
+
62
+ if (action) {
63
+ headers["X-Requested-With"] = "ManduAction";
64
+ headers["Accept"] = "application/json";
65
+ if (body && typeof body === "object" && !(body instanceof FormData)) {
66
+ body = { _action: action, ...(body as Record<string, unknown>) };
67
+ } else if (!body) {
68
+ body = { _action: action };
69
+ }
70
+ }
71
+
72
+ const url = new URL("http://localhost/test");
73
+ if (query) {
74
+ for (const [key, value] of Object.entries(query)) {
75
+ url.searchParams.set(key, value);
76
+ }
77
+ }
78
+
79
+ const requestInit: RequestInit = {
80
+ method,
81
+ headers,
82
+ };
83
+
84
+ if (body !== undefined && method !== "GET" && method !== "HEAD") {
85
+ if (body instanceof FormData) {
86
+ requestInit.body = body;
87
+ } else {
88
+ requestInit.body = JSON.stringify(body);
89
+ (requestInit.headers as Record<string, string>)["Content-Type"] = "application/json";
90
+ }
91
+ }
92
+
93
+ const request = new Request(url.toString(), requestInit);
94
+ return filling.handle(request, params);
95
+ }
96
+
97
+ /**
98
+ * 간단한 Request 생성 헬퍼
99
+ *
100
+ * @example
101
+ * ```typescript
102
+ * const req = createTestRequest("/api/todos", { method: "POST", body: { title: "test" } });
103
+ * ```
104
+ */
105
+ export function createTestRequest(
106
+ path: string,
107
+ options: TestRequestOptions = {}
108
+ ): Request {
109
+ const { method = "GET", query, body, headers = {} } = options;
110
+
111
+ const url = new URL(`http://localhost${path}`);
112
+ if (query) {
113
+ for (const [key, value] of Object.entries(query)) {
114
+ url.searchParams.set(key, value);
115
+ }
116
+ }
117
+
118
+ const requestInit: RequestInit = { method, headers: { ...headers } };
119
+
120
+ if (body !== undefined && method !== "GET" && method !== "HEAD") {
121
+ if (body instanceof FormData) {
122
+ requestInit.body = body;
123
+ } else {
124
+ requestInit.body = JSON.stringify(body);
125
+ (requestInit.headers as Record<string, string>)["Content-Type"] = "application/json";
126
+ }
127
+ }
128
+
129
+ return new Request(url.toString(), requestInit);
130
+ }
131
+
132
+ /**
133
+ * ManduContext 테스트용 생성 헬퍼
134
+ *
135
+ * @example
136
+ * ```typescript
137
+ * const ctx = createTestContext("/api/users/123", { params: { id: "123" } });
138
+ * expect(ctx.params.id).toBe("123");
139
+ * ```
140
+ */
141
+ export function createTestContext(
142
+ path: string,
143
+ options: TestRequestOptions = {}
144
+ ): ManduContext {
145
+ const request = createTestRequest(path, options);
146
+ return new ManduContext(request, options.params);
147
+ }
148
+
149
+ // ========== Test Factories ==========
150
+
151
+ /**
152
+ * Create a RoutesManifest from partial route definitions.
153
+ * Fills in sensible defaults so tests only specify the fields they care about.
154
+ *
155
+ * @example
156
+ * ```typescript
157
+ * const manifest = createTestManifest([
158
+ * { id: "home", kind: "page", pattern: "/" },
159
+ * { id: "api-users", kind: "api", pattern: "/api/users" },
160
+ * ]);
161
+ * ```
162
+ */
163
+ export function createTestManifest(routes: Partial<RouteSpec>[]): RoutesManifest {
164
+ return {
165
+ version: 1,
166
+ routes: routes.map((r, i) => ({
167
+ id: r.id ?? `test-route-${i}`,
168
+ kind: r.kind ?? "page",
169
+ pattern: r.pattern ?? `/test-${i}`,
170
+ module: r.module ?? `app/test-${i}/page.tsx`,
171
+ componentModule:
172
+ (r.kind ?? "page") === "page"
173
+ ? (r.componentModule ?? r.module ?? `app/test-${i}/page.tsx`)
174
+ : undefined,
175
+ ...r,
176
+ })) as RouteSpec[],
177
+ };
178
+ }
179
+
180
+ /**
181
+ * Create a minimal island descriptor for testing hydration logic.
182
+ *
183
+ * @example
184
+ * ```typescript
185
+ * const island = createTestIsland("counter", "interaction");
186
+ * expect(island.__hydrate).toBe("interaction");
187
+ * ```
188
+ */
189
+ export function createTestIsland(name: string, strategy: string = "visible") {
190
+ return { __island: true, __hydrate: strategy, __name: name };
191
+ }
192
+
193
+ // ========== createMockMcpContext ==========
194
+
195
+ /**
196
+ * Mock MCP context shape mirroring the real MCP server context
197
+ * (see `packages/mcp/src/utils/project.ts`).
198
+ *
199
+ * Use this in tests for MCP tool plugins and slot/contract validators
200
+ * that accept an MCP-context-like object.
201
+ */
202
+ export interface MockMcpContext {
203
+ paths: {
204
+ repoRoot: string;
205
+ manduDir: string;
206
+ manifestPath: string;
207
+ specsDir: string;
208
+ slotsDir: string;
209
+ };
210
+ readConfig: () => Promise<Record<string, unknown>>;
211
+ readManifest: () => Promise<RoutesManifest>;
212
+ }
213
+
214
+ /**
215
+ * Create a mock MCP context suitable for unit-testing MCP tools without
216
+ * spinning up an actual project on disk.
217
+ *
218
+ * @example
219
+ * ```typescript
220
+ * const ctx = createMockMcpContext({
221
+ * config: { guard: { preset: "fsd" } },
222
+ * manifest: createTestManifest([{ id: "home", kind: "page", pattern: "/" }]),
223
+ * });
224
+ * await myTool.execute(ctx, { input: "..." });
225
+ * ```
226
+ */
227
+ export function createMockMcpContext(options: {
228
+ root?: string;
229
+ config?: Record<string, unknown>;
230
+ manifest?: RoutesManifest;
231
+ } = {}): MockMcpContext {
232
+ const root = options.root ?? path.join(os.tmpdir(), "mandu-mock-mcp");
233
+ const config = options.config ?? {};
234
+ const manifest = options.manifest ?? { version: 1, routes: [] };
235
+
236
+ return {
237
+ paths: {
238
+ repoRoot: root,
239
+ manduDir: path.join(root, ".mandu"),
240
+ manifestPath: path.join(root, ".mandu", "routes.manifest.json"),
241
+ specsDir: path.join(root, "spec"),
242
+ slotsDir: path.join(root, "spec", "slots"),
243
+ },
244
+ readConfig: async () => config,
245
+ readManifest: async () => manifest,
246
+ };
247
+ }