@mandujs/core 0.53.3 → 0.54.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.
- package/README.md +654 -654
- package/package.json +2 -2
- package/src/a11y/__tests__/run-audit.test.ts +333 -333
- package/src/a11y/fix-hints.ts +76 -76
- package/src/a11y/index.ts +18 -18
- package/src/a11y/run-audit.ts +394 -394
- package/src/a11y/types.ts +125 -125
- package/src/auth/__tests__/login.test.ts +1 -1
- package/src/auth/__tests__/password.test.ts +122 -122
- package/src/auth/__tests__/tokens.test.ts +274 -274
- package/src/auth/__tests__/verification.test.ts +274 -274
- package/src/auth/index.ts +76 -76
- package/src/auth/login.ts +225 -225
- package/src/auth/password.ts +120 -120
- package/src/auth/reset.ts +243 -243
- package/src/auth/tokens.ts +612 -612
- package/src/auth/verification.ts +253 -253
- package/src/brain/__tests__/redactor.test.ts +94 -94
- package/src/brain/adapters/__tests__/_helpers.ts +83 -83
- package/src/brain/adapters/__tests__/anthropic-oauth.test.ts +196 -196
- package/src/brain/adapters/__tests__/chatgpt-auth.test.ts +193 -193
- package/src/brain/adapters/__tests__/openai-oauth.test.ts +209 -209
- package/src/brain/adapters/__tests__/resolver.test.ts +143 -143
- package/src/brain/adapters/anthropic-oauth.ts +1 -1
- package/src/brain/adapters/chatgpt-auth.ts +300 -300
- package/src/brain/adapters/index.ts +319 -319
- package/src/brain/adapters/oauth-flow.ts +439 -439
- package/src/brain/consent.ts +240 -240
- package/src/brain/credentials.ts +396 -396
- package/src/bundler/__tests__/build-runner.ts +113 -113
- package/src/bundler/__tests__/dev-reliability.test.ts +619 -619
- package/src/bundler/__tests__/extended-watch.test.ts +711 -711
- package/src/bundler/__tests__/fast-refresh.test.ts +10 -10
- package/src/bundler/__tests__/hdr.test.ts +353 -353
- package/src/bundler/__tests__/hmr-client.test.ts +532 -532
- package/src/bundler/__tests__/manifest-schema.test.ts +266 -266
- package/src/bundler/__tests__/prod-smoke.test.ts +138 -138
- package/src/bundler/__tests__/reverse-import-graph.test.ts +519 -519
- package/src/bundler/__tests__/slot-dispatch.test.ts +573 -573
- package/src/bundler/__tests__/url-cap-and-slot-regex.test.ts +286 -286
- package/src/bundler/__tests__/vendor-cache.test.ts +455 -455
- package/src/bundler/budget.ts +404 -404
- package/src/bundler/build.test.ts +179 -179
- package/src/bundler/build.ts +55 -55
- package/src/bundler/dev.ts +42 -42
- package/src/bundler/manifest-schema.ts +301 -301
- package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +263 -263
- package/src/bundler/plugins/__tests__/react-compiler-config.test.ts +83 -83
- package/src/bundler/plugins/__tests__/react-compiler-lint.test.ts +110 -110
- package/src/bundler/plugins/block-generated-imports.ts +155 -155
- package/src/bundler/plugins/index.ts +83 -83
- package/src/bundler/plugins/react-compiler-config.ts +108 -108
- package/src/bundler/plugins/react-compiler-lint.ts +253 -253
- package/src/bundler/plugins/react-compiler.ts +162 -162
- package/src/bundler/reverse-import-graph.ts +339 -339
- package/src/bundler/safe-build.test.ts +201 -201
- package/src/bundler/safe-build.ts +103 -103
- package/src/bundler/scenario-matrix.ts +229 -229
- package/src/bundler/types.ts +10 -10
- package/src/bundler/vendor-cache-types.ts +130 -130
- package/src/bundler/vendor-cache.ts +526 -526
- package/src/change/snapshot.ts +18 -18
- package/src/client/Form.tsx +105 -105
- package/src/client/Link.tsx +9 -9
- package/src/client/__tests__/use-sse.test.ts +153 -153
- package/src/client/globals.ts +1 -1
- package/src/client/hooks.ts +362 -362
- package/src/client/hydrate.ts +340 -340
- package/src/client/prefetch-helper.ts +55 -55
- package/src/client/router.ts +11 -11
- package/src/client/runtime.ts +47 -47
- package/src/client/serialize.ts +404 -404
- package/src/client/use-fetch.ts +6 -6
- package/src/client/use-head.ts +197 -197
- package/src/client/use-sse.ts +378 -378
- package/src/client/window-state.ts +101 -101
- package/src/components/Image.tsx +162 -162
- package/src/config/validate.ts +3 -3
- package/src/config/watcher.ts +311 -311
- package/src/constants.ts +40 -40
- package/src/content/collection.ts +8 -8
- package/src/content/content-layer.ts +7 -7
- package/src/content/data-store.ts +245 -245
- package/src/content/frontmatter.ts +189 -189
- package/src/content/loader-context.ts +171 -171
- package/src/content/loaders/api.ts +216 -216
- package/src/content/loaders/file.ts +172 -172
- package/src/content/loaders/glob.ts +253 -253
- package/src/content/loaders/index.ts +34 -34
- package/src/content/loaders/types.ts +137 -137
- package/src/content/meta-store.ts +209 -209
- package/src/content/prebuild.test.ts +571 -571
- package/src/content/prebuild.ts +636 -636
- package/src/content/schema.ts +20 -20
- package/src/content/sidebar.ts +630 -630
- package/src/content/slug.ts +110 -110
- package/src/content/types.ts +282 -282
- package/src/content/watcher.ts +135 -135
- package/src/contract/client-safe.test.ts +42 -42
- package/src/contract/client-safe.ts +114 -114
- package/src/contract/define.ts +11 -11
- package/src/contract/index.ts +1 -1
- package/src/contract/normalize.test.ts +276 -276
- package/src/contract/normalize.ts +410 -410
- package/src/contract/registry.test.ts +206 -206
- package/src/contract/route-helpers.ts +1 -1
- package/src/contract/rpc.ts +443 -443
- package/src/contract/schema.ts +48 -48
- package/src/contract/types.ts +58 -58
- package/src/db/__tests__/db.test.ts +482 -482
- package/src/db/index.ts +138 -138
- package/src/db/migrations/history-table.ts +345 -345
- package/src/db/migrations/index.ts +3 -3
- package/src/db/migrations/lock.ts +324 -324
- package/src/db/migrations/runner.ts +650 -650
- package/src/deploy/cache.ts +140 -140
- package/src/deploy/compile/vercel.ts +344 -344
- package/src/deploy/index.ts +87 -87
- package/src/deploy/inference/brain.ts +268 -268
- package/src/deploy/inference/context.ts +182 -182
- package/src/deploy/inference/filling-extract.ts +245 -245
- package/src/deploy/inference/heuristic.ts +182 -182
- package/src/deploy/intent.ts +173 -173
- package/src/deploy/plan.ts +178 -178
- package/src/design/__tests__/agents-link.test.ts +109 -109
- package/src/design/__tests__/extract-patch-diff.test.ts +265 -265
- package/src/design/__tests__/lint.test.ts +110 -110
- package/src/design/__tests__/parser.test.ts +195 -195
- package/src/design/__tests__/tailwind-theme.test.ts +229 -229
- package/src/design/agents-link.ts +165 -165
- package/src/design/diff.ts +138 -138
- package/src/design/extract.ts +285 -285
- package/src/design/index.ts +102 -102
- package/src/design/lint.ts +209 -209
- package/src/design/parser.ts +555 -555
- package/src/design/patch.ts +241 -241
- package/src/design/scaffold.ts +147 -147
- package/src/design/tailwind-theme.ts +441 -441
- package/src/design/types.ts +210 -210
- package/src/desktop/__tests__/webview-fallback.test.ts +254 -254
- package/src/desktop/__tests__/window.test.ts +248 -248
- package/src/desktop/__tests__/worker.test.ts +266 -266
- package/src/desktop/index.ts +43 -43
- package/src/desktop/types.ts +158 -158
- package/src/desktop/worker.ts +180 -180
- package/src/dev-error-overlay/__tests__/overlay-injector.test.ts +241 -241
- package/src/dev-error-overlay/index.ts +30 -30
- package/src/dev-error-overlay/overlay-injector.ts +243 -243
- package/src/dev-error-overlay/overlay-styles.ts +52 -52
- package/src/dev-error-overlay/types.ts +66 -66
- package/src/devtools/ai/context-builder.ts +375 -375
- package/src/devtools/ai/index.ts +25 -25
- package/src/devtools/ai/mcp-connector.ts +25 -25
- package/src/devtools/client/catchers/error-catcher.ts +344 -344
- package/src/devtools/client/catchers/index.ts +18 -18
- package/src/devtools/client/components/index.ts +39 -39
- package/src/devtools/client/components/mandu-character.tsx +331 -331
- package/src/devtools/client/components/overlay.tsx +368 -368
- package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
- package/src/devtools/client/components/panel/guard-panel.tsx +30 -30
- package/src/devtools/client/components/panel/islands-panel.tsx +320 -320
- package/src/devtools/client/components/panel/network-panel.tsx +291 -291
- package/src/devtools/client/components/panel/panel-container.tsx +500 -500
- package/src/devtools/client/components/panel/preview-panel.tsx +46 -46
- package/src/devtools/client/filters/context-filters.ts +282 -282
- package/src/devtools/client/filters/index.ts +16 -16
- package/src/devtools/client/index.ts +63 -63
- package/src/devtools/client/persistence.ts +335 -335
- package/src/devtools/hook/create-hook.ts +207 -207
- package/src/devtools/hook/index.ts +13 -13
- package/src/devtools/index.ts +439 -439
- package/src/devtools/init.ts +265 -265
- package/src/devtools/protocol.ts +237 -237
- package/src/devtools/server/index.ts +17 -17
- package/src/devtools/server/source-context.ts +450 -450
- package/src/devtools/types.ts +35 -35
- package/src/devtools/worker/index.ts +25 -25
- package/src/devtools/worker/redaction-worker.ts +233 -233
- package/src/devtools/worker/worker-manager.ts +410 -410
- package/src/diagnose/__tests__/checks.test.ts +451 -451
- package/src/diagnose/checks.ts +832 -832
- package/src/diagnose/index.ts +17 -17
- package/src/diagnose/run.ts +93 -93
- package/src/diagnose/types.ts +53 -53
- package/src/email/__tests__/email.test.ts +355 -355
- package/src/email/index.ts +282 -282
- package/src/email/smtp.ts +64 -64
- package/src/error/domains.ts +265 -265
- package/src/error/result.ts +60 -60
- package/src/error/types.ts +6 -6
- package/src/errors/extractor.ts +409 -409
- package/src/errors/index.ts +19 -19
- package/src/filling/__tests__/session-sqlite.test.ts +454 -454
- package/src/filling/auth.ts +308 -308
- package/src/filling/cookie-codec.ts +299 -299
- package/src/filling/deps.ts +265 -265
- package/src/filling/session-sqlite.ts +617 -617
- package/src/filling/sse.ts +5 -5
- package/src/filling/ws.ts +78 -78
- package/src/generator/index.ts +3 -3
- package/src/guard/__tests__/design-inline-class.test.ts +219 -219
- package/src/guard/__tests__/tsgolint-bridge.test.ts +347 -347
- package/src/guard/analyzer.ts +360 -360
- package/src/guard/auto-correct.ts +1 -1
- package/src/guard/contract-guard.ts +9 -9
- package/src/guard/define-rule.ts +243 -243
- package/src/guard/design-inline-class.ts +393 -393
- package/src/guard/file-type.test.ts +24 -24
- package/src/guard/graph.ts +1 -1
- package/src/guard/healing.ts +36 -36
- package/src/guard/presets/atomic.ts +70 -70
- package/src/guard/presets/clean.ts +77 -77
- package/src/guard/presets/fsd.ts +79 -79
- package/src/guard/presets/hexagonal.ts +68 -68
- package/src/guard/reporter.ts +442 -442
- package/src/guard/rule-presets.ts +379 -379
- package/src/guard/semantic-slots.ts +1 -1
- package/src/guard/suggestions.ts +358 -358
- package/src/guard/tsgolint-bridge.ts +512 -512
- package/src/guard/types.ts +348 -348
- package/src/guard/watcher.ts +405 -405
- package/src/i18n/define.ts +126 -126
- package/src/i18n/index.ts +52 -52
- package/src/i18n/message-registry.ts +173 -173
- package/src/i18n/types.ts +112 -112
- package/src/id/index.ts +105 -105
- package/src/index.ts +2 -2
- package/src/kitchen/api/agent-devtools-api.ts +779 -544
- package/src/kitchen/api/errors-grouping.ts +126 -0
- package/src/kitchen/kitchen-handler.ts +192 -63
- package/src/kitchen/kitchen-ui.ts +842 -464
- package/src/logging/index.ts +22 -22
- package/src/logging/transports.ts +365 -365
- package/src/middleware/bridge.ts +147 -147
- package/src/middleware/compose.ts +134 -134
- package/src/middleware/compress.ts +62 -62
- package/src/middleware/cors.ts +47 -47
- package/src/middleware/csrf.ts +328 -328
- package/src/middleware/define.ts +132 -132
- package/src/middleware/jwt.ts +134 -134
- package/src/middleware/logger.ts +58 -58
- package/src/middleware/oauth/__tests__/oauth.test.ts +1 -1
- package/src/middleware/oauth/index.ts +505 -505
- package/src/middleware/oauth/providers.ts +115 -115
- package/src/middleware/rate-limit/index.ts +522 -522
- package/src/middleware/rate-limit/sqlite-store.ts +382 -382
- package/src/middleware/scheduler-cron.ts +96 -96
- package/src/middleware/secure/__tests__/secure.test.ts +360 -360
- package/src/middleware/secure/csp.ts +193 -193
- package/src/middleware/session.ts +174 -174
- package/src/middleware/timeout.ts +55 -55
- package/src/observability/logger-adapter.ts +36 -36
- package/src/observability/sqlite-store.ts +254 -254
- package/src/openapi/generator.ts +1 -1
- package/src/openapi/openapi.test.ts +43 -43
- package/src/perf/__tests__/user-marks.test.ts +354 -354
- package/src/perf/index.ts +133 -133
- package/src/plugins/__tests__/lifecycle-integration.test.ts +272 -272
- package/src/plugins/__tests__/runner.test.ts +409 -409
- package/src/plugins/define.ts +124 -124
- package/src/plugins/examples/dep-check-plugin.ts +80 -80
- package/src/plugins/examples/prerender-cache-plugin.ts +111 -111
- package/src/plugins/examples/sitemap-plugin.ts +65 -65
- package/src/plugins/runner.ts +361 -361
- package/src/plugins/types.ts +368 -368
- package/src/report/index.ts +1 -1
- package/src/resource/__tests__/generator.test.ts +32 -32
- package/src/resource/ddl/__tests__/diff.test.ts +639 -639
- package/src/resource/ddl/__tests__/emit.test.ts +823 -823
- package/src/resource/ddl/__tests__/snapshot.test.ts +499 -499
- package/src/resource/ddl/emit.ts +559 -559
- package/src/resource/ddl/persistence-types.ts +218 -218
- package/src/resource/ddl/type-map.ts +223 -223
- package/src/resource/ddl/types.ts +232 -232
- package/src/resource/generator-repo.ts +630 -630
- package/src/resource/schema.ts +1 -1
- package/src/router/fs-patterns.test.ts +96 -96
- package/src/router/fs-routes.ts +6 -6
- package/src/routes/index.ts +74 -74
- package/src/routes/metadata-routes.ts +427 -427
- package/src/routes/types.ts +341 -341
- package/src/runtime/__tests__/error-boundary-redaction.test.ts +141 -141
- package/src/runtime/__tests__/hdr-client.test.ts +223 -223
- package/src/runtime/__tests__/http-errors.test.ts +117 -117
- package/src/runtime/__tests__/not-found.test.ts +152 -152
- package/src/runtime/adapter.ts +47 -47
- package/src/runtime/boundary.tsx +252 -252
- package/src/runtime/cache.ts +494 -494
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/fast-refresh-runtime.ts +322 -322
- package/src/runtime/handler.ts +65 -65
- package/src/runtime/handlers.ts +300 -300
- package/src/runtime/http-errors.ts +113 -113
- package/src/runtime/image-handler.ts +1 -1
- package/src/runtime/lifecycle.ts +381 -381
- package/src/runtime/logger.test.ts +345 -345
- package/src/runtime/middleware.ts +264 -264
- package/src/runtime/not-found.ts +93 -93
- package/src/runtime/openapi-endpoint.ts +236 -236
- package/src/runtime/ppr.ts +74 -74
- package/src/runtime/registry.ts +171 -171
- package/src/runtime/router.ts +105 -105
- package/src/runtime/server.ts +67 -67
- package/src/runtime/shims.ts +48 -48
- package/src/runtime/trace.ts +144 -144
- package/src/scheduler/validate.ts +169 -169
- package/src/seo/index.ts +219 -219
- package/src/seo/integration/ssr.ts +306 -306
- package/src/seo/render/basic.ts +435 -435
- package/src/seo/render/index.ts +143 -143
- package/src/seo/render/jsonld.ts +539 -539
- package/src/seo/render/opengraph.ts +197 -197
- package/src/seo/render/robots.ts +116 -116
- package/src/seo/render/sitemap.ts +137 -137
- package/src/seo/render/twitter.ts +127 -127
- package/src/seo/resolve/opengraph.ts +143 -143
- package/src/seo/resolve/robots.ts +73 -73
- package/src/seo/resolve/title.ts +94 -94
- package/src/seo/resolve/twitter.ts +73 -73
- package/src/seo/resolve/url.ts +104 -104
- package/src/seo/routes/index.ts +290 -290
- package/src/seo/types.ts +588 -588
- package/src/slot/validator.ts +39 -39
- package/src/storage/s3/__tests__/s3.test.ts +479 -479
- package/src/storage/s3/index.ts +412 -412
- package/src/testing/__tests__/assertions.test.ts +632 -632
- package/src/testing/__tests__/reporter.test.ts +454 -454
- package/src/testing/assertions.ts +986 -986
- package/src/testing/db.ts +157 -157
- package/src/testing/mocks.ts +203 -203
- package/src/testing/session.ts +190 -190
- package/src/types/branded.ts +56 -56
- package/src/types/index.ts +1 -1
- package/src/utils/safe-io.ts +188 -188
- package/src/utils/string-safe.ts +298 -298
- package/src/watcher/watcher.ts +18 -18
|
@@ -1,353 +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
|
-
});
|
|
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
|
+
});
|