@reckona/mreact-router 0.0.189 → 0.0.191

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 (76) hide show
  1. package/README.md +17 -4
  2. package/bin/mreact-router.js +23 -0
  3. package/dist/adapters/aws-lambda.d.ts +33 -13
  4. package/dist/adapters/aws-lambda.d.ts.map +1 -1
  5. package/dist/adapters/aws-lambda.js +128 -20
  6. package/dist/adapters/aws-lambda.js.map +1 -1
  7. package/dist/adapters/cloudflare.d.ts +18 -0
  8. package/dist/adapters/cloudflare.d.ts.map +1 -1
  9. package/dist/adapters/cloudflare.js.map +1 -1
  10. package/dist/adapters/edge.d.ts +2 -0
  11. package/dist/adapters/edge.d.ts.map +1 -1
  12. package/dist/adapters/edge.js.map +1 -1
  13. package/dist/adapters/node.d.ts +16 -2
  14. package/dist/adapters/node.d.ts.map +1 -1
  15. package/dist/adapters/node.js.map +1 -1
  16. package/dist/build.d.ts +6 -0
  17. package/dist/build.d.ts.map +1 -1
  18. package/dist/build.js +57 -12
  19. package/dist/build.js.map +1 -1
  20. package/dist/cache.d.ts +8 -0
  21. package/dist/cache.d.ts.map +1 -1
  22. package/dist/cache.js +34 -13
  23. package/dist/cache.js.map +1 -1
  24. package/dist/cli-options.d.ts +4 -1
  25. package/dist/cli-options.d.ts.map +1 -1
  26. package/dist/cli-options.js +63 -3
  27. package/dist/cli-options.js.map +1 -1
  28. package/dist/cli.js +12 -0
  29. package/dist/cli.js.map +1 -1
  30. package/dist/client.d.ts +1 -1
  31. package/dist/client.d.ts.map +1 -1
  32. package/dist/client.js +1 -0
  33. package/dist/client.js.map +1 -1
  34. package/dist/config.d.ts +4 -0
  35. package/dist/config.d.ts.map +1 -1
  36. package/dist/config.js +6 -2
  37. package/dist/config.js.map +1 -1
  38. package/dist/index.d.ts +19 -16
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +15 -5
  41. package/dist/index.js.map +1 -1
  42. package/dist/link.d.ts +22 -1
  43. package/dist/link.d.ts.map +1 -1
  44. package/dist/link.js +130 -5
  45. package/dist/link.js.map +1 -1
  46. package/dist/render.d.ts.map +1 -1
  47. package/dist/render.js +514 -517
  48. package/dist/render.js.map +1 -1
  49. package/dist/request.d.ts +10 -0
  50. package/dist/request.d.ts.map +1 -0
  51. package/dist/request.js +6 -0
  52. package/dist/request.js.map +1 -0
  53. package/dist/typed-routes.d.ts +4 -5
  54. package/dist/typed-routes.d.ts.map +1 -1
  55. package/dist/typed-routes.js.map +1 -1
  56. package/dist/vite.d.ts +6 -0
  57. package/dist/vite.d.ts.map +1 -1
  58. package/dist/vite.js +4 -1
  59. package/dist/vite.js.map +1 -1
  60. package/package.json +19 -13
  61. package/src/adapters/aws-lambda.ts +227 -56
  62. package/src/adapters/cloudflare.ts +59 -0
  63. package/src/adapters/edge.ts +14 -0
  64. package/src/adapters/node.ts +44 -2
  65. package/src/build.ts +113 -16
  66. package/src/cache.ts +39 -17
  67. package/src/cli-options.ts +92 -6
  68. package/src/cli.ts +12 -0
  69. package/src/client.ts +2 -1
  70. package/src/config.ts +10 -2
  71. package/src/index.ts +57 -10
  72. package/src/link.ts +205 -10
  73. package/src/render.ts +5 -7
  74. package/src/request.ts +35 -0
  75. package/src/typed-routes.ts +4 -4
  76. package/src/vite.ts +30 -1
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ export { defer, isDeferredLoaderData } from "./deferred.js";
7
7
  export type { DeferredLoaderData } from "./deferred.js";
8
8
  export { definePage } from "./types.js";
9
9
  export { Link, linkProps } from "./link.js";
10
+ export type { LinkSerializableAttribute, LinkSinkChild, LinkSinkProps } from "./link.js";
10
11
  export { href } from "./typed-routes.js";
11
12
  export { parseMultipartStream } from "./multipart.js";
12
13
  export type {
@@ -35,7 +36,7 @@ export {
35
36
  textError,
36
37
  throwNotFound,
37
38
  } from "./navigation.js";
38
- export type { ParseSchema } from "./navigation.js";
39
+ export type { MiddlewareNext, ParseSchema, RedirectOptions, RequestCookies } from "./navigation.js";
39
40
  export { createMemoryPrerenderStore } from "./prerender-store.js";
40
41
  export { getServerRuntimeState } from "./runtime-state.js";
41
42
  import {
@@ -57,43 +58,80 @@ import type {
57
58
  *
58
59
  * @deprecated Import session helpers from `@reckona/mreact-auth` instead.
59
60
  */
60
- export const createMemorySessionStore = createMemorySessionStoreInternal;
61
+ export function createMemorySessionStore<TData = unknown>(
62
+ options: MemorySessionStoreOptionsInternal = {},
63
+ ): SessionStoreInternal<TData> {
64
+ return createMemorySessionStoreInternal<TData>(options);
65
+ }
61
66
  /**
62
67
  * Creates a deprecated session record and cookie alias.
63
68
  *
64
69
  * @deprecated Import session helpers from `@reckona/mreact-auth` instead.
65
70
  */
66
- export const createSession = createSessionInternal;
71
+ export async function createSession<TData>(
72
+ response: Response,
73
+ store: SessionStoreInternal<TData>,
74
+ data: TData,
75
+ options: SessionCookieOptionsInternal = {},
76
+ ): Promise<SessionRecordInternal<TData>> {
77
+ return createSessionInternal(response, store, data, options);
78
+ }
67
79
  /**
68
80
  * Destroys a session through a deprecated router session alias.
69
81
  *
70
82
  * @deprecated Import session helpers from `@reckona/mreact-auth` instead.
71
83
  */
72
- export const destroySession = destroySessionInternal;
84
+ export async function destroySession<TData>(
85
+ request: Request,
86
+ response: Response,
87
+ store: SessionStoreInternal<TData>,
88
+ options: SessionCookieOptionsInternal = {},
89
+ ): Promise<void> {
90
+ return destroySessionInternal(request, response, store, options);
91
+ }
73
92
  /**
74
93
  * Reads a session through a deprecated router session alias.
75
94
  *
76
95
  * @deprecated Import session helpers from `@reckona/mreact-auth` instead.
77
96
  */
78
- export const getSession = getSessionInternal;
97
+ export async function getSession<TData>(
98
+ request: Request,
99
+ store: SessionStoreInternal<TData>,
100
+ options: SessionCookieOptionsInternal = {},
101
+ ): Promise<SessionRecordInternal<TData> | undefined> {
102
+ return getSessionInternal(request, store, options);
103
+ }
79
104
  /**
80
105
  * Rotates a session through a deprecated router session alias.
81
106
  *
82
107
  * @deprecated Import session helpers from `@reckona/mreact-auth` instead.
83
108
  */
84
- export const rotateSession = rotateSessionInternal;
109
+ export async function rotateSession<TData>(
110
+ request: Request,
111
+ response: Response,
112
+ store: SessionStoreInternal<TData>,
113
+ options: SessionCookieOptionsInternal = {},
114
+ ): Promise<SessionRecordInternal<TData> | undefined> {
115
+ return rotateSessionInternal(request, response, store, options);
116
+ }
85
117
  export type {
86
118
  AwsLambdaArtifactManifest,
119
+ AwsLambdaGeneratedHandlerPreloadMode,
87
120
  BuildAppPhase,
88
121
  BuildAppPhaseTiming,
89
122
  BuildAppOptions,
90
123
  BuildAppProgressEvent,
91
124
  BuildAppResult,
92
125
  BuiltImportPolicyArtifact,
126
+ BuiltPrerenderedRoute,
127
+ BuiltRouteSourceAnalysisSummary,
128
+ BuiltServerModuleArtifact,
129
+ BuiltServerModuleOutput,
93
130
  CloudflarePagesArtifactManifest,
94
131
  PackageAwsLambdaArtifactOptions,
95
132
  PackageCloudflarePagesArtifactOptions,
96
133
  } from "./build.js";
134
+ export type { AppRouterProjectOptions } from "./config.js";
97
135
  export type { ServerActionContext } from "./actions.js";
98
136
  export type {
99
137
  AppRouteHref,
@@ -106,7 +144,11 @@ export type {
106
144
  RouteParamsFor,
107
145
  RouteSearchParams,
108
146
  RouteSearchValue,
147
+ SegmentRouteParam,
109
148
  StaticHrefOptions,
149
+ ExtractRouteParams,
150
+ HasRouteParams,
151
+ Simplify,
110
152
  } from "./typed-routes.js";
111
153
  export type {
112
154
  InferLoaderData,
@@ -180,7 +222,7 @@ export type {
180
222
  } from "./navigation-state.js";
181
223
  export type { RouterRuntimeCacheStat } from "./runtime-cache.js";
182
224
  /** Configures the deprecated router memory session store alias. */
183
- export type { MemorySessionStoreOptionsInternal as MemorySessionStoreOptions };
225
+ export type { MemorySessionStoreOptions } from "./session.js";
184
226
  export type {
185
227
  AppRouterCspInlineNonceWarningLogEvent,
186
228
  AppRouterLogError,
@@ -211,23 +253,24 @@ export type {
211
253
  *
212
254
  * @deprecated Import session helpers and types from `@reckona/mreact-auth` instead.
213
255
  */
214
- export type SessionCookieOptions = SessionCookieOptionsInternal;
256
+ export type { SessionCookieOptions } from "./session.js";
215
257
  /**
216
258
  * Stores session data through the deprecated router session record alias.
217
259
  *
218
260
  * @deprecated Import session helpers and types from `@reckona/mreact-auth` instead.
219
261
  */
220
- export type SessionRecord<TData = unknown> = SessionRecordInternal<TData>;
262
+ export type { SessionRecord } from "./session.js";
221
263
  /**
222
264
  * Defines session persistence through the deprecated router session store alias.
223
265
  *
224
266
  * @deprecated Import session helpers and types from `@reckona/mreact-auth` instead.
225
267
  */
226
- export type SessionStore<TData = unknown> = SessionStoreInternal<TData>;
268
+ export type { SessionStore } from "./session.js";
227
269
  export { startDevServer } from "./dev-server.js";
228
270
  export type { StartDevServerOptions } from "./dev-server.js";
229
271
  export { renderAppRequest } from "./render.js";
230
272
  export type {
273
+ AppRouterRenderPreload,
231
274
  AppRouterResponseHook,
232
275
  AppRouterResponseHookContext,
233
276
  RenderAppRequestOptions,
@@ -250,6 +293,7 @@ export type {
250
293
  FileSystemPrerenderStoreOptions,
251
294
  KeyValuePrerenderStoreAdapter,
252
295
  KeyValuePrerenderStoreOptions,
296
+ MemoryPrerenderStoreEntry,
253
297
  MemoryPrerenderStoreOptions,
254
298
  } from "./prerender-store.js";
255
299
  export { createFileSystemPrerenderStore, createKeyValuePrerenderStore } from "./prerender-store.js";
@@ -260,9 +304,12 @@ export type {
260
304
  BuiltAppRuntimePreloadStrategy,
261
305
  RenderBuiltAppRequestOptions,
262
306
  RequestHostPolicy,
307
+ ResponseSinkStrategy,
263
308
  StartServerOptions,
264
309
  } from "./serve.js";
265
310
  export { matchRoute, scanAppRoutes } from "./routes.js";
311
+ export type { CachedClientRouteSource, ClientRouteInferenceCache } from "./client.js";
312
+ export type { RouteMatcher, ScanAppRoutesOptions } from "./routes.js";
266
313
  export type { AppFileConvention } from "./file-conventions.js";
267
314
  export type {
268
315
  AppAssetRoute,
package/src/link.ts CHANGED
@@ -13,6 +13,7 @@ export type {
13
13
  } from "./typed-routes.js";
14
14
 
15
15
  const TRUSTED_LINK_HTML = Symbol.for("modular.react.router.trusted_link_html");
16
+ const VALID_LINK_ATTRIBUTE_NAME = /^[A-Za-z_][\w.\-:]*$/;
16
17
 
17
18
  /**
18
19
  * Selects when the app router should prefetch a linked route.
@@ -70,6 +71,37 @@ export interface LinkProps<Href extends string = LinkHref> extends LinkOptions<H
70
71
  [attribute: string]: unknown;
71
72
  }
72
73
 
74
+ /** Represents attributes that can be serialized when Link writes to an HtmlSink. */
75
+ export type LinkSerializableAttribute = boolean | number | string | null | undefined;
76
+
77
+ /** Represents child values that an HtmlSink Link can serialize without a browser DOM. */
78
+ export type LinkSinkChild =
79
+ | boolean
80
+ | number
81
+ | string
82
+ | null
83
+ | undefined
84
+ | TrustedLinkHtml
85
+ | readonly LinkSinkChild[];
86
+
87
+ /** Configures an HtmlSink Link without browser-only event handlers or refs. */
88
+ export interface LinkSinkProps extends LinkOptions<string> {
89
+ children?: LinkSinkChild;
90
+ class?: string | undefined;
91
+ className?: string | undefined;
92
+ download?: string | boolean | undefined;
93
+ id?: string | undefined;
94
+ name?: string | undefined;
95
+ rel?: string | undefined;
96
+ role?: string | undefined;
97
+ style?: Readonly<Record<string, boolean | number | string | null | undefined>> | undefined;
98
+ tabIndex?: number | undefined;
99
+ target?: string | undefined;
100
+ title?: string | undefined;
101
+ [ariaAttribute: `aria-${string}`]: LinkSerializableAttribute;
102
+ [dataAttribute: `data-${string}`]: LinkSerializableAttribute;
103
+ }
104
+
73
105
  /**
74
106
  * Converts router link options into anchor attributes consumed by the client navigation runtime.
75
107
  *
@@ -85,7 +117,9 @@ export function linkProps(options: LinkOptions<string>): Record<string, string>
85
117
  ...(options.scroll === undefined || options.scroll === "top"
86
118
  ? {}
87
119
  : { "data-mreact-scroll": options.scroll }),
88
- ...(options.transition === undefined || options.transition === false || options.transition === "none"
120
+ ...(options.transition === undefined ||
121
+ options.transition === false ||
122
+ options.transition === "none"
89
123
  ? {}
90
124
  : { "data-mreact-transition": options.transition }),
91
125
  };
@@ -108,19 +142,51 @@ export type ConcreteLinkHrefGuard<Href extends string> = [RegisteredAppRoutePath
108
142
  export function Link<const Href extends LinkHref>(
109
143
  props: LinkProps<Href> & ConcreteLinkHrefGuard<Href>,
110
144
  ): ReactCompatElement;
111
- export function Link(sink: HtmlSink, props: LinkProps<string>): void;
145
+ export function Link(sink: HtmlSink, props: LinkSinkProps): void;
112
146
  export function Link(
113
147
  sinkOrProps: HtmlSink | LinkProps<string>,
114
- maybeProps?: LinkProps<string>,
148
+ maybeProps?: LinkProps<string> | LinkSinkProps,
115
149
  ): ReactCompatElement | string | HTMLAnchorElement | void {
116
150
  if (maybeProps !== undefined) {
117
- (sinkOrProps as HtmlSink).append(renderLinkString(maybeProps));
151
+ reportUnsupportedSinkProps(maybeProps);
152
+ (sinkOrProps as HtmlSink).append(renderLinkString(maybeProps as LinkSinkProps));
118
153
  return;
119
154
  }
120
155
 
121
156
  return renderLink(sinkOrProps as LinkProps);
122
157
  }
123
158
 
159
+ function reportUnsupportedSinkProps(props: LinkProps<string> | LinkSinkProps): void {
160
+ const nodeEnv = (
161
+ globalThis as typeof globalThis & {
162
+ process?: { env?: { NODE_ENV?: string | undefined } | undefined } | undefined;
163
+ }
164
+ ).process?.env?.NODE_ENV;
165
+
166
+ if (nodeEnv === "production") {
167
+ return;
168
+ }
169
+
170
+ const unsupported = Object.entries(props)
171
+ .filter(
172
+ ([name, value]) =>
173
+ name === "ref" ||
174
+ typeof value === "function" ||
175
+ typeof value === "symbol" ||
176
+ isLinkEventAttributeName(name) ||
177
+ (name === "children" && !isSerializableSinkChild(value)) ||
178
+ (name !== "children" && name !== "style" && typeof value === "object" && value !== null) ||
179
+ (name !== "children" && !isSafeLinkAttributeName(attributeName(name))),
180
+ )
181
+ .map(([name]) => name);
182
+
183
+ if (unsupported.length > 0) {
184
+ console.warn(
185
+ `[MR_LINK_SINK_UNSUPPORTED_PROP] HtmlSink Link cannot serialize browser-only props: ${unsupported.join(", ")}. Render Link through JSX for event handlers and refs.`,
186
+ );
187
+ }
188
+ }
189
+
124
190
  (Link as typeof Link & { trustedHtml(html: string): TrustedLinkHtml }).trustedHtml = (
125
191
  html: string,
126
192
  ): TrustedLinkHtml => ({
@@ -141,13 +207,14 @@ function renderLink(props: LinkProps<string>): string | HTMLAnchorElement {
141
207
  return renderAnchorString(propsWithLinkAttrs);
142
208
  }
143
209
 
144
- function renderLinkString(props: LinkProps<string>): string {
210
+ function renderLinkString(props: LinkSinkProps): string {
145
211
  const { href, prefetch, reload, scroll, transition, ...rest } = props;
146
-
147
- return renderAnchorString({
212
+ const propsWithLinkAttrs = {
148
213
  ...rest,
149
214
  ...linkProps({ href, prefetch, reload, scroll, transition }),
150
- });
215
+ };
216
+
217
+ return `<a${renderAnchorAttributes(propsWithLinkAttrs)}>${renderSinkChildren(props.children)}</a>`;
151
218
  }
152
219
 
153
220
  function createAnchorElement(props: Record<string, unknown>): HTMLAnchorElement {
@@ -159,18 +226,53 @@ function createAnchorElement(props: Record<string, unknown>): HTMLAnchorElement
159
226
  continue;
160
227
  }
161
228
 
229
+ if (name === "ref") {
230
+ if (typeof value === "function") {
231
+ (value as (element: HTMLAnchorElement) => void)(anchor);
232
+ } else if (typeof value === "object" && value !== null && "current" in value) {
233
+ (value as { current: HTMLAnchorElement | null }).current = anchor;
234
+ }
235
+ continue;
236
+ }
237
+
238
+ if (/^on[A-Z]/.test(name) && typeof value === "function") {
239
+ const capture = name.endsWith("Capture");
240
+ const eventName = name.slice(2, capture ? -"Capture".length : undefined).toLowerCase();
241
+ anchor.addEventListener(eventName, value as EventListener, capture);
242
+ continue;
243
+ }
244
+
245
+ if (isLinkEventAttributeName(name)) {
246
+ reportRejectedBrowserEventProp(name);
247
+ continue;
248
+ }
249
+
250
+ if (name === "style" && typeof value === "object" && value !== null) {
251
+ for (const [property, styleValue] of Object.entries(value as Record<string, unknown>)) {
252
+ const cssProperty = linkStylePropertyName(property);
253
+ const cssValue = linkStyleValue(styleValue);
254
+ if (cssProperty !== undefined && cssValue !== undefined) {
255
+ anchor.style.setProperty(cssProperty, cssValue);
256
+ }
257
+ }
258
+ continue;
259
+ }
260
+
162
261
  if (!shouldSetAttribute(name, value)) {
163
262
  continue;
164
263
  }
165
264
 
166
265
  const attrName = attributeName(name);
266
+ if (!isSafeLinkAttributeName(attrName)) {
267
+ continue;
268
+ }
167
269
  const safeValue = safeUrlAttributeValue(attrName, String(value));
168
270
 
169
271
  if (safeValue === undefined) {
170
272
  continue;
171
273
  }
172
274
 
173
- anchor.setAttribute(attrName, safeValue);
275
+ anchor.setAttribute(attrName, value === true ? "" : safeValue);
174
276
  }
175
277
 
176
278
  return anchor;
@@ -204,11 +306,33 @@ function renderAnchorAttributes(props: Record<string, unknown>): string {
204
306
  const attrs: string[] = [];
205
307
 
206
308
  for (const [name, value] of Object.entries(props)) {
207
- if (name === "children" || !shouldSetAttribute(name, value)) {
309
+ if (name === "children" || isLinkEventAttributeName(name) || !shouldSetAttribute(name, value)) {
310
+ continue;
311
+ }
312
+
313
+ if (name !== "style" && typeof value === "object" && value !== null) {
208
314
  continue;
209
315
  }
210
316
 
211
317
  const attrName = attributeName(name);
318
+ if (!isSafeLinkAttributeName(attrName)) {
319
+ continue;
320
+ }
321
+ if (name === "style" && typeof value === "object" && value !== null) {
322
+ const style = Object.entries(value as Record<string, unknown>)
323
+ .flatMap(([property, styleValue]) => {
324
+ const cssProperty = linkStylePropertyName(property);
325
+ const cssValue = linkStyleValue(styleValue);
326
+ return cssProperty === undefined || cssValue === undefined
327
+ ? []
328
+ : [`${cssProperty}:${cssValue}`];
329
+ })
330
+ .join(";");
331
+ if (style !== "") {
332
+ attrs.push(`${escapeHtmlAttribute(attrName)}="${escapeHtmlAttribute(style)}"`);
333
+ }
334
+ continue;
335
+ }
212
336
  const attrValue = String(value);
213
337
  const safeValue = safeUrlAttributeValue(attrName, attrValue);
214
338
 
@@ -238,6 +362,42 @@ function attributeName(name: string): string {
238
362
  return name === "className" ? "class" : name;
239
363
  }
240
364
 
365
+ function isSafeLinkAttributeName(name: string): boolean {
366
+ return VALID_LINK_ATTRIBUTE_NAME.test(name);
367
+ }
368
+
369
+ function isLinkEventAttributeName(name: string): boolean {
370
+ return /^on/i.test(name);
371
+ }
372
+
373
+ function reportRejectedBrowserEventProp(name: string): void {
374
+ const nodeEnv = (
375
+ globalThis as typeof globalThis & {
376
+ process?: { env?: { NODE_ENV?: string | undefined } | undefined } | undefined;
377
+ }
378
+ ).process?.env?.NODE_ENV;
379
+
380
+ if (nodeEnv !== "production") {
381
+ console.warn(
382
+ `[MR_LINK_UNSAFE_EVENT_PROP] Link rejected event-like prop ${JSON.stringify(name)}. Pass a supported camel-cased function handler such as onClick instead.`,
383
+ );
384
+ }
385
+ }
386
+
387
+ function linkStylePropertyName(property: string): string | undefined {
388
+ const cssProperty = property.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`);
389
+ return /^(?:--[a-zA-Z0-9_-]+|[a-zA-Z][a-zA-Z0-9-]*)$/.test(cssProperty) ? cssProperty : undefined;
390
+ }
391
+
392
+ function linkStyleValue(value: unknown): string | undefined {
393
+ if (value === null || value === undefined || value === false) {
394
+ return undefined;
395
+ }
396
+
397
+ const cssValue = String(value);
398
+ return /[;{}]/.test(cssValue) ? undefined : cssValue;
399
+ }
400
+
241
401
  function renderChildren(child: LinkChild): string {
242
402
  if (child === null || child === undefined || typeof child === "boolean") {
243
403
  return "";
@@ -254,6 +414,41 @@ function renderChildren(child: LinkChild): string {
254
414
  return escapeHtmlText(child);
255
415
  }
256
416
 
417
+ function renderSinkChildren(child: LinkSinkChild): string {
418
+ if (child === null || child === undefined || typeof child === "boolean") {
419
+ return "";
420
+ }
421
+
422
+ if (Array.isArray(child)) {
423
+ return child.map(renderSinkChildren).join("");
424
+ }
425
+
426
+ if (isTrustedLinkHtml(child)) {
427
+ return child[TRUSTED_LINK_HTML];
428
+ }
429
+
430
+ if (typeof child === "object") {
431
+ return "";
432
+ }
433
+
434
+ return escapeHtmlText(String(child));
435
+ }
436
+
437
+ function isSerializableSinkChild(value: unknown): value is LinkSinkChild {
438
+ if (
439
+ value === null ||
440
+ value === undefined ||
441
+ typeof value === "boolean" ||
442
+ typeof value === "number" ||
443
+ typeof value === "string" ||
444
+ isTrustedLinkHtml(value)
445
+ ) {
446
+ return true;
447
+ }
448
+
449
+ return Array.isArray(value) && value.every(isSerializableSinkChild);
450
+ }
451
+
257
452
  function isTrustedLinkHtml(value: unknown): value is TrustedLinkHtml {
258
453
  return (
259
454
  typeof value === "object" &&
package/src/render.ts CHANGED
@@ -53,7 +53,8 @@ import {
53
53
  import { serverActionCookie } from "./csrf.js";
54
54
  import {
55
55
  type AppRouterCache,
56
- beginRouteCacheContext,
56
+ activeRouteCacheContext,
57
+ withRouteCacheContext,
57
58
  cachedRouteResponse,
58
59
  cacheRouteResponse,
59
60
  routeCacheKey,
@@ -981,8 +982,7 @@ async function renderAppRequestInternal(
981
982
  script: string | undefined;
982
983
  }
983
984
  | undefined;
984
- let routeCacheContext: ReturnType<typeof beginRouteCacheContext> | undefined;
985
-
985
+ return (await withRouteCacheContext(options.routeCache, async () => {
986
986
  try {
987
987
  if (matched.route.kind === "asset") {
988
988
  return await dispatchConventionAssetRoute({
@@ -1031,7 +1031,6 @@ async function renderAppRequestInternal(
1031
1031
  return methodResponse;
1032
1032
  }
1033
1033
 
1034
- routeCacheContext = beginRouteCacheContext(options.routeCache);
1035
1034
  const clientScript = options.clientScripts?.get(matched.route.path);
1036
1035
  const clientStyleSheets = options.clientStyles?.get(matched.route.path);
1037
1036
  phaseStartedAt = renderTimingPhaseStartedAt(timing);
@@ -1607,7 +1606,7 @@ async function renderAppRequestInternal(
1607
1606
  preparedActions.csrfTokenIsNew === true,
1608
1607
  );
1609
1608
 
1610
- const effectiveCachePolicy = cachePolicy ?? routeCacheContext.cachePolicy;
1609
+ const effectiveCachePolicy = cachePolicy ?? activeRouteCacheContext()?.cachePolicy;
1611
1610
 
1612
1611
  const finalResponse = preparedActions.hasFormActions
1613
1612
  ? withRouteCacheHeader(response, effectiveCachePolicy)
@@ -1686,9 +1685,8 @@ async function renderAppRequestInternal(
1686
1685
  });
1687
1686
  emitRenderTiming(options, timing, response.status);
1688
1687
  return response;
1689
- } finally {
1690
- await routeCacheContext?.dispose();
1691
1688
  }
1689
+ })).value;
1692
1690
  }
1693
1691
 
1694
1692
  async function applyAppRouterResponseHook(
package/src/request.ts ADDED
@@ -0,0 +1,35 @@
1
+ /** Lightweight request/control-plane helpers that do not import router build or Vite modules. */
2
+ export { cacheControl, createMemoryRouteCache, revalidatePath } from "./cache.js";
3
+ export type {
4
+ AppRouterCache,
5
+ AppRouterCacheEntry,
6
+ CacheControlOptions,
7
+ MemoryRouteCacheOptions,
8
+ } from "./cache.js";
9
+ export { deleteCookie, parseCookieHeader, serializeCookie, setCookie } from "./cookies.js";
10
+ export type { CookieOptions } from "./cookies.js";
11
+ export { defer, isDeferredLoaderData } from "./deferred.js";
12
+ export type { DeferredLoaderData } from "./deferred.js";
13
+ export {
14
+ cookies,
15
+ headers,
16
+ html,
17
+ isNotFoundError,
18
+ isRedirectError,
19
+ json,
20
+ next,
21
+ notFound,
22
+ parseForm,
23
+ redirect,
24
+ redirect303,
25
+ redirectExternal,
26
+ rewrite,
27
+ textError,
28
+ throwNotFound,
29
+ } from "./navigation.js";
30
+ export type {
31
+ MiddlewareNext,
32
+ ParseSchema,
33
+ RedirectOptions,
34
+ RequestCookies,
35
+ } from "./navigation.js";
@@ -43,21 +43,21 @@ export interface DynamicHrefOptions<Path extends `/${string}`> extends StaticHre
43
43
  params: RouteParamsFor<Path>;
44
44
  }
45
45
 
46
- type HasRouteParams<Path extends `/${string}`> = keyof RouteParamsFor<Path> extends never
46
+ export type HasRouteParams<Path extends `/${string}`> = keyof RouteParamsFor<Path> extends never
47
47
  ? false
48
48
  : true;
49
49
 
50
- type ExtractRouteParams<Path extends string> = Path extends `${infer Segment}/${infer Rest}`
50
+ export type ExtractRouteParams<Path extends string> = Path extends `${infer Segment}/${infer Rest}`
51
51
  ? SegmentRouteParam<Segment> & ExtractRouteParams<Rest>
52
52
  : SegmentRouteParam<Path>;
53
53
 
54
- type SegmentRouteParam<Segment extends string> = Segment extends `:...${infer Name}`
54
+ export type SegmentRouteParam<Segment extends string> = Segment extends `:...${infer Name}`
55
55
  ? { [Key in Name]: readonly string[] }
56
56
  : Segment extends `:${infer Name}`
57
57
  ? { [Key in Name]: string }
58
58
  : Record<never, never>;
59
59
 
60
- type Simplify<T> = { [Key in keyof T]: T[Key] } & {};
60
+ export type Simplify<T> = { [Key in keyof T]: T[Key] } & {};
61
61
 
62
62
  /**
63
63
  * Represents the search and hash suffix portion of a typed route href.
package/src/vite.ts CHANGED
@@ -54,6 +54,32 @@ import { hasJsxSyntax } from "./source-jsx.js";
54
54
  import { prependTailwindSourceDirectives } from "./tailwind-source.js";
55
55
  import { workspacePackageFile } from "./workspace-packages.js";
56
56
 
57
+ export type { AppRouterAllowedServerAction, AppRouterServerActionOptions } from "./actions.js";
58
+ export type { AppRouterCache, AppRouterCacheEntry } from "./cache.js";
59
+ export type {
60
+ AppRouterBuildTarget,
61
+ AppRouterClientConsoleMethod,
62
+ AppRouterClientSourceMapMode,
63
+ AppRouterClientSourceMapOption,
64
+ AppRouterProductionOptions,
65
+ AppRouterProjectOptions,
66
+ ResolvedAppRouterProject,
67
+ } from "./config.js";
68
+ export type { AppRouterImportPolicy } from "./import-policy.js";
69
+ export type {
70
+ AppRouterCspInlineNonceWarningLogEvent,
71
+ AppRouterLogError,
72
+ AppRouterLogEvent,
73
+ AppRouterLogger,
74
+ AppRouterRuntime,
75
+ AppRouterRenderTimingLogEvent,
76
+ AppRouterRequestEndLogEvent,
77
+ AppRouterRequestErrorLogEvent,
78
+ AppRouterRequestStartLogEvent,
79
+ AppRouterRequestTimingLogEvent,
80
+ } from "./logger.js";
81
+ export type { RequestHostPolicy } from "./serve.js";
82
+
57
83
  /**
58
84
  * Configures the app-router Vite middleware used by development servers.
59
85
  */
@@ -316,9 +342,12 @@ export function cell(initial) {
316
342
 
317
343
  if (id === virtualReactiveDevtoolsId) {
318
344
  return `export function emitReactiveDevtoolsEvent() {}
345
+ export function emitReactiveEffectRunDevtoolsEvent() {}
319
346
  export function hasReactiveDevtoolsEmitter() { return false; }
320
347
  export function currentDevtoolsEmitter() { return undefined; }
321
- export function currentReactiveDevtools() { return undefined; }`;
348
+ export function currentReactiveDevtools() { return undefined; }
349
+ export function invalidateReactiveDevtoolsCache() {}
350
+ export function prepareReactiveEffectRunDevtoolsEvent() { return undefined; }`;
322
351
  }
323
352
 
324
353
  if (id.startsWith(virtualClientPrefix)) {