@reckona/mreact-router 0.0.188 → 0.0.190
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 +16 -3
- package/bin/mreact-router.js +23 -0
- package/dist/adapters/aws-lambda.d.ts +33 -13
- package/dist/adapters/aws-lambda.d.ts.map +1 -1
- package/dist/adapters/aws-lambda.js +128 -20
- package/dist/adapters/aws-lambda.js.map +1 -1
- package/dist/adapters/cloudflare.d.ts +18 -0
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js.map +1 -1
- package/dist/adapters/edge.d.ts +2 -0
- package/dist/adapters/edge.d.ts.map +1 -1
- package/dist/adapters/edge.js.map +1 -1
- package/dist/adapters/node.d.ts +16 -2
- package/dist/adapters/node.d.ts.map +1 -1
- package/dist/adapters/node.js.map +1 -1
- package/dist/cache.d.ts +8 -0
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +34 -13
- package/dist/cache.js.map +1 -1
- package/dist/cli-options.d.ts +1 -1
- package/dist/cli-options.d.ts.map +1 -1
- package/dist/cli-options.js +21 -3
- package/dist/cli-options.js.map +1 -1
- package/dist/cli.js +0 -0
- package/dist/client.d.ts +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +1 -0
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +4 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +6 -2
- package/dist/config.js.map +1 -1
- package/dist/index.d.ts +19 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -5
- package/dist/index.js.map +1 -1
- package/dist/link.d.ts +22 -1
- package/dist/link.d.ts.map +1 -1
- package/dist/link.js +130 -5
- package/dist/link.js.map +1 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +514 -517
- package/dist/render.js.map +1 -1
- package/dist/request.d.ts +10 -0
- package/dist/request.d.ts.map +1 -0
- package/dist/request.js +6 -0
- package/dist/request.js.map +1 -0
- package/dist/typed-routes.d.ts +4 -5
- package/dist/typed-routes.d.ts.map +1 -1
- package/dist/typed-routes.js.map +1 -1
- package/dist/vite.d.ts +6 -0
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +4 -1
- package/dist/vite.js.map +1 -1
- package/package.json +19 -13
- package/src/adapters/aws-lambda.ts +227 -56
- package/src/adapters/cloudflare.ts +59 -0
- package/src/adapters/edge.ts +14 -0
- package/src/adapters/node.ts +44 -2
- package/src/cache.ts +39 -17
- package/src/cli-options.ts +28 -6
- package/src/client.ts +2 -1
- package/src/config.ts +10 -2
- package/src/index.ts +56 -10
- package/src/link.ts +205 -10
- package/src/render.ts +5 -7
- package/src/request.ts +35 -0
- package/src/typed-routes.ts +4 -4
- package/src/vite.ts +30 -1
package/src/cli-options.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { AppRouterLogger, AppRouterLogEvent } from "./logger.js";
|
|
2
|
-
import
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
import {
|
|
3
|
+
appRouterBuildTargetMetadata,
|
|
4
|
+
type AppRouterBuildTarget,
|
|
5
|
+
type AppRouterClientSourceMapMode,
|
|
5
6
|
} from "./config.js";
|
|
6
7
|
import type { RequestHostPolicy } from "./serve.js";
|
|
7
8
|
|
|
@@ -178,6 +179,11 @@ export function parseCliArguments(argv: readonly string[]): ParsedCliArguments {
|
|
|
178
179
|
}
|
|
179
180
|
|
|
180
181
|
export function formatCliHelp(command?: string | undefined): string {
|
|
182
|
+
const defaultTargetLabel = formatBuildTargetDisplayList(
|
|
183
|
+
appRouterBuildTargetMetadata.defaultTargets,
|
|
184
|
+
);
|
|
185
|
+
const allTargetValues = formatBuildTargetValueList(appRouterBuildTargetMetadata.allTargets);
|
|
186
|
+
|
|
181
187
|
if (command === "build") {
|
|
182
188
|
return [
|
|
183
189
|
"Usage: mreact-router build [appDir] [options]",
|
|
@@ -186,7 +192,7 @@ export function formatCliHelp(command?: string | undefined): string {
|
|
|
186
192
|
"",
|
|
187
193
|
"Options:",
|
|
188
194
|
" --target=node|cloudflare|aws-lambda|all",
|
|
189
|
-
|
|
195
|
+
` Select build artifacts. Defaults to ${defaultTargetLabel.toLowerCase()}. all selects ${allTargetValues}. aws-lambda writes .mreact/aws-lambda/mreact-handler.mjs and .mreact/server/import-policy.json.`,
|
|
190
196
|
" --client-source-maps=none|hidden",
|
|
191
197
|
" Control production client source map output.",
|
|
192
198
|
" -h, --help",
|
|
@@ -196,6 +202,7 @@ export function formatCliHelp(command?: string | undefined): string {
|
|
|
196
202
|
" mreact-router build --target=node",
|
|
197
203
|
" mreact-router build --target=cloudflare",
|
|
198
204
|
" mreact-router build --target=aws-lambda",
|
|
205
|
+
" mreact-router build --target=all",
|
|
199
206
|
].join("\n");
|
|
200
207
|
}
|
|
201
208
|
|
|
@@ -272,7 +279,7 @@ export function formatCliHelp(command?: string | undefined): string {
|
|
|
272
279
|
"",
|
|
273
280
|
"Commands:",
|
|
274
281
|
" dev [appDir] Start the development server.",
|
|
275
|
-
|
|
282
|
+
` build [appDir] Build ${defaultTargetLabel} artifacts by default.`,
|
|
276
283
|
" build --target=aws-lambda Build Lambda artifacts including generated handler and import policy.",
|
|
277
284
|
" start [outDir] Serve built Node output.",
|
|
278
285
|
" package aws-lambda --from .mreact --out .lambda",
|
|
@@ -290,6 +297,7 @@ export function formatCliHelp(command?: string | undefined): string {
|
|
|
290
297
|
"Examples:",
|
|
291
298
|
" mreact-router build --target=cloudflare",
|
|
292
299
|
" mreact-router build --target=aws-lambda",
|
|
300
|
+
" mreact-router build --target=all",
|
|
293
301
|
" mreact-router package aws-lambda --from .mreact --out .lambda",
|
|
294
302
|
" mreact-router build --help",
|
|
295
303
|
].join("\n");
|
|
@@ -302,7 +310,21 @@ export function buildTargetsFromCliTarget(
|
|
|
302
310
|
return undefined;
|
|
303
311
|
}
|
|
304
312
|
|
|
305
|
-
return target === "all" ?
|
|
313
|
+
return target === "all" ? appRouterBuildTargetMetadata.allTargets : [target];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function formatBuildTargetDisplayList(targets: readonly AppRouterBuildTarget[]): string {
|
|
317
|
+
return targets.map(formatBuildTargetDisplayName).join(", ");
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function formatBuildTargetDisplayName(target: AppRouterBuildTarget): string {
|
|
321
|
+
if (target === "aws-lambda") return "AWS Lambda";
|
|
322
|
+
return target[0]?.toUpperCase() + target.slice(1);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function formatBuildTargetValueList(targets: readonly AppRouterBuildTarget[]): string {
|
|
326
|
+
if (targets.length < 2) return targets[0] ?? "";
|
|
327
|
+
return `${targets.slice(0, -1).join(", ")}, and ${targets.at(-1)}`;
|
|
306
328
|
}
|
|
307
329
|
|
|
308
330
|
export function resolveCliRequestLogMode(
|
package/src/client.ts
CHANGED
|
@@ -103,7 +103,7 @@ export interface ClientRouteInferenceCache {
|
|
|
103
103
|
transformedSourceByFile: Map<string, Promise<CachedClientRouteSource>>;
|
|
104
104
|
}
|
|
105
105
|
|
|
106
|
-
interface CachedClientRouteSource {
|
|
106
|
+
export interface CachedClientRouteSource {
|
|
107
107
|
signature: string;
|
|
108
108
|
source: string;
|
|
109
109
|
}
|
|
@@ -3535,6 +3535,7 @@ function __mreactSyncRouteDataScripts(root, currentRouteId, nextRouteId) {
|
|
|
3535
3535
|
|
|
3536
3536
|
function __mreactRouteDataScriptIds(...routeIds) {
|
|
3537
3537
|
const ids = new Set();
|
|
3538
|
+
ids.add("__mreact_auth_session");
|
|
3538
3539
|
|
|
3539
3540
|
for (const routeId of routeIds) {
|
|
3540
3541
|
if (typeof routeId !== "string" || routeId === "") {
|
package/src/config.ts
CHANGED
|
@@ -4,6 +4,14 @@ import { basename, dirname, isAbsolute, relative, resolve } from "node:path";
|
|
|
4
4
|
* Selects the deployment runtime artifacts emitted by an app-router build.
|
|
5
5
|
*/
|
|
6
6
|
export type AppRouterBuildTarget = "node" | "cloudflare" | "aws-lambda";
|
|
7
|
+
|
|
8
|
+
export const appRouterBuildTargetMetadata = {
|
|
9
|
+
allTargets: ["node", "cloudflare", "aws-lambda"],
|
|
10
|
+
defaultTargets: ["node"],
|
|
11
|
+
} as const satisfies {
|
|
12
|
+
allTargets: readonly AppRouterBuildTarget[];
|
|
13
|
+
defaultTargets: readonly AppRouterBuildTarget[];
|
|
14
|
+
};
|
|
7
15
|
/**
|
|
8
16
|
* Names a browser console method that can be stripped from client bundles.
|
|
9
17
|
*/
|
|
@@ -180,7 +188,7 @@ export function resolveBuildTargets(
|
|
|
180
188
|
targets: readonly AppRouterBuildTarget[] | undefined,
|
|
181
189
|
): readonly AppRouterBuildTarget[] {
|
|
182
190
|
if (targets === undefined) {
|
|
183
|
-
return
|
|
191
|
+
return appRouterBuildTargetMetadata.defaultTargets;
|
|
184
192
|
}
|
|
185
193
|
|
|
186
194
|
const uniqueTargets = [...new Set(targets)];
|
|
@@ -190,7 +198,7 @@ export function resolveBuildTargets(
|
|
|
190
198
|
}
|
|
191
199
|
|
|
192
200
|
for (const target of uniqueTargets) {
|
|
193
|
-
if (
|
|
201
|
+
if (!appRouterBuildTargetMetadata.allTargets.some((supported) => supported === target)) {
|
|
194
202
|
throw new Error(
|
|
195
203
|
`Unsupported mreactRouter build target ${JSON.stringify(target)}. Expected "node", "cloudflare", or "aws-lambda".`,
|
|
196
204
|
);
|
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,31 +58,62 @@ import type {
|
|
|
57
58
|
*
|
|
58
59
|
* @deprecated Import session helpers from `@reckona/mreact-auth` instead.
|
|
59
60
|
*/
|
|
60
|
-
export
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
87
119
|
BuildAppPhase,
|
|
@@ -90,10 +122,15 @@ export type {
|
|
|
90
122
|
BuildAppProgressEvent,
|
|
91
123
|
BuildAppResult,
|
|
92
124
|
BuiltImportPolicyArtifact,
|
|
125
|
+
BuiltPrerenderedRoute,
|
|
126
|
+
BuiltRouteSourceAnalysisSummary,
|
|
127
|
+
BuiltServerModuleArtifact,
|
|
128
|
+
BuiltServerModuleOutput,
|
|
93
129
|
CloudflarePagesArtifactManifest,
|
|
94
130
|
PackageAwsLambdaArtifactOptions,
|
|
95
131
|
PackageCloudflarePagesArtifactOptions,
|
|
96
132
|
} from "./build.js";
|
|
133
|
+
export type { AppRouterProjectOptions } from "./config.js";
|
|
97
134
|
export type { ServerActionContext } from "./actions.js";
|
|
98
135
|
export type {
|
|
99
136
|
AppRouteHref,
|
|
@@ -106,7 +143,11 @@ export type {
|
|
|
106
143
|
RouteParamsFor,
|
|
107
144
|
RouteSearchParams,
|
|
108
145
|
RouteSearchValue,
|
|
146
|
+
SegmentRouteParam,
|
|
109
147
|
StaticHrefOptions,
|
|
148
|
+
ExtractRouteParams,
|
|
149
|
+
HasRouteParams,
|
|
150
|
+
Simplify,
|
|
110
151
|
} from "./typed-routes.js";
|
|
111
152
|
export type {
|
|
112
153
|
InferLoaderData,
|
|
@@ -180,7 +221,7 @@ export type {
|
|
|
180
221
|
} from "./navigation-state.js";
|
|
181
222
|
export type { RouterRuntimeCacheStat } from "./runtime-cache.js";
|
|
182
223
|
/** Configures the deprecated router memory session store alias. */
|
|
183
|
-
export type {
|
|
224
|
+
export type { MemorySessionStoreOptions } from "./session.js";
|
|
184
225
|
export type {
|
|
185
226
|
AppRouterCspInlineNonceWarningLogEvent,
|
|
186
227
|
AppRouterLogError,
|
|
@@ -211,23 +252,24 @@ export type {
|
|
|
211
252
|
*
|
|
212
253
|
* @deprecated Import session helpers and types from `@reckona/mreact-auth` instead.
|
|
213
254
|
*/
|
|
214
|
-
export type SessionCookieOptions
|
|
255
|
+
export type { SessionCookieOptions } from "./session.js";
|
|
215
256
|
/**
|
|
216
257
|
* Stores session data through the deprecated router session record alias.
|
|
217
258
|
*
|
|
218
259
|
* @deprecated Import session helpers and types from `@reckona/mreact-auth` instead.
|
|
219
260
|
*/
|
|
220
|
-
export type SessionRecord
|
|
261
|
+
export type { SessionRecord } from "./session.js";
|
|
221
262
|
/**
|
|
222
263
|
* Defines session persistence through the deprecated router session store alias.
|
|
223
264
|
*
|
|
224
265
|
* @deprecated Import session helpers and types from `@reckona/mreact-auth` instead.
|
|
225
266
|
*/
|
|
226
|
-
export type SessionStore
|
|
267
|
+
export type { SessionStore } from "./session.js";
|
|
227
268
|
export { startDevServer } from "./dev-server.js";
|
|
228
269
|
export type { StartDevServerOptions } from "./dev-server.js";
|
|
229
270
|
export { renderAppRequest } from "./render.js";
|
|
230
271
|
export type {
|
|
272
|
+
AppRouterRenderPreload,
|
|
231
273
|
AppRouterResponseHook,
|
|
232
274
|
AppRouterResponseHookContext,
|
|
233
275
|
RenderAppRequestOptions,
|
|
@@ -250,6 +292,7 @@ export type {
|
|
|
250
292
|
FileSystemPrerenderStoreOptions,
|
|
251
293
|
KeyValuePrerenderStoreAdapter,
|
|
252
294
|
KeyValuePrerenderStoreOptions,
|
|
295
|
+
MemoryPrerenderStoreEntry,
|
|
253
296
|
MemoryPrerenderStoreOptions,
|
|
254
297
|
} from "./prerender-store.js";
|
|
255
298
|
export { createFileSystemPrerenderStore, createKeyValuePrerenderStore } from "./prerender-store.js";
|
|
@@ -260,9 +303,12 @@ export type {
|
|
|
260
303
|
BuiltAppRuntimePreloadStrategy,
|
|
261
304
|
RenderBuiltAppRequestOptions,
|
|
262
305
|
RequestHostPolicy,
|
|
306
|
+
ResponseSinkStrategy,
|
|
263
307
|
StartServerOptions,
|
|
264
308
|
} from "./serve.js";
|
|
265
309
|
export { matchRoute, scanAppRoutes } from "./routes.js";
|
|
310
|
+
export type { CachedClientRouteSource, ClientRouteInferenceCache } from "./client.js";
|
|
311
|
+
export type { RouteMatcher, ScanAppRoutesOptions } from "./routes.js";
|
|
266
312
|
export type { AppFileConvention } from "./file-conventions.js";
|
|
267
313
|
export type {
|
|
268
314
|
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 ||
|
|
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:
|
|
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
|
-
(
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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 ??
|
|
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";
|