@rangojs/router 0.0.0-experimental.49 → 0.0.0-experimental.4fba1c4c

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 (272) hide show
  1. package/README.md +196 -43
  2. package/dist/bin/rango.js +269 -96
  3. package/dist/testing/vitest.js +48 -0
  4. package/dist/vite/index.js +2659 -883
  5. package/dist/vite/index.js.bak +5448 -0
  6. package/dist/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  7. package/package.json +57 -11
  8. package/skills/breadcrumbs/SKILL.md +3 -1
  9. package/skills/bundle-analysis/SKILL.md +159 -0
  10. package/skills/cache-guide/SKILL.md +243 -21
  11. package/skills/caching/SKILL.md +118 -2
  12. package/skills/composability/SKILL.md +27 -2
  13. package/skills/document-cache/SKILL.md +78 -55
  14. package/skills/handler-use/SKILL.md +364 -0
  15. package/skills/hooks/SKILL.md +229 -20
  16. package/skills/host-router/SKILL.md +45 -20
  17. package/skills/i18n/SKILL.md +276 -0
  18. package/skills/intercept/SKILL.md +46 -4
  19. package/skills/layout/SKILL.md +28 -7
  20. package/skills/links/SKILL.md +249 -17
  21. package/skills/loader/SKILL.md +273 -53
  22. package/skills/middleware/SKILL.md +49 -12
  23. package/skills/migrate-nextjs/SKILL.md +562 -0
  24. package/skills/migrate-react-router/SKILL.md +769 -0
  25. package/skills/mime-routes/SKILL.md +27 -0
  26. package/skills/observability/SKILL.md +137 -0
  27. package/skills/parallel/SKILL.md +71 -6
  28. package/skills/prerender/SKILL.md +123 -100
  29. package/skills/rango/SKILL.md +242 -22
  30. package/skills/react-compiler/SKILL.md +168 -0
  31. package/skills/response-routes/SKILL.md +66 -9
  32. package/skills/route/SKILL.md +88 -4
  33. package/skills/router-setup/SKILL.md +90 -5
  34. package/skills/server-actions/SKILL.md +751 -0
  35. package/skills/streams-and-websockets/SKILL.md +283 -0
  36. package/skills/testing/SKILL.md +734 -0
  37. package/skills/typesafety/SKILL.md +329 -27
  38. package/skills/use-cache/SKILL.md +34 -5
  39. package/skills/view-transitions/SKILL.md +294 -0
  40. package/src/__augment-tests__/augment.ts +81 -0
  41. package/src/__augment-tests__/augmented.check.ts +117 -0
  42. package/src/__internal.ts +1 -1
  43. package/src/browser/action-coordinator.ts +53 -36
  44. package/src/browser/app-shell.ts +52 -0
  45. package/src/browser/app-version.ts +14 -0
  46. package/src/browser/event-controller.ts +86 -70
  47. package/src/browser/history-state.ts +21 -0
  48. package/src/browser/index.ts +3 -3
  49. package/src/browser/navigation-bridge.ts +101 -13
  50. package/src/browser/navigation-client.ts +125 -53
  51. package/src/browser/navigation-store.ts +75 -17
  52. package/src/browser/navigation-transaction.ts +10 -28
  53. package/src/browser/partial-update.ts +90 -30
  54. package/src/browser/prefetch/cache.ts +129 -21
  55. package/src/browser/prefetch/fetch.ts +156 -18
  56. package/src/browser/prefetch/queue.ts +92 -29
  57. package/src/browser/prefetch/resource-ready.ts +77 -0
  58. package/src/browser/rango-state.ts +53 -13
  59. package/src/browser/react/Link.tsx +72 -8
  60. package/src/browser/react/NavigationProvider.tsx +83 -33
  61. package/src/browser/react/context.ts +7 -2
  62. package/src/browser/react/filter-segment-order.ts +51 -7
  63. package/src/browser/react/index.ts +3 -0
  64. package/src/browser/react/location-state-shared.ts +175 -4
  65. package/src/browser/react/location-state.ts +39 -13
  66. package/src/browser/react/use-handle.ts +23 -64
  67. package/src/browser/react/use-navigation.ts +22 -2
  68. package/src/browser/react/use-params.ts +20 -8
  69. package/src/browser/react/use-reverse.ts +106 -0
  70. package/src/browser/react/use-router.ts +43 -10
  71. package/src/browser/react/use-segments.ts +11 -8
  72. package/src/browser/response-adapter.ts +25 -0
  73. package/src/browser/rsc-router.tsx +87 -22
  74. package/src/browser/scroll-restoration.ts +29 -19
  75. package/src/browser/segment-reconciler.ts +36 -14
  76. package/src/browser/segment-structure-assert.ts +2 -2
  77. package/src/browser/server-action-bridge.ts +31 -36
  78. package/src/browser/types.ts +48 -5
  79. package/src/build/collect-fallback-refs.ts +107 -0
  80. package/src/build/generate-manifest.ts +65 -40
  81. package/src/build/generate-route-types.ts +5 -0
  82. package/src/build/index.ts +2 -0
  83. package/src/build/route-trie.ts +52 -25
  84. package/src/build/route-types/codegen.ts +4 -4
  85. package/src/build/route-types/include-resolution.ts +9 -2
  86. package/src/build/route-types/per-module-writer.ts +7 -4
  87. package/src/build/route-types/router-processing.ts +266 -86
  88. package/src/build/route-types/scan-filter.ts +9 -2
  89. package/src/build/route-types/source-scan.ts +118 -0
  90. package/src/build/runtime-discovery.ts +9 -20
  91. package/src/cache/cache-scope.ts +74 -47
  92. package/src/cache/cf/cf-cache-store.ts +54 -13
  93. package/src/cache/taint.ts +55 -0
  94. package/src/client.rsc.tsx +3 -0
  95. package/src/client.tsx +94 -238
  96. package/src/context-var.ts +72 -2
  97. package/src/decode-loader-results.ts +36 -0
  98. package/src/errors.ts +30 -1
  99. package/src/handle.ts +65 -12
  100. package/src/host/index.ts +2 -2
  101. package/src/host/router.ts +129 -57
  102. package/src/host/types.ts +31 -2
  103. package/src/host/utils.ts +1 -1
  104. package/src/href-client.ts +140 -20
  105. package/src/index.rsc.ts +12 -5
  106. package/src/index.ts +61 -11
  107. package/src/loader-store.ts +500 -0
  108. package/src/loader.rsc.ts +21 -6
  109. package/src/loader.ts +3 -10
  110. package/src/missing-id-error.ts +68 -0
  111. package/src/outlet-context.ts +1 -1
  112. package/src/prerender/store.ts +5 -4
  113. package/src/prerender.ts +141 -80
  114. package/src/response-utils.ts +37 -0
  115. package/src/reverse.ts +65 -15
  116. package/src/route-content-wrapper.tsx +6 -28
  117. package/src/route-definition/dsl-helpers.ts +411 -261
  118. package/src/route-definition/helper-factories.ts +29 -139
  119. package/src/route-definition/helpers-types.ts +110 -34
  120. package/src/route-definition/index.ts +3 -0
  121. package/src/route-definition/redirect.ts +9 -1
  122. package/src/route-definition/resolve-handler-use.ts +155 -0
  123. package/src/route-definition/use-item-types.ts +32 -0
  124. package/src/route-types.ts +37 -41
  125. package/src/router/basename.ts +14 -0
  126. package/src/router/content-negotiation.ts +113 -1
  127. package/src/router/error-handling.ts +1 -1
  128. package/src/router/handler-context.ts +77 -38
  129. package/src/router/intercept-resolution.ts +13 -22
  130. package/src/router/lazy-includes.ts +8 -8
  131. package/src/router/loader-resolution.ts +174 -22
  132. package/src/router/manifest.ts +22 -13
  133. package/src/router/match-api.ts +128 -192
  134. package/src/router/match-handlers.ts +63 -20
  135. package/src/router/match-middleware/cache-lookup.ts +70 -97
  136. package/src/router/match-middleware/cache-store.ts +8 -2
  137. package/src/router/match-middleware/segment-resolution.ts +53 -0
  138. package/src/router/match-result.ts +103 -4
  139. package/src/router/metrics.ts +1 -1
  140. package/src/router/middleware-types.ts +21 -34
  141. package/src/router/middleware.ts +101 -89
  142. package/src/router/navigation-snapshot.ts +182 -0
  143. package/src/router/pattern-matching.ts +101 -17
  144. package/src/router/prerender-match.ts +110 -10
  145. package/src/router/preview-match.ts +32 -102
  146. package/src/router/request-classification.ts +286 -0
  147. package/src/router/revalidation.ts +58 -2
  148. package/src/router/route-snapshot.ts +245 -0
  149. package/src/router/router-interfaces.ts +77 -28
  150. package/src/router/router-options.ts +76 -11
  151. package/src/router/router-registry.ts +2 -5
  152. package/src/router/segment-resolution/fresh.ts +105 -13
  153. package/src/router/segment-resolution/helpers.ts +29 -24
  154. package/src/router/segment-resolution/revalidation.ts +236 -112
  155. package/src/router/segment-resolution/view-transition-default.ts +36 -0
  156. package/src/router/substitute-pattern-params.ts +56 -0
  157. package/src/router/telemetry.ts +99 -0
  158. package/src/router/trie-matching.ts +18 -13
  159. package/src/router/types.ts +9 -0
  160. package/src/router/url-params.ts +49 -0
  161. package/src/router.ts +86 -22
  162. package/src/rsc/handler-context.ts +2 -2
  163. package/src/rsc/handler.ts +440 -381
  164. package/src/rsc/helpers.ts +91 -43
  165. package/src/rsc/index.ts +1 -1
  166. package/src/rsc/loader-fetch.ts +23 -3
  167. package/src/rsc/manifest-init.ts +5 -1
  168. package/src/rsc/origin-guard.ts +28 -10
  169. package/src/rsc/progressive-enhancement.ts +18 -2
  170. package/src/rsc/response-route-handler.ts +46 -53
  171. package/src/rsc/rsc-rendering.ts +41 -48
  172. package/src/rsc/runtime-warnings.ts +9 -10
  173. package/src/rsc/server-action.ts +25 -37
  174. package/src/rsc/ssr-setup.ts +18 -2
  175. package/src/rsc/types.ts +17 -3
  176. package/src/search-params.ts +4 -4
  177. package/src/segment-content-promise.ts +67 -0
  178. package/src/segment-loader-promise.ts +122 -0
  179. package/src/segment-system.tsx +132 -116
  180. package/src/serialize.ts +243 -0
  181. package/src/server/context.ts +190 -51
  182. package/src/server/cookie-store.ts +28 -4
  183. package/src/server/handle-store.ts +19 -0
  184. package/src/server/loader-registry.ts +9 -8
  185. package/src/server/request-context.ts +195 -57
  186. package/src/ssr/index.tsx +8 -1
  187. package/src/static-handler.ts +19 -7
  188. package/src/testing/cache-status.ts +166 -0
  189. package/src/testing/collect-handle.ts +63 -0
  190. package/src/testing/dispatch.ts +440 -0
  191. package/src/testing/dom.entry.ts +22 -0
  192. package/src/testing/e2e/fixture.ts +154 -0
  193. package/src/testing/e2e/index.ts +149 -0
  194. package/src/testing/e2e/matchers.ts +51 -0
  195. package/src/testing/e2e/page-helpers.ts +272 -0
  196. package/src/testing/e2e/parity.ts +306 -0
  197. package/src/testing/e2e/server.ts +183 -0
  198. package/src/testing/flight-matchers.ts +104 -0
  199. package/src/testing/flight-runtime.d.ts +21 -0
  200. package/src/testing/flight.entry.ts +22 -0
  201. package/src/testing/flight.ts +182 -0
  202. package/src/testing/generated-routes.ts +223 -0
  203. package/src/testing/index.ts +106 -0
  204. package/src/testing/internal/context.ts +304 -0
  205. package/src/testing/render-route.tsx +565 -0
  206. package/src/testing/run-loader.ts +341 -0
  207. package/src/testing/run-middleware.ts +179 -0
  208. package/src/testing/vitest-stubs/cloudflare-email.ts +9 -0
  209. package/src/testing/vitest-stubs/cloudflare-workers.ts +21 -0
  210. package/src/testing/vitest-stubs/plugin-rsc.ts +16 -0
  211. package/src/testing/vitest-stubs/version.ts +5 -0
  212. package/src/testing/vitest.ts +183 -0
  213. package/src/types/cache-types.ts +4 -4
  214. package/src/types/global-namespace.ts +39 -26
  215. package/src/types/handler-context.ts +103 -67
  216. package/src/types/index.ts +1 -0
  217. package/src/types/loader-types.ts +41 -15
  218. package/src/types/request-scope.ts +126 -0
  219. package/src/types/route-entry.ts +12 -1
  220. package/src/types/segments.ts +36 -2
  221. package/src/urls/include-helper.ts +34 -67
  222. package/src/urls/index.ts +0 -3
  223. package/src/urls/path-helper-types.ts +50 -9
  224. package/src/urls/path-helper.ts +63 -63
  225. package/src/urls/pattern-types.ts +48 -19
  226. package/src/urls/response-types.ts +25 -22
  227. package/src/urls/type-extraction.ts +26 -116
  228. package/src/urls/urls-function.ts +1 -5
  229. package/src/use-loader.tsx +487 -44
  230. package/src/vite/debug.ts +185 -0
  231. package/src/vite/discovery/bundle-postprocess.ts +34 -37
  232. package/src/vite/discovery/discover-routers.ts +105 -51
  233. package/src/vite/discovery/discovery-errors.ts +194 -0
  234. package/src/vite/discovery/gate-state.ts +171 -0
  235. package/src/vite/discovery/prerender-collection.ts +188 -93
  236. package/src/vite/discovery/route-types-writer.ts +40 -84
  237. package/src/vite/discovery/self-gen-tracking.ts +27 -1
  238. package/src/vite/discovery/state.ts +46 -4
  239. package/src/vite/discovery/virtual-module-codegen.ts +13 -23
  240. package/src/vite/index.ts +6 -0
  241. package/src/vite/plugin-types.ts +126 -4
  242. package/src/vite/plugins/cjs-to-esm.ts +8 -7
  243. package/src/vite/plugins/client-ref-dedup.ts +16 -0
  244. package/src/vite/plugins/client-ref-hashing.ts +28 -5
  245. package/src/vite/plugins/cloudflare-protocol-loader-hook.d.mts +23 -0
  246. package/src/vite/plugins/cloudflare-protocol-loader-hook.mjs +76 -0
  247. package/src/vite/plugins/cloudflare-protocol-stub.ts +214 -0
  248. package/src/vite/plugins/expose-action-id.ts +54 -30
  249. package/src/vite/plugins/expose-id-utils.ts +24 -8
  250. package/src/vite/plugins/expose-ids/export-analysis.ts +100 -20
  251. package/src/vite/plugins/expose-ids/handler-transform.ts +12 -35
  252. package/src/vite/plugins/expose-ids/loader-transform.ts +3 -5
  253. package/src/vite/plugins/expose-ids/router-transform.ts +20 -3
  254. package/src/vite/plugins/expose-internal-ids.ts +544 -317
  255. package/src/vite/plugins/performance-tracks.ts +92 -0
  256. package/src/vite/plugins/refresh-cmd.ts +88 -26
  257. package/src/vite/plugins/use-cache-transform.ts +65 -50
  258. package/src/vite/plugins/version-injector.ts +39 -23
  259. package/src/vite/plugins/version-plugin.ts +59 -2
  260. package/src/vite/plugins/virtual-entries.ts +2 -2
  261. package/src/vite/rango.ts +130 -26
  262. package/src/vite/router-discovery.ts +920 -129
  263. package/src/vite/utils/ast-handler-extract.ts +15 -15
  264. package/src/vite/utils/banner.ts +1 -1
  265. package/src/vite/utils/bundle-analysis.ts +4 -2
  266. package/src/vite/utils/client-chunks.ts +190 -0
  267. package/src/vite/utils/forward-user-plugins.ts +193 -0
  268. package/src/vite/utils/manifest-utils.ts +21 -5
  269. package/src/vite/utils/package-resolution.ts +41 -1
  270. package/src/vite/utils/prerender-utils.ts +38 -5
  271. package/src/vite/utils/shared-utils.ts +109 -27
  272. package/src/browser/action-response-classifier.ts +0 -99
@@ -0,0 +1,304 @@
1
+ /**
2
+ * Shared internals for the consumer testing primitives.
3
+ *
4
+ * Builds a real RequestContext via the same createRequestContext the RSC
5
+ * handler uses, with test-friendly defaults, so loaders and middleware run
6
+ * with production-fidelity context (cookies, headers, get/set, use, reverse)
7
+ * instead of a hand-rolled mock.
8
+ */
9
+
10
+ import {
11
+ createRequestContext,
12
+ runWithRequestContext,
13
+ type RequestContext,
14
+ } from "../../server/request-context.js";
15
+ import { resolveLocationStateEntries } from "../../browser/react/location-state-shared.js";
16
+ import { createReverseFunction } from "../../router/handler-context.js";
17
+ import { normalizeBasename } from "../../router/basename.js";
18
+ import { contextSet, type ContextVar } from "../../context-var.js";
19
+ import type { ThemeConfig } from "../../theme/types.js";
20
+ import { resolveThemeConfig } from "../../theme/constants.js";
21
+ import type { SegmentCacheStore } from "../../cache/types.js";
22
+ import type { CacheProfile } from "../../cache/profile-registry.js";
23
+
24
+ const DEFAULT_ORIGIN = "http://localhost/";
25
+
26
+ /**
27
+ * Initializer for seeded context variables (as a prior middleware would have
28
+ * set). Either a plain object keyed by var name (the common, best-inferring
29
+ * form: `{ user: u }`) or a list of `[key, value]` tuples where the key may be a
30
+ * `createVar()` handle or a string (`[[userVar, u], ["flag", true]]`).
31
+ */
32
+ export type VarsInit =
33
+ | Record<string, unknown>
34
+ | ReadonlyArray<readonly [ContextVar<unknown> | string, unknown]>;
35
+
36
+ /** Normalize a Request | string | undefined into a concrete Request. */
37
+ export function toRequest(
38
+ request: Request | string | undefined,
39
+ init?: RequestInit,
40
+ ): Request {
41
+ if (request instanceof Request) return request;
42
+ if (typeof request === "string") {
43
+ return new Request(new URL(request, DEFAULT_ORIGIN), init);
44
+ }
45
+ return new Request(DEFAULT_ORIGIN, init);
46
+ }
47
+
48
+ /**
49
+ * Preload variables as if set by upstream middleware. Accepts entries keyed by
50
+ * either a ContextVar (from createVar) or a string, matching ctx.set().
51
+ */
52
+ export function seedVariables(
53
+ variables: Record<string, unknown>,
54
+ vars?: VarsInit,
55
+ ): Record<string, unknown> {
56
+ if (!vars) return variables;
57
+ // Array/iterable form -> use the tuples as-is; plain object -> its entries.
58
+ const entries: Iterable<readonly [ContextVar<unknown> | string, unknown]> =
59
+ Symbol.iterator in (vars as object)
60
+ ? (vars as ReadonlyArray<
61
+ readonly [ContextVar<unknown> | string, unknown]
62
+ >)
63
+ : Object.entries(vars as Record<string, unknown>);
64
+ for (const [key, value] of entries) {
65
+ contextSet(variables, key as ContextVar<unknown>, value);
66
+ }
67
+ return variables;
68
+ }
69
+
70
+ export interface CreateTestContextOptions<TEnv> {
71
+ env?: TEnv;
72
+ request?: Request | string;
73
+ requestInit?: RequestInit;
74
+ /** Backing store for ctx.get()/ctx.set(); pre-seeded from `vars`. */
75
+ variables?: Record<string, unknown>;
76
+ /** Variables a prior middleware would have set (object or [key, value] list). */
77
+ vars?: VarsInit;
78
+ /** Route name -> pattern map enabling ctx.reverse() without global state. */
79
+ routeMap?: Record<string, string>;
80
+ routeName?: string;
81
+ params?: Record<string, string>;
82
+ /**
83
+ * Router basename for this request (what the RSC handler stores on the
84
+ * context). Drives redirect() prefixing. Normalized exactly like
85
+ * createRouter({ basename }) (leading slash forced, trailing stripped, bare
86
+ * "/" -> undefined) so passing the same value your router takes yields the
87
+ * same redirect Location. Defaults to undefined (no basename).
88
+ */
89
+ basename?: string;
90
+ /**
91
+ * Cache store backing `use cache` functions invoked during the test, the
92
+ * same shape `createRouter({ cache })` resolves. Without it,
93
+ * registerCachedFunction bypasses (it checks for a store FIRST), so a cached
94
+ * function runs uncached and its taint/profile guards never fire. Wire one
95
+ * (e.g. `new MemorySegmentCacheStore()`) to exercise real cache behavior.
96
+ */
97
+ cacheStore?: SegmentCacheStore;
98
+ /**
99
+ * Cache profiles in the `createRouter({ cacheProfiles })` shape. Required for
100
+ * a `use cache: "profileName"` function to resolve its profile (an unknown
101
+ * profile throws), once a `cacheStore` is wired.
102
+ */
103
+ cacheProfiles?: Record<string, CacheProfile>;
104
+ /**
105
+ * Theme config in the same shape `createRouter({ theme })` takes (resolved
106
+ * internally). Without it `ctx.theme`/`ctx.setTheme` are inert (undefined),
107
+ * mirroring an app with no theme configured. Pass one (e.g. `true`, or
108
+ * `{ themes: [...] }`) to exercise a handler that reads them.
109
+ */
110
+ theme?: ThemeConfig | true;
111
+ }
112
+
113
+ export interface TestRequestContext<TEnv> {
114
+ ctx: RequestContext<TEnv>;
115
+ request: Request;
116
+ url: URL;
117
+ variables: Record<string, unknown>;
118
+ }
119
+
120
+ /**
121
+ * Create a real RequestContext for unit-testing loaders/middleware.
122
+ *
123
+ * The returned `ctx` must be ENTERED before use — wrap your call in
124
+ * `runWithRequestContext(ctx, fn)` (re-exported from `@rangojs/router/testing`)
125
+ * so that cookie/header mutations and `getRequestContext()` resolve. For the
126
+ * common case prefer {@link runInRequestContext}, which builds AND enters the
127
+ * context in a single call.
128
+ */
129
+ export function createTestRequestContext<TEnv>(
130
+ opts: CreateTestContextOptions<TEnv> = {},
131
+ ): TestRequestContext<TEnv> {
132
+ const request = toRequest(opts.request, opts.requestInit);
133
+ const url = new URL(request.url);
134
+ const variables = seedVariables(opts.variables ?? {}, opts.vars);
135
+ const ctx = createRequestContext<TEnv>({
136
+ env: (opts.env ?? {}) as TEnv,
137
+ request,
138
+ url,
139
+ variables,
140
+ themeConfig:
141
+ opts.theme === undefined ? undefined : resolveThemeConfig(opts.theme),
142
+ cacheStore: opts.cacheStore,
143
+ cacheProfiles: opts.cacheProfiles,
144
+ });
145
+ if (opts.basename !== undefined)
146
+ ctx._basename = normalizeBasename(opts.basename);
147
+ if (opts.params) ctx.params = opts.params;
148
+ if (opts.routeMap) {
149
+ ctx._routeName = opts.routeName;
150
+ ctx.reverse = createReverseFunction(
151
+ opts.routeMap,
152
+ opts.routeName,
153
+ opts.params ?? {},
154
+ ) as RequestContext<TEnv>["reverse"];
155
+ }
156
+ return { ctx, request, url, variables };
157
+ }
158
+
159
+ /**
160
+ * What a run accumulated on the request context, surfaced as PUBLIC values so a
161
+ * test never has to cast through the `@internal` `ctx.res` / `ctx.cookies()` to
162
+ * assert what an action produced.
163
+ */
164
+ export interface RunInRequestContextResult<T> {
165
+ /**
166
+ * The value `fn` returned (awaited), or `undefined` if `fn` threw — in which
167
+ * case the thrown value is on {@link thrown}. The snapshot below is captured
168
+ * either way.
169
+ */
170
+ result: T | undefined;
171
+ /**
172
+ * The value `fn` threw, or `undefined` if it returned normally. Commonly a
173
+ * `Response` from `throw redirect(...)` / `throw notFound()` — the dominant
174
+ * cookie+flash case is an action that sets them then throws a redirect — so
175
+ * this (and the snapshot below) is observable WITHOUT wrapping the action in
176
+ * your own try/catch. NOTE: the value is captured, NOT re-thrown; assert on it
177
+ * for a throwing action.
178
+ */
179
+ thrown: unknown;
180
+ /**
181
+ * A Response carrying the status, headers, and Set-Cookie the run set (via
182
+ * `cookies().set()`, `ctx.header()`, etc.). Assert Set-Cookie with
183
+ * `response.headers.getSetCookie()`. When `fn` threw a `Response` (a redirect),
184
+ * THIS is that Response with the accumulated Set-Cookie/headers merged in
185
+ * (mirroring how the framework merges them in production), so a redirect's
186
+ * Location AND the cookies it set are both observable here.
187
+ */
188
+ response: Response;
189
+ /**
190
+ * The effective cookie view after the run: request cookies merged with
191
+ * anything the run set or deleted (last-write-wins), as `{ name: value }`.
192
+ */
193
+ cookies: Record<string, string>;
194
+ /**
195
+ * Location state the run set via `ctx.setLocationState()` / `redirect({ state })`,
196
+ * resolved to the flat `{ key: value }` shape the client reads off
197
+ * `history.state` (empty object when none) — so a post-action flash ("Saved!")
198
+ * is assertable at the unit layer.
199
+ */
200
+ locationState: Record<string, unknown>;
201
+ }
202
+
203
+ /**
204
+ * Snapshot the observable effects a run left on `ctx` (cookies + location
205
+ * state). Reads the fields directly off the ctx object, so it works both inside
206
+ * and outside the AsyncLocalStorage scope (no `getRequestContext()`).
207
+ */
208
+ export function snapshotRunEffects<TEnv>(ctx: RequestContext<TEnv>): {
209
+ cookies: Record<string, string>;
210
+ locationState: Record<string, unknown>;
211
+ } {
212
+ return {
213
+ cookies: { ...ctx.cookies() },
214
+ locationState: resolveLocationStateEntries(ctx._locationState ?? []),
215
+ };
216
+ }
217
+
218
+ /**
219
+ * Build the observable response from what the run accumulated on `ctx.res`. When
220
+ * `fn` threw a `Response` (a `redirect()`/`notFound()`), that Response IS the
221
+ * response — merge the accumulated Set-Cookie/other headers into it (the
222
+ * framework does this when it catches the thrown Response in production), with
223
+ * its status/Location preserved. Otherwise snapshot the stub (status + headers).
224
+ * The `Response`/`Headers` constructors copy, so the result is immutable.
225
+ */
226
+ function buildRunResponse<TEnv>(
227
+ ctx: RequestContext<TEnv>,
228
+ thrown: unknown,
229
+ ): Response {
230
+ const stub = ctx.res;
231
+ if (thrown instanceof Response) {
232
+ const headers = new Headers(thrown.headers);
233
+ for (const cookie of stub.headers.getSetCookie()) {
234
+ headers.append("set-cookie", cookie);
235
+ }
236
+ stub.headers.forEach((value, name) => {
237
+ if (name.toLowerCase() === "set-cookie") return;
238
+ if (!headers.has(name)) headers.set(name, value);
239
+ });
240
+ return new Response(null, { status: thrown.status, headers });
241
+ }
242
+ return new Response(null, { status: stub.status, headers: stub.headers });
243
+ }
244
+
245
+ /**
246
+ * Build a seeded RequestContext (via {@link createTestRequestContext}) and run
247
+ * `fn` inside it, so code under test that calls `getRequestContext()`,
248
+ * `cookies()`, or reads/mutates request headers resolves exactly as in
249
+ * production.
250
+ *
251
+ * This is the entry point for the advanced cases the unit wrappers
252
+ * (`runLoader` / `runMiddleware`) do not model — most notably a server ACTION
253
+ * that authenticates off the request cookie or sets a session cookie / flash:
254
+ * an action has no loader context, so `runLoader` is the wrong shape, yet it
255
+ * still needs a real request context to read the cookie and resolve
256
+ * `getRequestContext()`.
257
+ *
258
+ * Returns `{ result, thrown, response, cookies, locationState }` so the action's
259
+ * OUTPUT (Set-Cookie, headers, flash) is assertable without casting through the
260
+ * `@internal` `ctx.res` / `ctx.cookies()`. `fn` may be async — the context stays
261
+ * active across its awaits (AsyncLocalStorage), and the snapshot is captured
262
+ * whether `fn` returns OR throws. The throw path matters: the most common
263
+ * cookie+flash case is an auth action that sets a cookie + flash then
264
+ * `throw redirect(...)` on success — the thrown redirect is on `thrown` (NOT
265
+ * re-thrown) and its Location plus the cookies are on `response`/`cookies`.
266
+ *
267
+ * @example
268
+ * ```ts
269
+ * const { result, cookies, response, thrown } = await runInRequestContext(
270
+ * () => loginAction(input), // sets a session cookie, then `throw redirect("/app")`
271
+ * {
272
+ * env,
273
+ * request: new Request("https://app.test/", {
274
+ * headers: { Cookie: "sid=abc" },
275
+ * }),
276
+ * },
277
+ * );
278
+ * expect(cookies.session).toBe("new-token");
279
+ * expect((thrown as Response).headers.get("Location")).toBe("/app");
280
+ * expect(response.headers.getSetCookie()).toContainEqual(
281
+ * expect.stringContaining("session="),
282
+ * );
283
+ * ```
284
+ */
285
+ export async function runInRequestContext<T, TEnv = unknown>(
286
+ fn: (ctx: RequestContext<TEnv>) => T | Promise<T>,
287
+ opts: CreateTestContextOptions<TEnv> = {},
288
+ ): Promise<RunInRequestContextResult<T>> {
289
+ const { ctx } = createTestRequestContext<TEnv>(opts);
290
+ let result: T | undefined;
291
+ let thrown: unknown;
292
+ let didThrow = false;
293
+ try {
294
+ result = (await runWithRequestContext(ctx, () => fn(ctx))) as T;
295
+ } catch (error) {
296
+ // Capture (do NOT re-throw): a redirect/notFound action throws its Response
297
+ // on the SUCCESS path, and its cookie/flash output must stay observable.
298
+ didThrow = true;
299
+ thrown = error;
300
+ }
301
+ const { cookies, locationState } = snapshotRunEffects(ctx);
302
+ const response = buildRunResponse(ctx, didThrow ? thrown : undefined);
303
+ return { result, thrown, response, cookies, locationState };
304
+ }