@mandujs/core 0.21.0 → 0.22.1

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 (122) hide show
  1. package/package.json +101 -69
  2. package/src/auth/__tests__/login.test.ts +419 -0
  3. package/src/auth/__tests__/password.test.ts +122 -0
  4. package/src/auth/__tests__/reset.test.ts +296 -0
  5. package/src/auth/__tests__/tokens.test.ts +274 -0
  6. package/src/auth/__tests__/verification.test.ts +274 -0
  7. package/src/auth/index.ts +76 -0
  8. package/src/auth/login.ts +225 -0
  9. package/src/auth/password.ts +120 -0
  10. package/src/auth/reset.ts +243 -0
  11. package/src/auth/tokens.ts +612 -0
  12. package/src/auth/verification.ts +253 -0
  13. package/src/bundler/__tests__/cli-bench-utils.test.ts +149 -0
  14. package/src/bundler/__tests__/cold-start.test.ts +504 -0
  15. package/src/bundler/__tests__/csp-nonce.test.ts +278 -0
  16. package/src/bundler/__tests__/dev-reliability.test.ts +619 -0
  17. package/src/bundler/__tests__/extended-watch.test.ts +710 -0
  18. package/src/bundler/__tests__/fast-refresh.test.ts +596 -0
  19. package/src/bundler/__tests__/hdr.test.ts +353 -0
  20. package/src/bundler/__tests__/hmr-client.test.ts +532 -0
  21. package/src/bundler/__tests__/manifest-schema.test.ts +266 -0
  22. package/src/bundler/__tests__/prod-smoke.test.ts +138 -0
  23. package/src/bundler/__tests__/slot-dispatch.test.ts +573 -0
  24. package/src/bundler/__tests__/url-cap-and-slot-regex.test.ts +286 -0
  25. package/src/bundler/__tests__/vendor-cache.test.ts +455 -0
  26. package/src/bundler/build.test.ts +8 -1
  27. package/src/bundler/build.ts +310 -18
  28. package/src/bundler/css.ts +326 -323
  29. package/src/bundler/dev.ts +1611 -59
  30. package/src/bundler/fast-refresh-plugin.ts +307 -0
  31. package/src/bundler/hmr-types.ts +252 -0
  32. package/src/bundler/manifest-schema.ts +301 -0
  33. package/src/bundler/safe-build.test.ts +128 -0
  34. package/src/bundler/safe-build.ts +77 -0
  35. package/src/bundler/scenario-matrix.ts +229 -0
  36. package/src/bundler/types.ts +11 -0
  37. package/src/bundler/vendor-cache-types.ts +130 -0
  38. package/src/bundler/vendor-cache.ts +526 -0
  39. package/src/client/router.ts +214 -56
  40. package/src/db/__tests__/db.test.ts +485 -0
  41. package/src/db/index.ts +513 -0
  42. package/src/db/migrations/__tests__/runner.test.ts +661 -0
  43. package/src/db/migrations/history-table.ts +345 -0
  44. package/src/db/migrations/lock.ts +269 -0
  45. package/src/db/migrations/runner.ts +633 -0
  46. package/src/desktop/__tests__/smoke.test.ts +100 -0
  47. package/src/desktop/__tests__/window.test.ts +172 -0
  48. package/src/desktop/__tests__/worker.test.ts +266 -0
  49. package/src/desktop/index.ts +43 -0
  50. package/src/desktop/types.ts +158 -0
  51. package/src/desktop/window.ts +492 -0
  52. package/src/desktop/worker.ts +180 -0
  53. package/src/email/__tests__/email.test.ts +355 -0
  54. package/src/email/index.ts +282 -0
  55. package/src/email/resend.ts +163 -0
  56. package/src/email/smtp.ts +64 -0
  57. package/src/filling/__tests__/session-sqlite.test.ts +454 -0
  58. package/src/filling/context.ts +72 -78
  59. package/src/filling/cookie-codec.ts +299 -0
  60. package/src/filling/deps.ts +25 -1
  61. package/src/filling/filling.ts +28 -3
  62. package/src/filling/session-sqlite.ts +617 -0
  63. package/src/filling/session.ts +265 -216
  64. package/src/guard/decision-memory.test.ts +52 -22
  65. package/src/id/__tests__/id.test.ts +120 -0
  66. package/src/id/index.ts +105 -0
  67. package/src/kitchen/index.ts +2 -2
  68. package/src/kitchen/kitchen-handler.ts +86 -0
  69. package/src/kitchen/stream/activity-sse.ts +2 -1
  70. package/src/middleware/csrf.ts +328 -0
  71. package/src/middleware/index.ts +40 -0
  72. package/src/middleware/oauth/__tests__/oauth.test.ts +574 -0
  73. package/src/middleware/oauth/index.ts +505 -0
  74. package/src/middleware/oauth/providers.ts +115 -0
  75. package/src/middleware/rate-limit/__tests__/rate-limit.test.ts +642 -0
  76. package/src/middleware/rate-limit/index.ts +522 -0
  77. package/src/middleware/rate-limit/sqlite-store.ts +382 -0
  78. package/src/middleware/secure/__tests__/secure.test.ts +360 -0
  79. package/src/middleware/secure/csp.ts +193 -0
  80. package/src/middleware/secure/index.ts +417 -0
  81. package/src/middleware/session.ts +174 -0
  82. package/src/observability/event-bus.ts +81 -79
  83. package/src/paths.ts +37 -0
  84. package/src/perf/hmr-markers.ts +215 -0
  85. package/src/perf/index.ts +104 -0
  86. package/src/resource/__tests__/generator.test.ts +603 -2
  87. package/src/resource/ddl/__tests__/diff.test.ts +639 -0
  88. package/src/resource/ddl/__tests__/emit.test.ts +799 -0
  89. package/src/resource/ddl/__tests__/snapshot.test.ts +499 -0
  90. package/src/resource/ddl/diff.ts +392 -0
  91. package/src/resource/ddl/emit.ts +548 -0
  92. package/src/resource/ddl/persistence-types.ts +218 -0
  93. package/src/resource/ddl/snapshot.ts +447 -0
  94. package/src/resource/ddl/type-map.ts +223 -0
  95. package/src/resource/ddl/types.ts +232 -0
  96. package/src/resource/generator-repo.ts +610 -0
  97. package/src/resource/generator-schema.ts +476 -0
  98. package/src/resource/generator.ts +117 -1
  99. package/src/resource/index.ts +17 -1
  100. package/src/resource/schema.ts +30 -0
  101. package/src/router/fs-scanner.ts +3 -0
  102. package/src/runtime/__tests__/error-boundary-redaction.test.ts +141 -0
  103. package/src/runtime/__tests__/hdr-client.test.ts +223 -0
  104. package/src/runtime/__tests__/http-errors.test.ts +117 -0
  105. package/src/runtime/__tests__/not-found.test.ts +152 -0
  106. package/src/runtime/boundary.tsx +21 -1
  107. package/src/runtime/fast-refresh-runtime.ts +322 -0
  108. package/src/runtime/fast-refresh-types.ts +128 -0
  109. package/src/runtime/hmr-client.ts +409 -0
  110. package/src/runtime/http-errors.ts +113 -0
  111. package/src/runtime/index.ts +6 -0
  112. package/src/runtime/logger.ts +678 -677
  113. package/src/runtime/not-found.ts +93 -0
  114. package/src/runtime/redirect.ts +133 -0
  115. package/src/runtime/server.ts +518 -20
  116. package/src/runtime/ssr.ts +340 -10
  117. package/src/runtime/streaming-ssr.ts +222 -19
  118. package/src/scheduler/__tests__/scheduler.test.ts +514 -0
  119. package/src/scheduler/index.ts +343 -0
  120. package/src/storage/s3/__tests__/s3.test.ts +479 -0
  121. package/src/storage/s3/index.ts +412 -0
  122. package/src/testing/index.ts +58 -0
@@ -0,0 +1,353 @@
1
+ /**
2
+ * Phase 7.2 R1 Agent B — HDR (Hot Data Revalidation) server-side tests.
3
+ *
4
+ * Coverage:
5
+ * 1. Slot file detection — correctly identifies `.slot.ts` and
6
+ * `.slot.tsx` files, rejects other extensions.
7
+ * 2. routeId mapping — given a manifest with `slotModule`, the
8
+ * normalized path comparison finds the owning route and
9
+ * correctly returns `null` for unrelated paths.
10
+ * 3. rebuildId monotonicity — `broadcastVite` assigns envelope ids
11
+ * that strictly increase, so the replay buffer is compatible
12
+ * with HDR custom events.
13
+ * 4. timestamp freshness — emitted HDR payload timestamps are
14
+ * within 1000 ms of Date.now().
15
+ * 5. HMR server broadcasts custom slot-refetch over the wire — the
16
+ * payload reaches connected clients with the expected shape.
17
+ * 6. HDR opt-out env — when `MANDU_HDR=0` the helper indicates HDR
18
+ * disabled so the caller falls back to legacy reload path.
19
+ *
20
+ * These tests exercise the PURE contract: path normalization, lookup
21
+ * semantics, and the HMR server's broadcast shape. The browser-side
22
+ * fetch + startTransition path lives in `hdr-client.test.ts` (pure
23
+ * unit) and `fast-refresh.spec.ts` (Playwright e2e).
24
+ *
25
+ * References:
26
+ * docs/bun/phase-7-2-team-plan.md §3 Agent B
27
+ * packages/core/src/bundler/hmr-types.ts — HDRPayload
28
+ * packages/cli/src/commands/dev.ts — findRouteIdForSlot / isSlotFile
29
+ */
30
+
31
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
32
+ import path from "path";
33
+ import { tmpdir } from "os";
34
+ import {
35
+ createHMRServer,
36
+ _testOnly_normalizeFsPath,
37
+ type HMRServer,
38
+ } from "../dev";
39
+ import type { RoutesManifest } from "../../spec/schema";
40
+ import type { HDRPayload, ViteHMRPayload } from "../hmr-types";
41
+ import { PORTS } from "../../constants";
42
+
43
+ // -----------------------------------------------------------------------------
44
+ // Mirrored helpers — we reproduce the CLI helper logic here so the test
45
+ // file doesn't have to import from `@mandujs/cli` (circular package
46
+ // dependency). Any divergence across the two implementations is a test
47
+ // failure we want, not a silent skew.
48
+ // -----------------------------------------------------------------------------
49
+
50
+ function isSlotFile(filePath: string): boolean {
51
+ return filePath.endsWith(".slot.ts") || filePath.endsWith(".slot.tsx");
52
+ }
53
+
54
+ function normalizeCompareFactory(rootDir: string) {
55
+ return (p: string): string => {
56
+ const resolved = path.resolve(rootDir, p).replace(/\\/g, "/");
57
+ return process.platform === "win32" ? resolved.toLowerCase() : resolved;
58
+ };
59
+ }
60
+
61
+ function findRouteIdForSlot(
62
+ manifest: RoutesManifest,
63
+ rootDir: string,
64
+ filePath: string,
65
+ ): string | null {
66
+ const normalizeCompare = normalizeCompareFactory(rootDir);
67
+ const target = normalizeCompare(filePath);
68
+ for (const route of manifest.routes) {
69
+ if (!route.slotModule) continue;
70
+ if (normalizeCompare(route.slotModule) === target) {
71
+ return route.id;
72
+ }
73
+ }
74
+ return null;
75
+ }
76
+
77
+ // -----------------------------------------------------------------------------
78
+ // Fixtures
79
+ // -----------------------------------------------------------------------------
80
+
81
+ function buildManifest(): RoutesManifest {
82
+ return {
83
+ version: 1,
84
+ routes: [
85
+ {
86
+ id: "home",
87
+ kind: "page",
88
+ pattern: "/",
89
+ module: "app/page.tsx",
90
+ componentModule: "app/page.tsx",
91
+ slotModule: "app/page.slot.ts",
92
+ },
93
+ {
94
+ id: "dashboard",
95
+ kind: "page",
96
+ pattern: "/dashboard",
97
+ module: "app/dashboard/page.tsx",
98
+ componentModule: "app/dashboard/page.tsx",
99
+ slotModule: "app/dashboard/page.slot.ts",
100
+ },
101
+ {
102
+ id: "about",
103
+ kind: "page",
104
+ pattern: "/about",
105
+ module: "app/about/page.tsx",
106
+ componentModule: "app/about/page.tsx",
107
+ // no slotModule — proves the falsy-skip branch
108
+ },
109
+ ],
110
+ } as RoutesManifest;
111
+ }
112
+
113
+ // -----------------------------------------------------------------------------
114
+ // Section A — Slot detection + routeId mapping
115
+ // -----------------------------------------------------------------------------
116
+
117
+ describe("Phase 7.2 Agent B — slot detection", () => {
118
+ test("[1] isSlotFile accepts .slot.ts and .slot.tsx", () => {
119
+ expect(isSlotFile("app/page.slot.ts")).toBe(true);
120
+ expect(isSlotFile("app/page.slot.tsx")).toBe(true);
121
+ // Full absolute paths also work (the helper only checks the suffix).
122
+ expect(
123
+ isSlotFile(path.resolve(tmpdir(), "proj/app/dashboard/page.slot.ts")),
124
+ ).toBe(true);
125
+ });
126
+
127
+ test("[1b] isSlotFile rejects non-slot files", () => {
128
+ expect(isSlotFile("app/page.tsx")).toBe(false);
129
+ expect(isSlotFile("app/page.ts")).toBe(false);
130
+ expect(isSlotFile("app/page.client.tsx")).toBe(false);
131
+ expect(isSlotFile("app/layout.tsx")).toBe(false);
132
+ // Files with ".slot" in the NAME but not as suffix must not match.
133
+ expect(isSlotFile("app/slot-like.ts")).toBe(false);
134
+ expect(isSlotFile("app/not-a.slot.js")).toBe(false);
135
+ });
136
+ });
137
+
138
+ describe("Phase 7.2 Agent B — findRouteIdForSlot", () => {
139
+ test("[2] returns the owning routeId when the slot path matches exactly", () => {
140
+ const rootDir = path.join(tmpdir(), "mandu-hdr-fixture-1");
141
+ const manifest = buildManifest();
142
+ const absSlotPath = path.resolve(rootDir, "app/page.slot.ts");
143
+ expect(findRouteIdForSlot(manifest, rootDir, absSlotPath)).toBe("home");
144
+
145
+ const absDashboard = path.resolve(rootDir, "app/dashboard/page.slot.ts");
146
+ expect(findRouteIdForSlot(manifest, rootDir, absDashboard)).toBe("dashboard");
147
+ });
148
+
149
+ test("[2b] normalizes Windows backslashes + forward-slashes identically", () => {
150
+ const rootDir = path.join(tmpdir(), "mandu-hdr-fixture-2");
151
+ const manifest = buildManifest();
152
+ // Build the path with backslashes regardless of platform — the
153
+ // helper must normalize it before comparing.
154
+ const backslashPath = path
155
+ .resolve(rootDir, "app/page.slot.ts")
156
+ .replace(/\//g, "\\");
157
+ const forwardPath = path
158
+ .resolve(rootDir, "app/page.slot.ts")
159
+ .replace(/\\/g, "/");
160
+ expect(findRouteIdForSlot(manifest, rootDir, backslashPath)).toBe("home");
161
+ expect(findRouteIdForSlot(manifest, rootDir, forwardPath)).toBe("home");
162
+ });
163
+
164
+ test("[2c] returns null when slot path doesn't match any route", () => {
165
+ const rootDir = path.join(tmpdir(), "mandu-hdr-fixture-3");
166
+ const manifest = buildManifest();
167
+ const unrelated = path.resolve(rootDir, "app/orphan.slot.ts");
168
+ expect(findRouteIdForSlot(manifest, rootDir, unrelated)).toBe(null);
169
+ });
170
+
171
+ test("[2d] skips routes without slotModule — doesn't throw on falsy", () => {
172
+ const rootDir = path.join(tmpdir(), "mandu-hdr-fixture-4");
173
+ // Manifest with only one route, no slotModule.
174
+ const manifest: RoutesManifest = {
175
+ version: 1,
176
+ routes: [
177
+ {
178
+ id: "home",
179
+ kind: "page",
180
+ pattern: "/",
181
+ module: "app/page.tsx",
182
+ componentModule: "app/page.tsx",
183
+ },
184
+ ],
185
+ } as RoutesManifest;
186
+ const anySlot = path.resolve(rootDir, "app/page.slot.ts");
187
+ expect(findRouteIdForSlot(manifest, rootDir, anySlot)).toBe(null);
188
+ });
189
+
190
+ test("[4] after handler re-registration, subsequent slot change still resolves", () => {
191
+ // Simulates the dev.ts flow: the same manifest instance survives
192
+ // across multiple slot edits. The helper must be stable — no
193
+ // internal mutation.
194
+ const rootDir = path.join(tmpdir(), "mandu-hdr-fixture-5");
195
+ const manifest = buildManifest();
196
+ const slotPath = path.resolve(rootDir, "app/page.slot.ts");
197
+ for (let i = 0; i < 3; i++) {
198
+ expect(findRouteIdForSlot(manifest, rootDir, slotPath)).toBe("home");
199
+ }
200
+ });
201
+ });
202
+
203
+ // -----------------------------------------------------------------------------
204
+ // Section B — HMR server broadcast of slot-refetch
205
+ // -----------------------------------------------------------------------------
206
+
207
+ function pickPort(): number {
208
+ return 40000 + Math.floor(Math.random() * 10000);
209
+ }
210
+
211
+ describe("Phase 7.2 Agent B — slot-refetch broadcast", () => {
212
+ let server: HMRServer | null = null;
213
+ let hmrPort = 0;
214
+
215
+ beforeEach(() => {
216
+ const basePort = pickPort();
217
+ hmrPort = basePort + PORTS.HMR_OFFSET;
218
+ server = createHMRServer(basePort);
219
+ });
220
+
221
+ afterEach(() => {
222
+ server?.close();
223
+ server = null;
224
+ });
225
+
226
+ test("[3] broadcastVite custom slot-refetch receives monotonic envelope id", () => {
227
+ const e1 = server!.broadcastVite({
228
+ type: "custom",
229
+ event: "mandu:slot-refetch",
230
+ data: { routeId: "home", slotPath: "app/page.slot.ts", timestamp: Date.now() },
231
+ });
232
+ const e2 = server!.broadcastVite({
233
+ type: "custom",
234
+ event: "mandu:slot-refetch",
235
+ data: { routeId: "home", slotPath: "app/page.slot.ts", timestamp: Date.now() },
236
+ });
237
+ expect(e2.id).toBeGreaterThan(e1.id);
238
+ });
239
+
240
+ test("[5] custom slot-refetch reaches the client with expected shape", async () => {
241
+ const ws = new WebSocket(`ws://localhost:${hmrPort}/`);
242
+ await new Promise<void>((resolve, reject) => {
243
+ ws.addEventListener("open", () => resolve(), { once: true });
244
+ ws.addEventListener("error", (e) => reject(e as unknown as Error), {
245
+ once: true,
246
+ });
247
+ });
248
+
249
+ const messages: Array<{
250
+ type: string;
251
+ payload?: { type: string; event?: string; data?: unknown };
252
+ }> = [];
253
+ ws.addEventListener("message", (ev) => {
254
+ try {
255
+ messages.push(JSON.parse(String((ev as MessageEvent).data)));
256
+ } catch {
257
+ // ignore
258
+ }
259
+ });
260
+
261
+ // Drain the 'connected' greeting if any.
262
+ await new Promise((r) => setTimeout(r, 50));
263
+
264
+ const payload: ViteHMRPayload = {
265
+ type: "custom",
266
+ event: "mandu:slot-refetch",
267
+ data: {
268
+ routeId: "dashboard",
269
+ slotPath: "app/dashboard/page.slot.ts",
270
+ timestamp: Date.now(),
271
+ },
272
+ };
273
+ server!.broadcastVite(payload);
274
+
275
+ // Poll for the message to land.
276
+ const deadline = Date.now() + 2000;
277
+ while (Date.now() < deadline) {
278
+ if (messages.some((m) => m.type === "vite" && m.payload?.type === "custom")) {
279
+ break;
280
+ }
281
+ await new Promise((r) => setTimeout(r, 20));
282
+ }
283
+ const custom = messages.find(
284
+ (m) => m.type === "vite" && m.payload?.type === "custom",
285
+ );
286
+ expect(custom).toBeDefined();
287
+ expect(custom?.payload?.event).toBe("mandu:slot-refetch");
288
+ expect((custom?.payload?.data as { routeId: string }).routeId).toBe(
289
+ "dashboard",
290
+ );
291
+ ws.close();
292
+ });
293
+
294
+ test("[6] HDR disabled via env (MANDU_HDR=0) — helper check", () => {
295
+ // The helper itself is a one-liner but we lock the semantics as a
296
+ // regression guard. The CLI uses `process.env.MANDU_HDR !== "0"`
297
+ // so any non-"0" value (including unset) enables HDR.
298
+ const prev = process.env.MANDU_HDR;
299
+ try {
300
+ process.env.MANDU_HDR = "0";
301
+ expect(process.env.MANDU_HDR !== "0").toBe(false);
302
+ process.env.MANDU_HDR = "1";
303
+ expect(process.env.MANDU_HDR !== "0").toBe(true);
304
+ delete process.env.MANDU_HDR;
305
+ expect(process.env.MANDU_HDR !== "0").toBe(true);
306
+ } finally {
307
+ if (prev === undefined) {
308
+ delete process.env.MANDU_HDR;
309
+ } else {
310
+ process.env.MANDU_HDR = prev;
311
+ }
312
+ }
313
+ });
314
+ });
315
+
316
+ // -----------------------------------------------------------------------------
317
+ // Section C — HDRPayload shape contract
318
+ // -----------------------------------------------------------------------------
319
+
320
+ describe("Phase 7.2 Agent B — HDRPayload shape", () => {
321
+ test("[7] HDRPayload type includes routeId / slotPath / rebuildId / timestamp", () => {
322
+ // Compile-time assertion: if the HDRPayload type shape changes in
323
+ // hmr-types.ts without updating this test, TypeScript compilation
324
+ // fails. Run-time assertion just locks the value shape.
325
+ const payload: HDRPayload = {
326
+ type: "slot-refetch",
327
+ routeId: "home",
328
+ slotPath: "app/page.slot.ts",
329
+ rebuildId: 1,
330
+ timestamp: Date.now(),
331
+ };
332
+ expect(payload.type).toBe("slot-refetch");
333
+ expect(payload.routeId).toBe("home");
334
+ expect(payload.slotPath).toBe("app/page.slot.ts");
335
+ expect(typeof payload.rebuildId).toBe("number");
336
+ expect(typeof payload.timestamp).toBe("number");
337
+ expect(Math.abs(payload.timestamp - Date.now())).toBeLessThan(1000);
338
+ });
339
+
340
+ test("[8] path normalization round-trip through _testOnly_normalizeFsPath", () => {
341
+ // The `findRouteIdForSlot` helper must use the same normalization
342
+ // the bundler's `startDevBundler` manifest iteration uses so both
343
+ // sides of the comparison produce identical keys.
344
+ const abs = path.resolve(tmpdir(), "proj/app/page.slot.ts");
345
+ const n1 = _testOnly_normalizeFsPath(abs);
346
+ expect(n1.endsWith("/app/page.slot.ts")).toBe(true);
347
+ expect(n1.includes("\\")).toBe(false);
348
+ // `.slot.tsx` variant normalizes identically (permitted by guard).
349
+ const abs2 = path.resolve(tmpdir(), "proj/app/page.slot.tsx");
350
+ const n2 = _testOnly_normalizeFsPath(abs2);
351
+ expect(n2.endsWith("/app/page.slot.tsx")).toBe(true);
352
+ });
353
+ });