@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,266 @@
1
+ /**
2
+ * Phase 7.2 R1 Agent C (H2) — BundleManifest schema validation tests.
3
+ *
4
+ * Covers the primary Medium-severity finding from the Phase 7.1 audit
5
+ * (`docs/security/phase-7-1-audit.md` §M-02): tampered manifest injection
6
+ * via `shared.fastRefresh.{glue,runtime}` URL replacement. The schema
7
+ * now rejects such tampers at load time; the contract asserted below
8
+ * pins the precise shape so regressions fail fast.
9
+ *
10
+ * References:
11
+ * docs/security/phase-7-1-audit.md §M-02
12
+ * docs/bun/phase-7-2-team-plan.md §3 Agent C H2
13
+ * packages/core/src/bundler/manifest-schema.ts
14
+ */
15
+
16
+ import { describe, expect, test } from "bun:test";
17
+ import {
18
+ BundleManifestSchema,
19
+ MAX_MANIFEST_URL_LEN,
20
+ ManifestValidationError,
21
+ SAFE_MANDU_URL_REGEX,
22
+ isSafeManduUrl,
23
+ safeValidateBundleManifest,
24
+ validateBundleManifest,
25
+ } from "../manifest-schema";
26
+
27
+ /** Known-good manifest, used as the basis for mutation tests. */
28
+ const VALID_MANIFEST = {
29
+ version: 1,
30
+ buildTime: "2026-04-19T00:00:00.000Z",
31
+ env: "development" as const,
32
+ bundles: {
33
+ page: {
34
+ js: "/.mandu/client/page.js",
35
+ dependencies: [],
36
+ priority: "visible" as const,
37
+ },
38
+ },
39
+ shared: {
40
+ runtime: "/.mandu/client/_runtime.js",
41
+ vendor: "/.mandu/client/_vendor-react.js",
42
+ fastRefresh: {
43
+ runtime: "/.mandu/client/_fast-refresh-runtime.js",
44
+ glue: "/.mandu/client/_vendor-react-refresh.js",
45
+ },
46
+ },
47
+ };
48
+
49
+ // ═══════════════════════════════════════════════════════════════════
50
+ // Section A — primitive URL predicate
51
+ // ═══════════════════════════════════════════════════════════════════
52
+
53
+ describe("manifest-schema — isSafeManduUrl", () => {
54
+ test("[A1] accepts bundler-shape URLs", () => {
55
+ expect(isSafeManduUrl("/.mandu/client/page.js")).toBe(true);
56
+ expect(isSafeManduUrl("/.mandu/client/_vendor-react.js")).toBe(true);
57
+ expect(isSafeManduUrl("/.mandu/client/nested/island.js")).toBe(true);
58
+ expect(isSafeManduUrl("/.mandu/client/styles.css")).toBe(true);
59
+ });
60
+
61
+ test("[A2] rejects absolute URLs with protocol", () => {
62
+ expect(isSafeManduUrl("https://evil.example.com/steal.js")).toBe(false);
63
+ expect(isSafeManduUrl("http://evil.example.com/x.js")).toBe(false);
64
+ expect(isSafeManduUrl("data:application/javascript,alert(1)")).toBe(false);
65
+ expect(isSafeManduUrl("javascript:alert(1)")).toBe(false);
66
+ });
67
+
68
+ test("[A3] rejects path traversal", () => {
69
+ expect(isSafeManduUrl("/.mandu/client/../secret.js")).toBe(false);
70
+ expect(isSafeManduUrl("/.mandu/client/a/../b.js")).toBe(false);
71
+ expect(isSafeManduUrl("/.mandu/..//client/page.js")).toBe(false);
72
+ });
73
+
74
+ test("[A4] rejects empty / overlong / wrong-shape", () => {
75
+ expect(isSafeManduUrl("")).toBe(false);
76
+ expect(isSafeManduUrl("x".repeat(MAX_MANIFEST_URL_LEN + 1))).toBe(false);
77
+ expect(isSafeManduUrl("/not-mandu/client/page.js")).toBe(false);
78
+ expect(isSafeManduUrl("/.mandu/client/page.txt")).toBe(false);
79
+ expect(isSafeManduUrl("page.js")).toBe(false); // relative
80
+ });
81
+
82
+ test("[A5] rejects control / special characters", () => {
83
+ expect(isSafeManduUrl("/.mandu/client/page\n.js")).toBe(false);
84
+ expect(isSafeManduUrl("/.mandu/client/page<script>.js")).toBe(false);
85
+ expect(isSafeManduUrl('/.mandu/client/page".js')).toBe(false);
86
+ expect(isSafeManduUrl("/.mandu/client/page\\.js")).toBe(false);
87
+ });
88
+
89
+ test("[A6] non-string input returns false (Zod pre-check)", () => {
90
+ expect(isSafeManduUrl(123)).toBe(false);
91
+ expect(isSafeManduUrl(null)).toBe(false);
92
+ expect(isSafeManduUrl(undefined)).toBe(false);
93
+ expect(isSafeManduUrl({})).toBe(false);
94
+ });
95
+
96
+ test("[A7] SAFE_MANDU_URL_REGEX anchors correctly", () => {
97
+ // Must match start AND end — no embedded matches permitted
98
+ expect(SAFE_MANDU_URL_REGEX.test("/.mandu/client/page.js")).toBe(true);
99
+ expect(SAFE_MANDU_URL_REGEX.test("/prefix/.mandu/client/page.js")).toBe(false);
100
+ expect(SAFE_MANDU_URL_REGEX.test("/.mandu/client/page.js?t=1")).toBe(false);
101
+ });
102
+ });
103
+
104
+ // ═══════════════════════════════════════════════════════════════════
105
+ // Section B — full manifest validation
106
+ // ═══════════════════════════════════════════════════════════════════
107
+
108
+ describe("manifest-schema — validateBundleManifest", () => {
109
+ test("[B1] accepts a valid dev manifest", () => {
110
+ const ok = validateBundleManifest(VALID_MANIFEST);
111
+ expect(ok.version).toBe(1);
112
+ expect(ok.shared.fastRefresh?.glue).toBe("/.mandu/client/_vendor-react-refresh.js");
113
+ });
114
+
115
+ test("[B2] rejects tampered shared.fastRefresh.glue URL (https://evil.example.com)", () => {
116
+ const tampered = {
117
+ ...VALID_MANIFEST,
118
+ shared: {
119
+ ...VALID_MANIFEST.shared,
120
+ fastRefresh: {
121
+ runtime: "/.mandu/client/_fast-refresh-runtime.js",
122
+ glue: "https://evil.example.com/steal.js",
123
+ },
124
+ },
125
+ };
126
+ expect(() => validateBundleManifest(tampered)).toThrow(ManifestValidationError);
127
+ });
128
+
129
+ test("[B3] rejects tampered shared.fastRefresh.runtime URL (data: scheme)", () => {
130
+ const tampered = {
131
+ ...VALID_MANIFEST,
132
+ shared: {
133
+ ...VALID_MANIFEST.shared,
134
+ fastRefresh: {
135
+ runtime: "data:application/javascript;base64,YWxlcnQoMSk=",
136
+ glue: "/.mandu/client/_vendor-react-refresh.js",
137
+ },
138
+ },
139
+ };
140
+ expect(() => validateBundleManifest(tampered)).toThrow(ManifestValidationError);
141
+ });
142
+
143
+ test("[B4] rejects missing version", () => {
144
+ const invalid = { ...VALID_MANIFEST };
145
+ delete (invalid as Record<string, unknown>).version;
146
+ expect(() => validateBundleManifest(invalid)).toThrow(ManifestValidationError);
147
+ });
148
+
149
+ test("[B5] rejects missing shared block", () => {
150
+ const invalid = { ...VALID_MANIFEST };
151
+ delete (invalid as Record<string, unknown>).shared;
152
+ expect(() => validateBundleManifest(invalid)).toThrow(ManifestValidationError);
153
+ });
154
+
155
+ test("[B6] rejects unknown top-level keys (strict mode catches schema drift)", () => {
156
+ const withExtra = {
157
+ ...VALID_MANIFEST,
158
+ evilField: { steal: "everything" },
159
+ };
160
+ expect(() => validateBundleManifest(withExtra)).toThrow(ManifestValidationError);
161
+ });
162
+
163
+ test("[B7] rejects negative / zero / non-integer version", () => {
164
+ expect(() => validateBundleManifest({ ...VALID_MANIFEST, version: 0 })).toThrow();
165
+ expect(() => validateBundleManifest({ ...VALID_MANIFEST, version: -1 })).toThrow();
166
+ expect(() => validateBundleManifest({ ...VALID_MANIFEST, version: 1.5 })).toThrow();
167
+ });
168
+
169
+ test("[B8] rejects traversal in bundles[].js", () => {
170
+ const bad = {
171
+ ...VALID_MANIFEST,
172
+ bundles: {
173
+ page: {
174
+ js: "/.mandu/client/../evil.js",
175
+ dependencies: [],
176
+ priority: "visible" as const,
177
+ },
178
+ },
179
+ };
180
+ expect(() => validateBundleManifest(bad)).toThrow(ManifestValidationError);
181
+ });
182
+
183
+ test("[B9] rejects overlong URL in shared.runtime", () => {
184
+ const bad = {
185
+ ...VALID_MANIFEST,
186
+ shared: {
187
+ ...VALID_MANIFEST.shared,
188
+ runtime: "/.mandu/client/" + "a".repeat(MAX_MANIFEST_URL_LEN) + ".js",
189
+ },
190
+ };
191
+ expect(() => validateBundleManifest(bad)).toThrow(ManifestValidationError);
192
+ });
193
+
194
+ test("[B10] accepts manifest without optional fastRefresh (prod shape)", () => {
195
+ const prodish = {
196
+ ...VALID_MANIFEST,
197
+ env: "production" as const,
198
+ shared: {
199
+ runtime: VALID_MANIFEST.shared.runtime,
200
+ vendor: VALID_MANIFEST.shared.vendor,
201
+ // no fastRefresh
202
+ },
203
+ };
204
+ const ok = validateBundleManifest(prodish);
205
+ expect(ok.shared.fastRefresh).toBeUndefined();
206
+ });
207
+
208
+ test("[B11] ManifestValidationError carries structured issue list", () => {
209
+ const tampered = {
210
+ ...VALID_MANIFEST,
211
+ shared: {
212
+ ...VALID_MANIFEST.shared,
213
+ fastRefresh: {
214
+ runtime: "https://evil.example.com/r.js",
215
+ glue: "https://evil.example.com/g.js",
216
+ },
217
+ },
218
+ };
219
+ try {
220
+ validateBundleManifest(tampered);
221
+ throw new Error("should have thrown");
222
+ } catch (err) {
223
+ expect(err).toBeInstanceOf(ManifestValidationError);
224
+ const verr = err as ManifestValidationError;
225
+ expect(verr.issues.length).toBeGreaterThanOrEqual(2);
226
+ // Both tampered URLs must be reported
227
+ const paths = verr.issues.map((i) => i.path).join(" ");
228
+ expect(paths).toContain("shared.fastRefresh.runtime");
229
+ expect(paths).toContain("shared.fastRefresh.glue");
230
+ }
231
+ });
232
+ });
233
+
234
+ // ═══════════════════════════════════════════════════════════════════
235
+ // Section C — safeValidateBundleManifest (non-throwing)
236
+ // ═══════════════════════════════════════════════════════════════════
237
+
238
+ describe("manifest-schema — safeValidateBundleManifest", () => {
239
+ test("[C1] ok:true with a valid manifest", () => {
240
+ const r = safeValidateBundleManifest(VALID_MANIFEST);
241
+ expect(r.ok).toBe(true);
242
+ if (r.ok) {
243
+ expect(r.manifest.shared.fastRefresh?.glue).toBe(
244
+ "/.mandu/client/_vendor-react-refresh.js",
245
+ );
246
+ }
247
+ });
248
+
249
+ test("[C2] ok:false with issues list on invalid manifest", () => {
250
+ const bad = { ...VALID_MANIFEST, version: "not a number" as unknown };
251
+ const r = safeValidateBundleManifest(bad);
252
+ expect(r.ok).toBe(false);
253
+ if (!r.ok) {
254
+ expect(r.issues.length).toBeGreaterThan(0);
255
+ expect(r.issues[0].path).toBe("version");
256
+ }
257
+ });
258
+
259
+ test("[C3] BundleManifestSchema.safeParse matches the public validator", () => {
260
+ // Sanity: both paths agree on accept/reject for the same input
261
+ const rp = BundleManifestSchema.safeParse(VALID_MANIFEST);
262
+ expect(rp.success).toBe(true);
263
+ const rv = safeValidateBundleManifest(VALID_MANIFEST);
264
+ expect(rv.ok).toBe(true);
265
+ });
266
+ });
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Phase 7.2 R1 Agent C (H4) — production bundle smoke tests.
3
+ *
4
+ * Confirms the Phase 7.1 audit finding L-02 regression is closed: a
5
+ * production build (`minify: true`, no Fast Refresh plugin) must NEVER
6
+ * leak the dev-only symbols that would otherwise install Fast Refresh
7
+ * stubs on `window` (`$RefreshReg$` / `$RefreshSig$` / `__MANDU_HMR__`)
8
+ * or pull in the vendor runtime (`_vendor-react-refresh`).
9
+ *
10
+ * We assert at two layers:
11
+ * (A) the generated output JS bundle is free of the dev symbols
12
+ * (B) the returned manifest has `shared.fastRefresh === undefined`
13
+ *
14
+ * Both layers matter because (A) can drift independently of (B) if a
15
+ * future refactor inlines a symbol name into a comment or data literal.
16
+ *
17
+ * References:
18
+ * docs/security/phase-7-1-audit.md §3 L-02
19
+ * docs/bun/phase-7-2-team-plan.md §3 Agent C H4
20
+ */
21
+
22
+ import { describe, expect, test, beforeAll, afterAll } from "bun:test";
23
+ import { mkdtemp, mkdir, rm, writeFile } from "fs/promises";
24
+ import fs from "fs/promises";
25
+ import { tmpdir } from "os";
26
+ import path from "path";
27
+ import { fastRefreshPlugin } from "../fast-refresh-plugin";
28
+
29
+ /**
30
+ * Strings we treat as dev-only Fast Refresh fingerprints. Any of them
31
+ * appearing in a prod bundle is a leak. We keep the list small and
32
+ * precise so we don't false-flag legitimate unrelated code.
33
+ */
34
+ const DEV_LEAK_FINGERPRINTS = [
35
+ "$RefreshReg$",
36
+ "$RefreshSig$",
37
+ "__MANDU_HMR__",
38
+ "_vendor-react-refresh",
39
+ "_fast-refresh-runtime",
40
+ ] as const;
41
+
42
+ // Integration tests that spin up Bun.build are gated on CI flaky-matrix
43
+ // environments. See `fast-refresh.test.ts` for the same pattern.
44
+ describe.skipIf(process.env.MANDU_SKIP_BUNDLER_TESTS === "1")(
45
+ "prod-smoke — no Fast Refresh leak in production bundles",
46
+ () => {
47
+ let rootDir = "";
48
+
49
+ beforeAll(async () => {
50
+ rootDir = await mkdtemp(path.join(tmpdir(), "mandu-prod-smoke-"));
51
+ // Minimal island source — no JSX runtime, no imports, small AST
52
+ await writeFile(
53
+ path.join(rootDir, "counter.client.tsx"),
54
+ `import { useState } from 'react';
55
+ export default function Counter() {
56
+ const [count, setCount] = useState(0);
57
+ return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
58
+ }
59
+ `,
60
+ "utf-8",
61
+ );
62
+ });
63
+
64
+ afterAll(async () => {
65
+ if (rootDir) await rm(rootDir, { recursive: true, force: true });
66
+ });
67
+
68
+ test("[S1] production build output contains no dev-only Fast Refresh symbols", async () => {
69
+ // Prod path: `reactFastRefresh` off, plugin `disabled: true`,
70
+ // minify on. Matches the config `buildClientBundles` uses when
71
+ // `options.minify === true`.
72
+ const result = await Bun.build({
73
+ entrypoints: [path.join(rootDir, "counter.client.tsx")],
74
+ target: "browser",
75
+ minify: true,
76
+ plugins: [fastRefreshPlugin({ disabled: true })],
77
+ external: ["react", "react-dom", "react/jsx-dev-runtime", "react/jsx-runtime"],
78
+ });
79
+ expect(result.success).toBe(true);
80
+
81
+ const src = await result.outputs[0]!.text();
82
+ for (const fingerprint of DEV_LEAK_FINGERPRINTS) {
83
+ expect(src.includes(fingerprint)).toBe(false);
84
+ }
85
+ });
86
+
87
+ test("[S2] prod build with reactFastRefresh=true explicitly disabled does not emit stubs", async () => {
88
+ // Defense-in-depth: even if a future config accident leaves
89
+ // `reactFastRefresh: false` off-by-default, the disabled plugin
90
+ // MUST still prevent injection. We pass reactFastRefresh: false
91
+ // here to represent the guaranteed-prod config.
92
+ const result = await Bun.build({
93
+ entrypoints: [path.join(rootDir, "counter.client.tsx")],
94
+ target: "browser",
95
+ minify: true,
96
+ reactFastRefresh: false,
97
+ plugins: [fastRefreshPlugin({ disabled: true })],
98
+ external: ["react", "react-dom", "react/jsx-dev-runtime", "react/jsx-runtime"],
99
+ });
100
+ expect(result.success).toBe(true);
101
+
102
+ const src = await result.outputs[0]!.text();
103
+ expect(src).not.toContain("$RefreshReg$");
104
+ expect(src).not.toContain("$RefreshSig$");
105
+ expect(src).not.toContain("__MANDU_HMR__");
106
+ });
107
+ },
108
+ );
109
+
110
+ // Pure manifest-shape tests run regardless of the bundler skip flag —
111
+ // they don't invoke Bun.build and are safe everywhere.
112
+ describe("prod-smoke — manifest shape invariants (no Bun.build)", () => {
113
+ test("[M1] buildClientBundles prod path would omit shared.fastRefresh", async () => {
114
+ // We cannot easily invoke `buildClientBundles` here without a
115
+ // full Mandu project scaffold, so we assert the contract
116
+ // semantically via the static build.ts source: the `fastRefresh`
117
+ // field is populated only when the vendor shim result has both
118
+ // `reactRefreshRuntime` and `fastRefreshRuntime`. In prod mode
119
+ // `buildVendorShims` is called with `isDev: false` which returns
120
+ // empty strings for those — unwinding to `undefined` in the
121
+ // ternary at `build.ts:1548-1554`.
122
+ //
123
+ // This test documents the invariant textually; the bundler-level
124
+ // integration tests (S1/S2 above) prove the output side.
125
+ const buildSource = await fs.readFile(
126
+ path.join(
127
+ import.meta.dir,
128
+ "..",
129
+ "build.ts",
130
+ ),
131
+ "utf-8",
132
+ );
133
+ // The precise construction MUST be guarded by runtime presence.
134
+ expect(buildSource).toContain("vendorResult.reactRefreshRuntime && vendorResult.fastRefreshRuntime");
135
+ // And MUST spread-conditionally include the field.
136
+ expect(buildSource).toContain("...(fastRefresh ? { fastRefresh } : {})");
137
+ });
138
+ });