@reckona/mreact-router 0.0.152 → 0.0.154
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 +1 -1
- package/dist/actions.d.ts +10 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +6 -0
- package/dist/actions.js.map +1 -1
- package/dist/adapters/aws-lambda.d.ts +18 -0
- package/dist/adapters/aws-lambda.d.ts.map +1 -1
- package/dist/adapters/aws-lambda.js +18 -0
- package/dist/adapters/aws-lambda.js.map +1 -1
- package/dist/adapters/cloudflare.d.ts +33 -0
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +33 -0
- package/dist/adapters/cloudflare.js.map +1 -1
- package/dist/adapters/edge.d.ts +5 -0
- package/dist/adapters/edge.d.ts.map +1 -1
- package/dist/adapters/edge.js +5 -0
- package/dist/adapters/edge.js.map +1 -1
- package/dist/adapters/node.d.ts +5 -0
- package/dist/adapters/node.d.ts.map +1 -1
- package/dist/adapters/node.js +5 -0
- package/dist/adapters/node.js.map +1 -1
- package/dist/adapters/static.d.ts +5 -0
- package/dist/adapters/static.d.ts.map +1 -1
- package/dist/adapters/static.js +5 -0
- package/dist/adapters/static.js.map +1 -1
- package/dist/build.d.ts +15 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +85 -2
- package/dist/build.js.map +1 -1
- package/dist/cache.d.ts +15 -0
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +15 -0
- package/dist/cache.js.map +1 -1
- package/dist/cookies.d.ts +16 -0
- package/dist/cookies.d.ts.map +1 -1
- package/dist/cookies.js +16 -0
- package/dist/cookies.js.map +1 -1
- package/dist/dev-server.d.ts +5 -0
- package/dist/dev-server.d.ts.map +1 -1
- package/dist/dev-server.js +5 -0
- package/dist/dev-server.js.map +1 -1
- package/dist/link.d.ts +10 -0
- package/dist/link.d.ts.map +1 -1
- package/dist/link.js +5 -0
- package/dist/link.js.map +1 -1
- package/dist/middleware.d.ts.map +1 -1
- package/dist/middleware.js +39 -4
- package/dist/middleware.js.map +1 -1
- package/dist/module-runner.d.ts +6 -0
- package/dist/module-runner.d.ts.map +1 -1
- package/dist/module-runner.js +71 -0
- package/dist/module-runner.js.map +1 -1
- package/dist/navigation.d.ts +53 -0
- package/dist/navigation.d.ts.map +1 -1
- package/dist/navigation.js +48 -0
- package/dist/navigation.js.map +1 -1
- package/dist/render.d.ts +34 -0
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +161 -27
- package/dist/render.js.map +1 -1
- package/dist/route-module-loader.d.ts +6 -1
- package/dist/route-module-loader.d.ts.map +1 -1
- package/dist/route-module-loader.js +8 -1
- package/dist/route-module-loader.js.map +1 -1
- package/dist/serve.d.ts +5 -0
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +5 -0
- package/dist/serve.js.map +1 -1
- package/dist/session.d.ts +19 -0
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +19 -0
- package/dist/session.js.map +1 -1
- package/dist/typed-routes.d.ts +5 -0
- package/dist/typed-routes.d.ts.map +1 -1
- package/dist/typed-routes.js +5 -0
- package/dist/typed-routes.js.map +1 -1
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -1
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +1 -0
- package/dist/vite.js.map +1 -1
- package/package.json +11 -11
- package/src/actions.ts +18 -0
- package/src/adapters/aws-lambda.ts +18 -0
- package/src/adapters/cloudflare.ts +33 -0
- package/src/adapters/edge.ts +5 -0
- package/src/adapters/node.ts +5 -0
- package/src/adapters/static.ts +5 -0
- package/src/build.ts +118 -1
- package/src/cache.ts +15 -0
- package/src/cookies.ts +16 -0
- package/src/dev-server.ts +5 -0
- package/src/link.ts +10 -0
- package/src/middleware.ts +54 -4
- package/src/module-runner.ts +91 -0
- package/src/navigation.ts +53 -0
- package/src/render.ts +229 -25
- package/src/route-module-loader.ts +15 -0
- package/src/serve.ts +5 -0
- package/src/session.ts +19 -0
- package/src/typed-routes.ts +5 -0
- package/src/types.ts +15 -0
- package/src/vite.ts +1 -0
package/src/build.ts
CHANGED
|
@@ -44,9 +44,12 @@ import {
|
|
|
44
44
|
type BuildClientRouteOutputOptions,
|
|
45
45
|
} from "./navigation-runtime.js";
|
|
46
46
|
import {
|
|
47
|
+
COMPAT_VENDOR_PLACEHOLDER_PREFIX,
|
|
47
48
|
bundleAppRouterSourceModule,
|
|
48
49
|
fileImportMetaUrlPlugin,
|
|
49
50
|
importAppRouterSourceModule,
|
|
51
|
+
resolveCompatVendorEntryFiles,
|
|
52
|
+
sourceReferencesCompatVendorSpecifier,
|
|
50
53
|
} from "./module-runner.js";
|
|
51
54
|
import { scanAppRoutes } from "./routes.js";
|
|
52
55
|
import type { AppRoute } from "./routes.js";
|
|
@@ -334,6 +337,11 @@ export interface BuiltPrerenderedRoute {
|
|
|
334
337
|
|
|
335
338
|
type StaticParams = Record<string, string | number | boolean | readonly string[]>;
|
|
336
339
|
|
|
340
|
+
/**
|
|
341
|
+
* Builds an app-router project into server, client, and optional deployment-target artifacts.
|
|
342
|
+
*
|
|
343
|
+
* Use this from custom build scripts when the CLI is too coarse-grained; the returned manifest paths describe the files written under `outDir`. The build reads route files, loaders, middleware, metadata, server actions, and client references, so callers should pass the same project root and source allow-list they expect to deploy.
|
|
344
|
+
*/
|
|
337
345
|
export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult> {
|
|
338
346
|
const timingSink = options.onBuildPhaseTiming;
|
|
339
347
|
const progressSink = options.onBuildProgress;
|
|
@@ -952,6 +960,85 @@ async function buildPublicAssetManifest(
|
|
|
952
960
|
return [...new Set([...publicAssetPaths, ...appConventionPublicAssets])].sort();
|
|
953
961
|
}
|
|
954
962
|
|
|
963
|
+
const COMPAT_VENDOR_PLACEHOLDER_IMPORT_PATTERN = /(["'])mreact-compat-vendor:([\w-]+)\1/gu;
|
|
964
|
+
|
|
965
|
+
function rewriteCompatVendorPlaceholderImports(code: string): string {
|
|
966
|
+
if (!code.includes(COMPAT_VENDOR_PLACEHOLDER_PREFIX)) {
|
|
967
|
+
return code;
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
// Module files live in server-modules/code/, vendor chunks in
|
|
971
|
+
// server-modules/chunks/, so the relative path is stable.
|
|
972
|
+
return code.replace(
|
|
973
|
+
COMPAT_VENDOR_PLACEHOLDER_IMPORT_PATTERN,
|
|
974
|
+
(_match, quote: string, entry: string) => `${quote}../chunks/compat.${entry}.mjs${quote}`,
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function collectCompatVendorEntryUsage(
|
|
979
|
+
artifacts: ReadonlyArray<readonly [string, BuiltServerModuleArtifact]>,
|
|
980
|
+
): ReadonlySet<string> {
|
|
981
|
+
const usedEntries = new Set<string>();
|
|
982
|
+
|
|
983
|
+
for (const [, artifact] of artifacts) {
|
|
984
|
+
for (const output of [artifact.string, artifact.stream]) {
|
|
985
|
+
const bundleCode = output?.bundleCode;
|
|
986
|
+
|
|
987
|
+
if (bundleCode === undefined || !bundleCode.includes(COMPAT_VENDOR_PLACEHOLDER_PREFIX)) {
|
|
988
|
+
continue;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
for (const match of bundleCode.matchAll(COMPAT_VENDOR_PLACEHOLDER_IMPORT_PATTERN)) {
|
|
992
|
+
const entry = match[2];
|
|
993
|
+
|
|
994
|
+
if (entry !== undefined) {
|
|
995
|
+
usedEntries.add(entry);
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
return usedEntries;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
// Bundles the react-compat server runtime once per build into shared chunks
|
|
1005
|
+
// under server-modules/chunks/ so per-route server modules can import it via
|
|
1006
|
+
// relative paths instead of inlining their own copy.
|
|
1007
|
+
async function writeCompatVendorChunks(
|
|
1008
|
+
serverDir: string,
|
|
1009
|
+
usedEntries: ReadonlySet<string>,
|
|
1010
|
+
): Promise<void> {
|
|
1011
|
+
const entryFiles = resolveCompatVendorEntryFiles(serverDir);
|
|
1012
|
+
const entries = await Promise.all(
|
|
1013
|
+
[...entryFiles]
|
|
1014
|
+
.filter(([name]) => usedEntries.has(name))
|
|
1015
|
+
.map(async ([name, filename]) => ({
|
|
1016
|
+
code: await readFile(filename, "utf8"),
|
|
1017
|
+
filename,
|
|
1018
|
+
name,
|
|
1019
|
+
})),
|
|
1020
|
+
);
|
|
1021
|
+
const firstEntry = entries[0];
|
|
1022
|
+
|
|
1023
|
+
if (firstEntry === undefined) {
|
|
1024
|
+
return;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
const output = await bundleRouterModules({
|
|
1028
|
+
chunkFileNames: "compat-shared.[hash].mjs",
|
|
1029
|
+
entries,
|
|
1030
|
+
entryFileNames: "compat.[name].mjs",
|
|
1031
|
+
platform: "node",
|
|
1032
|
+
root: dirname(firstEntry.filename),
|
|
1033
|
+
});
|
|
1034
|
+
const chunksDir = join(serverDir, "server-modules", "chunks");
|
|
1035
|
+
|
|
1036
|
+
await mkdir(chunksDir, { recursive: true });
|
|
1037
|
+
await Promise.all(
|
|
1038
|
+
output.chunks.map((chunk) => writeFile(join(chunksDir, chunk.fileName), chunk.code)),
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
955
1042
|
async function writeServerModuleArtifactFiles(
|
|
956
1043
|
serverDir: string,
|
|
957
1044
|
serverModules: Record<string, BuiltServerModuleArtifact>,
|
|
@@ -977,6 +1064,12 @@ async function writeServerModuleArtifactFiles(
|
|
|
977
1064
|
mkdir(join(modulesDir, "render"), { recursive: true }),
|
|
978
1065
|
]);
|
|
979
1066
|
|
|
1067
|
+
const usedCompatVendorEntries = collectCompatVendorEntryUsage(artifactEntries);
|
|
1068
|
+
|
|
1069
|
+
if (usedCompatVendorEntries.size > 0) {
|
|
1070
|
+
await writeCompatVendorChunks(serverDir, usedCompatVendorEntries);
|
|
1071
|
+
}
|
|
1072
|
+
|
|
980
1073
|
const writtenArtifacts = await mapWithBuildConcurrency<
|
|
981
1074
|
[string, BuiltServerModuleArtifact],
|
|
982
1075
|
WrittenServerModuleArtifact
|
|
@@ -1106,7 +1199,9 @@ async function externalizeServerModuleOutputCode(
|
|
|
1106
1199
|
const moduleCode =
|
|
1107
1200
|
sourceCode === undefined
|
|
1108
1201
|
? undefined
|
|
1109
|
-
:
|
|
1202
|
+
: rewriteCompatVendorPlaceholderImports(
|
|
1203
|
+
rewritePortableRuntimePackageImports(sourceCode, portableRuntimePackages),
|
|
1204
|
+
);
|
|
1110
1205
|
|
|
1111
1206
|
if (moduleCode === undefined || moduleCode.length === 0) {
|
|
1112
1207
|
return output;
|
|
@@ -1914,6 +2009,11 @@ async function buildServerModuleArtifacts(options: {
|
|
|
1914
2009
|
const routeByFile = new Map(
|
|
1915
2010
|
options.routes.map((route) => [relative(options.projectRoot, route.file), route]),
|
|
1916
2011
|
);
|
|
2012
|
+
// One shared compat vendor chunk set replaces per-route inlining whenever
|
|
2013
|
+
// any app source references the react/compat specifier family.
|
|
2014
|
+
const externalizeCompatVendor =
|
|
2015
|
+
options.prebundleServerComponents &&
|
|
2016
|
+
Object.values(options.files).some(sourceReferencesCompatVendorSpecifier);
|
|
1917
2017
|
const loaderArtifactFiles = new Set<string>();
|
|
1918
2018
|
const metadataArtifactFiles = new Set<string>();
|
|
1919
2019
|
const requestArtifactFiles = new Set<string>();
|
|
@@ -2134,6 +2234,10 @@ async function buildServerModuleArtifacts(options: {
|
|
|
2134
2234
|
filename: join(options.projectRoot, file),
|
|
2135
2235
|
moduleContextCache: options.clientRouteInferenceCache,
|
|
2136
2236
|
serverOutput,
|
|
2237
|
+
...(serverOutput === "stream" &&
|
|
2238
|
+
(routeAnalysis?.clientRoute ?? clientInference?.client) === true
|
|
2239
|
+
? { serverAwaitHydration: true as const }
|
|
2240
|
+
: {}),
|
|
2137
2241
|
});
|
|
2138
2242
|
const fatalDiagnostics = output.diagnostics.filter(
|
|
2139
2243
|
(diagnostic) => diagnostic.code !== "MR_UNSUPPORTED_SERVER_EVENT_HANDLER",
|
|
@@ -2177,6 +2281,7 @@ async function buildServerModuleArtifacts(options: {
|
|
|
2177
2281
|
bundleCode: await buildServerComponentBundleArtifactCode({
|
|
2178
2282
|
clientRouteInferenceCache: options.clientRouteInferenceCache,
|
|
2179
2283
|
code: output.code,
|
|
2284
|
+
externalizeCompatVendor,
|
|
2180
2285
|
filename: absoluteFile,
|
|
2181
2286
|
define: options.define,
|
|
2182
2287
|
root: options.projectRoot,
|
|
@@ -2216,6 +2321,7 @@ async function buildServerComponentBundleArtifactCode(options: {
|
|
|
2216
2321
|
clientRouteInferenceCache: ClientRouteInferenceCache;
|
|
2217
2322
|
code: string;
|
|
2218
2323
|
define?: UserConfig["define"] | undefined;
|
|
2324
|
+
externalizeCompatVendor?: boolean | undefined;
|
|
2219
2325
|
filename: string;
|
|
2220
2326
|
root?: string | undefined;
|
|
2221
2327
|
serverOutput: ServerOutputMode;
|
|
@@ -2224,6 +2330,7 @@ async function buildServerComponentBundleArtifactCode(options: {
|
|
|
2224
2330
|
return await bundleAppRouterSourceModule({
|
|
2225
2331
|
code: options.code,
|
|
2226
2332
|
define: options.define,
|
|
2333
|
+
externalizeCompatVendor: options.externalizeCompatVendor,
|
|
2227
2334
|
label: `server-component:${options.filename}`,
|
|
2228
2335
|
resolveDir: dirname(options.filename),
|
|
2229
2336
|
root: options.root,
|
|
@@ -5612,6 +5719,11 @@ async function writeCloudflareWorkerArtifact(options: {
|
|
|
5612
5719
|
);
|
|
5613
5720
|
}
|
|
5614
5721
|
|
|
5722
|
+
/**
|
|
5723
|
+
* Packages a built app-router output directory into the minimal AWS Lambda artifact layout.
|
|
5724
|
+
*
|
|
5725
|
+
* The package contains `.mreact`, a Lambda handler, project package metadata, production dependencies, and `mreact-lambda-artifact.json`; it intentionally excludes source files and development tooling that are not needed at runtime.
|
|
5726
|
+
*/
|
|
5615
5727
|
export async function packageAwsLambdaArtifact(
|
|
5616
5728
|
options: PackageAwsLambdaArtifactOptions,
|
|
5617
5729
|
): Promise<AwsLambdaArtifactManifest> {
|
|
@@ -5719,6 +5831,11 @@ async function assertAwsLambdaRuntimeDependencies(outDir: string): Promise<void>
|
|
|
5719
5831
|
);
|
|
5720
5832
|
}
|
|
5721
5833
|
|
|
5834
|
+
/**
|
|
5835
|
+
* Packages a Cloudflare-target build for Cloudflare Pages advanced mode.
|
|
5836
|
+
*
|
|
5837
|
+
* The package writes `_worker.js`, generated client assets, public assets, and `mreact-cloudflare-pages-artifact.json`; deploy the output directory rather than the raw `.mreact` build tree.
|
|
5838
|
+
*/
|
|
5722
5839
|
export async function packageCloudflarePagesArtifact(
|
|
5723
5840
|
options: PackageCloudflarePagesArtifactOptions,
|
|
5724
5841
|
): Promise<CloudflarePagesArtifactManifest> {
|
package/src/cache.ts
CHANGED
|
@@ -56,6 +56,11 @@ const cacheState = ((
|
|
|
56
56
|
});
|
|
57
57
|
cacheState.storage ??= new AsyncLocalStorage<RouteCacheContext>();
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Creates an in-memory route response cache for app-router rendering.
|
|
61
|
+
*
|
|
62
|
+
* The cache is process-local, evicts expired entries during reads/writes, and is suitable for development, tests, or single-process deployments that do not need shared invalidation.
|
|
63
|
+
*/
|
|
59
64
|
export function createMemoryRouteCache(options: MemoryRouteCacheOptions = {}): AppRouterCache {
|
|
60
65
|
const maxEntries = positiveIntegerOrDefault(options.maxEntries, 10_000);
|
|
61
66
|
const sweepIntervalMs = nonNegativeIntegerOrDefault(options.sweepIntervalMs, 60_000);
|
|
@@ -192,6 +197,11 @@ export function routeCachePolicyFromSource(code: string): RouteCachePolicy | und
|
|
|
192
197
|
};
|
|
193
198
|
}
|
|
194
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Sets the cache policy for the current app-router render.
|
|
202
|
+
*
|
|
203
|
+
* Call this during a loader or route render to override the response `Cache-Control` policy; it throws outside an active app-router request.
|
|
204
|
+
*/
|
|
195
205
|
export function cacheControl(options: CacheControlOptions): void {
|
|
196
206
|
const activeContext = activeRouteCacheContext();
|
|
197
207
|
|
|
@@ -326,6 +336,11 @@ function requestCarriesCredentials(request: Request | undefined): boolean {
|
|
|
326
336
|
return request.headers.has("authorization") || request.headers.has("cookie");
|
|
327
337
|
}
|
|
328
338
|
|
|
339
|
+
/**
|
|
340
|
+
* Invalidates cached route responses for a normalized app-router path.
|
|
341
|
+
*
|
|
342
|
+
* During a request the invalidation is scoped to that route cache context; outside a request it is queued for the next cache access.
|
|
343
|
+
*/
|
|
329
344
|
export function revalidatePath(path: string): void {
|
|
330
345
|
const normalizedPath = normalizeRevalidationPath(path);
|
|
331
346
|
const activeContext = activeRouteCacheContext();
|
package/src/cookies.ts
CHANGED
|
@@ -22,6 +22,11 @@ function assertAttributeValue(value: string): void {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
/**
|
|
26
|
+
* Parses a raw `Cookie` header into decoded name/value pairs.
|
|
27
|
+
*
|
|
28
|
+
* Malformed percent-encoded values are skipped so one bad cookie does not abort request handling.
|
|
29
|
+
*/
|
|
25
30
|
export function parseCookieHeader(
|
|
26
31
|
cookieHeader: string | null | undefined,
|
|
27
32
|
): Map<string, string> {
|
|
@@ -50,6 +55,11 @@ export function parseCookieHeader(
|
|
|
50
55
|
return values;
|
|
51
56
|
}
|
|
52
57
|
|
|
58
|
+
/**
|
|
59
|
+
* Serializes a cookie name, value, and attributes for a `Set-Cookie` header.
|
|
60
|
+
*
|
|
61
|
+
* Cookie names and attribute values are validated, and `SameSite=None` requires `Secure`.
|
|
62
|
+
*/
|
|
53
63
|
export function serializeCookie(
|
|
54
64
|
name: string,
|
|
55
65
|
value: string,
|
|
@@ -91,6 +101,9 @@ export function serializeCookie(
|
|
|
91
101
|
return parts.join("; ");
|
|
92
102
|
}
|
|
93
103
|
|
|
104
|
+
/**
|
|
105
|
+
* Appends a serialized cookie to a response's `Set-Cookie` headers and returns the same response.
|
|
106
|
+
*/
|
|
94
107
|
export function setCookie(
|
|
95
108
|
response: Response,
|
|
96
109
|
name: string,
|
|
@@ -101,6 +114,9 @@ export function setCookie(
|
|
|
101
114
|
return response;
|
|
102
115
|
}
|
|
103
116
|
|
|
117
|
+
/**
|
|
118
|
+
* Appends an expiring `Set-Cookie` header that removes a cookie in the browser.
|
|
119
|
+
*/
|
|
104
120
|
export function deleteCookie(
|
|
105
121
|
response: Response,
|
|
106
122
|
name: string,
|
package/src/dev-server.ts
CHANGED
|
@@ -33,6 +33,11 @@ export interface StartDevServerOptions extends AppRouterProjectOptions {
|
|
|
33
33
|
onUpgrade?: HttpUpgradeHandler | undefined;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
/**
|
|
37
|
+
* Starts the local app-router development server with Vite middleware and route compilation.
|
|
38
|
+
*
|
|
39
|
+
* The server trusts only the configured host settings, merges declared application packages into the development import policy, and returns a `close()` method that should be awaited by tests and scripts.
|
|
40
|
+
*/
|
|
36
41
|
export async function startDevServer(
|
|
37
42
|
options: StartDevServerOptions,
|
|
38
43
|
): Promise<{ close(): Promise<void>; server: Server; url: string }> {
|
package/src/link.ts
CHANGED
|
@@ -39,6 +39,11 @@ export interface LinkProps<Href extends string = LinkHref> extends LinkOptions<H
|
|
|
39
39
|
[attribute: string]: unknown;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Converts router link options into anchor attributes consumed by the client navigation runtime.
|
|
44
|
+
*
|
|
45
|
+
* Use this when rendering a custom anchor component that should still opt into mreact prefetch, scroll, reload, or transition behavior.
|
|
46
|
+
*/
|
|
42
47
|
export function linkProps(options: LinkOptions<string>): Record<string, string> {
|
|
43
48
|
return {
|
|
44
49
|
href: options.href,
|
|
@@ -61,6 +66,11 @@ export type ConcreteLinkHrefGuard<Href extends string> = [RegisteredAppRoutePath
|
|
|
61
66
|
? { readonly __mreactRoutePatternHrefError__: never }
|
|
62
67
|
: unknown;
|
|
63
68
|
|
|
69
|
+
/**
|
|
70
|
+
* Renders an app-router anchor with typed `href` support and navigation runtime attributes.
|
|
71
|
+
*
|
|
72
|
+
* In JSX it returns an anchor element compatible with the mreact runtime; during server streaming it can also write directly to an `HtmlSink`. Unsafe URL attribute values are dropped during rendering.
|
|
73
|
+
*/
|
|
64
74
|
export function Link<const Href extends LinkHref>(
|
|
65
75
|
props: LinkProps<Href> & ConcreteLinkHrefGuard<Href>,
|
|
66
76
|
): ReactCompatElement;
|
package/src/middleware.ts
CHANGED
|
@@ -110,28 +110,78 @@ export function middlewareMatches(config: MiddlewareModule["config"], pathname:
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
export function parseRouteMiddlewareControl(code: string): RouteMiddlewareControl | undefined {
|
|
113
|
-
|
|
113
|
+
const masked = maskCodeLiterals(code);
|
|
114
|
+
const declaration = /\bexport\s+const\s+middleware\s*=\s*\{/.exec(masked);
|
|
115
|
+
|
|
116
|
+
if (declaration === null) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const objectStart = masked.indexOf("{", declaration.index);
|
|
121
|
+
const objectEnd = findMatchingBrace(masked, objectStart);
|
|
122
|
+
|
|
123
|
+
if (objectStart < 0 || objectEnd < 0) {
|
|
114
124
|
return undefined;
|
|
115
125
|
}
|
|
116
126
|
|
|
117
|
-
|
|
127
|
+
const maskedBody = masked.slice(objectStart + 1, objectEnd);
|
|
128
|
+
const body = code.slice(objectStart + 1, objectEnd);
|
|
129
|
+
|
|
130
|
+
if (/\bskip\s*:\s*true\b/.test(maskedBody)) {
|
|
118
131
|
return { skip: true };
|
|
119
132
|
}
|
|
120
133
|
|
|
121
|
-
const skipArray = /\
|
|
134
|
+
const skipArray = /\bskip\s*:\s*\[([\s\S]*?)\]/.exec(maskedBody);
|
|
122
135
|
|
|
123
136
|
if (skipArray === null) {
|
|
124
137
|
return undefined;
|
|
125
138
|
}
|
|
126
139
|
|
|
140
|
+
const arrayStart = skipArray.index + skipArray[0].indexOf("[") + 1;
|
|
141
|
+
const arrayEnd = skipArray.index + skipArray[0].lastIndexOf("]");
|
|
142
|
+
const arrayBody = body.slice(arrayStart, arrayEnd);
|
|
127
143
|
const ids = Array.from(
|
|
128
|
-
|
|
144
|
+
arrayBody.matchAll(/["']([^"']+)["']/g),
|
|
129
145
|
(match) => match[1],
|
|
130
146
|
).filter((id) => id !== undefined);
|
|
131
147
|
|
|
132
148
|
return ids.length === 0 ? undefined : { skip: ids };
|
|
133
149
|
}
|
|
134
150
|
|
|
151
|
+
function maskCodeLiterals(code: string): string {
|
|
152
|
+
return code.replace(
|
|
153
|
+
/\/\*[\s\S]*?\*\/|\/\/[^\n\r]*|"(?:\\[\s\S]|[^"\\])*"|'(?:\\[\s\S]|[^'\\])*'|`(?:\\[\s\S]|[^`\\])*`/g,
|
|
154
|
+
(match) => " ".repeat(match.length),
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function findMatchingBrace(code: string, openIndex: number): number {
|
|
159
|
+
if (openIndex < 0) {
|
|
160
|
+
return -1;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
let depth = 0;
|
|
164
|
+
|
|
165
|
+
for (let index = openIndex; index < code.length; index += 1) {
|
|
166
|
+
const char = code[index];
|
|
167
|
+
|
|
168
|
+
if (char === "{") {
|
|
169
|
+
depth += 1;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (char === "}") {
|
|
174
|
+
depth -= 1;
|
|
175
|
+
|
|
176
|
+
if (depth === 0) {
|
|
177
|
+
return index;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return -1;
|
|
183
|
+
}
|
|
184
|
+
|
|
135
185
|
export function mergeRouteMiddlewareControls(
|
|
136
186
|
controls: readonly (RouteMiddlewareControl | undefined)[],
|
|
137
187
|
): RouteMiddlewareControl | undefined {
|
package/src/module-runner.ts
CHANGED
|
@@ -274,10 +274,100 @@ export async function importAppRouterBuiltFileModule<T>(options: {
|
|
|
274
274
|
return (await import(pathToFileURL(options.file).href)) as T;
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
+
|
|
278
|
+
export const COMPAT_VENDOR_PLACEHOLDER_PREFIX = "mreact-compat-vendor:";
|
|
279
|
+
|
|
280
|
+
// Specifier-to-dist-entry table for the react-compat server family. The
|
|
281
|
+
// per-route externalization plugin and the shared vendor chunk build must
|
|
282
|
+
// agree exactly with the workspace alias table above, so both derive from
|
|
283
|
+
// this map.
|
|
284
|
+
const compatVendorSpecifierEntries = new Map<string, string>([
|
|
285
|
+
["react", "index"],
|
|
286
|
+
["react-dom", "index"],
|
|
287
|
+
["react-dom/client", "index"],
|
|
288
|
+
["react-dom/server", "index"],
|
|
289
|
+
["react/jsx-dev-runtime", "jsx-dev-runtime"],
|
|
290
|
+
["react/jsx-runtime", "jsx-runtime"],
|
|
291
|
+
["@reckona/mreact-compat", "index"],
|
|
292
|
+
["@reckona/mreact-compat/event-priority", "event-priority"],
|
|
293
|
+
["@reckona/mreact-compat/flight", "flight"],
|
|
294
|
+
["@reckona/mreact-compat/hooks", "hooks-entry"],
|
|
295
|
+
["@reckona/mreact-compat/internal", "internal"],
|
|
296
|
+
["@reckona/mreact-compat/jsx-dev-runtime", "jsx-dev-runtime"],
|
|
297
|
+
["@reckona/mreact-compat/jsx-runtime", "jsx-runtime"],
|
|
298
|
+
["@reckona/mreact-compat/scheduler", "scheduler"],
|
|
299
|
+
]);
|
|
300
|
+
|
|
301
|
+
const compatVendorEntrySpecifiers = new Map<string, string>([
|
|
302
|
+
["index", "@reckona/mreact-compat"],
|
|
303
|
+
["jsx-runtime", "@reckona/mreact-compat/jsx-runtime"],
|
|
304
|
+
["jsx-dev-runtime", "@reckona/mreact-compat/jsx-dev-runtime"],
|
|
305
|
+
["hooks-entry", "@reckona/mreact-compat/hooks"],
|
|
306
|
+
["scheduler", "@reckona/mreact-compat/scheduler"],
|
|
307
|
+
["event-priority", "@reckona/mreact-compat/event-priority"],
|
|
308
|
+
["flight", "@reckona/mreact-compat/flight"],
|
|
309
|
+
["internal", "@reckona/mreact-compat/internal"],
|
|
310
|
+
]);
|
|
311
|
+
|
|
312
|
+
export function compatVendorEntryNames(): readonly string[] {
|
|
313
|
+
return [...compatVendorEntrySpecifiers.keys()];
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Cheap build-time prefilter. A false positive only costs one unused vendor
|
|
317
|
+
// chunk build; a false negative keeps the per-route inlining fallback.
|
|
318
|
+
const compatVendorSpecifierPattern =
|
|
319
|
+
/["'](?:react|react-dom|react\/jsx(?:-dev)?-runtime|react-dom\/(?:client|server)|@reckona\/mreact-compat(?:\/[\w-]+)?)["']/u;
|
|
320
|
+
|
|
321
|
+
export function sourceReferencesCompatVendorSpecifier(source: string): boolean {
|
|
322
|
+
return compatVendorSpecifierPattern.test(source);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
export function resolveCompatVendorEntryFiles(resolveDir?: string): Map<string, string> {
|
|
326
|
+
const files = new Map<string, string>();
|
|
327
|
+
|
|
328
|
+
for (const [entry, specifier] of compatVendorEntrySpecifiers) {
|
|
329
|
+
files.set(
|
|
330
|
+
entry,
|
|
331
|
+
resolveWorkspacePackageFile({
|
|
332
|
+
currentFileUrl: import.meta.url,
|
|
333
|
+
entry,
|
|
334
|
+
monorepoDir: "react-compat",
|
|
335
|
+
packageName: "@reckona/mreact-compat",
|
|
336
|
+
resolveDir,
|
|
337
|
+
specifier,
|
|
338
|
+
}),
|
|
339
|
+
);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return files;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Marks every compat-family import as external with a deterministic
|
|
346
|
+
// placeholder specifier; writeServerModuleArtifactFiles later rewrites the
|
|
347
|
+
// placeholders to relative paths into the emitted shared vendor chunks.
|
|
348
|
+
export function compatVendorExternalizePlugin(): RouterCompatPlugin {
|
|
349
|
+
return {
|
|
350
|
+
name: "mreact-compat-vendor-externalize",
|
|
351
|
+
setup(buildApi) {
|
|
352
|
+
buildApi.onResolve(
|
|
353
|
+
{ filter: /^(?:react|react-dom|react\/.+|react-dom\/.+|@reckona\/mreact-compat(?:\/.+)?)$/u },
|
|
354
|
+
(args) => {
|
|
355
|
+
const entry = compatVendorSpecifierEntries.get(args.path);
|
|
356
|
+
|
|
357
|
+
return entry === undefined
|
|
358
|
+
? undefined
|
|
359
|
+
: { external: true, path: `${COMPAT_VENDOR_PLACEHOLDER_PREFIX}${entry}` };
|
|
360
|
+
},
|
|
361
|
+
);
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
|
|
277
366
|
export async function bundleAppRouterSourceModule(options: {
|
|
278
367
|
code: string;
|
|
279
368
|
define?: UserConfig["define"] | undefined;
|
|
280
369
|
externalizeAppSourceModuleDirs?: readonly string[] | undefined;
|
|
370
|
+
externalizeCompatVendor?: boolean | undefined;
|
|
281
371
|
label: string;
|
|
282
372
|
plugins?: readonly RouterCompatPlugin[] | undefined;
|
|
283
373
|
resolveDir?: string | undefined;
|
|
@@ -295,6 +385,7 @@ export async function bundleAppRouterSourceModule(options: {
|
|
|
295
385
|
root: options.root,
|
|
296
386
|
vitePlugins: options.vitePlugins,
|
|
297
387
|
plugins: [
|
|
388
|
+
...(options.externalizeCompatVendor === true ? [compatVendorExternalizePlugin()] : []),
|
|
298
389
|
workspacePackageResolutionPlugin(),
|
|
299
390
|
...(options.serverSourceTransform === undefined
|
|
300
391
|
? []
|
package/src/navigation.ts
CHANGED
|
@@ -61,6 +61,11 @@ function throwUnsafeRewrite(location: string): never {
|
|
|
61
61
|
throw new TypeError(`unsafe rewrite target: ${JSON.stringify(location)}`);
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
/**
|
|
65
|
+
* Throws an internal redirect for loaders, middleware, route handlers, or server actions.
|
|
66
|
+
*
|
|
67
|
+
* The target must be same-origin or relative; use `redirectExternal()` for trusted `http` or `https` destinations. The default status is `303`.
|
|
68
|
+
*/
|
|
64
69
|
export function redirect(location: string, options: RedirectOptions = {}): never {
|
|
65
70
|
if (!isSafeInternalRedirect(location)) {
|
|
66
71
|
throwUnsafeRedirect(location);
|
|
@@ -72,6 +77,11 @@ export function redirect(location: string, options: RedirectOptions = {}): never
|
|
|
72
77
|
});
|
|
73
78
|
}
|
|
74
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Throws an external redirect for trusted `http` or `https` destinations.
|
|
82
|
+
*
|
|
83
|
+
* This helper is intentionally separate from `redirect()` so off-site navigation is explicit. The default status is `307`.
|
|
84
|
+
*/
|
|
75
85
|
export function redirectExternal(
|
|
76
86
|
location: string,
|
|
77
87
|
options: RedirectOptions = {},
|
|
@@ -86,6 +96,11 @@ export function redirectExternal(
|
|
|
86
96
|
});
|
|
87
97
|
}
|
|
88
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Creates a same-origin `303 See Other` redirect response.
|
|
101
|
+
*
|
|
102
|
+
* Use this from route handlers when returning a `Response` is more convenient than throwing through the app-router control flow.
|
|
103
|
+
*/
|
|
89
104
|
export function redirect303(location: string, init: ResponseInit = {}): Response {
|
|
90
105
|
if (!isSafeInternalRedirect(location)) {
|
|
91
106
|
throwUnsafeRedirect(location);
|
|
@@ -101,6 +116,9 @@ export function redirect303(location: string, init: ResponseInit = {}): Response
|
|
|
101
116
|
});
|
|
102
117
|
}
|
|
103
118
|
|
|
119
|
+
/**
|
|
120
|
+
* Creates a plain-text error response with a default `text/plain` content type.
|
|
121
|
+
*/
|
|
104
122
|
export function textError(message: string, status = 400, init: ResponseInit = {}): Response {
|
|
105
123
|
const headers = new Headers(init.headers);
|
|
106
124
|
|
|
@@ -115,6 +133,11 @@ export function textError(message: string, status = 400, init: ResponseInit = {}
|
|
|
115
133
|
});
|
|
116
134
|
}
|
|
117
135
|
|
|
136
|
+
/**
|
|
137
|
+
* Reads `request.formData()` and optionally validates it with a schema-like parser.
|
|
138
|
+
*
|
|
139
|
+
* Pass a schema object with `parse(FormData)` when a route handler or server action should receive typed form data.
|
|
140
|
+
*/
|
|
118
141
|
export async function parseForm(request: Request): Promise<FormData>;
|
|
119
142
|
export async function parseForm<T>(request: Request, schema: ParseSchema<T>): Promise<T>;
|
|
120
143
|
export async function parseForm<T>(
|
|
@@ -126,6 +149,11 @@ export async function parseForm<T>(
|
|
|
126
149
|
return schema === undefined ? form : schema.parse(form);
|
|
127
150
|
}
|
|
128
151
|
|
|
152
|
+
/**
|
|
153
|
+
* Throws a route-level 404 control error.
|
|
154
|
+
*
|
|
155
|
+
* Use this from loaders, metadata, middleware, or route handlers when the matched route is valid but the requested resource is absent.
|
|
156
|
+
*/
|
|
129
157
|
export function notFound(): never {
|
|
130
158
|
throw Object.assign(new Error("Not Found"), {
|
|
131
159
|
name: notFoundErrorName,
|
|
@@ -133,14 +161,25 @@ export function notFound(): never {
|
|
|
133
161
|
});
|
|
134
162
|
}
|
|
135
163
|
|
|
164
|
+
/**
|
|
165
|
+
* Alias for `notFound()` for codebases that prefer an explicit throwing helper name.
|
|
166
|
+
*/
|
|
136
167
|
export function throwNotFound(): never {
|
|
137
168
|
return notFound();
|
|
138
169
|
}
|
|
139
170
|
|
|
171
|
+
/**
|
|
172
|
+
* Continues middleware processing without changing the request.
|
|
173
|
+
*/
|
|
140
174
|
export function next(): MiddlewareNext {
|
|
141
175
|
return undefined;
|
|
142
176
|
}
|
|
143
177
|
|
|
178
|
+
/**
|
|
179
|
+
* Creates an internal rewrite response consumed by app-router middleware.
|
|
180
|
+
*
|
|
181
|
+
* The target must be a same-origin route path, query, hash, or relative URL; protocol-relative, external, and control-character targets are rejected.
|
|
182
|
+
*/
|
|
144
183
|
export function rewrite(location: string, init: ResponseInit = {}): Response {
|
|
145
184
|
if (!isSafeInternalRedirect(location)) {
|
|
146
185
|
throwUnsafeRewrite(location);
|
|
@@ -160,10 +199,16 @@ export function rewrite(location: string, init: ResponseInit = {}): Response {
|
|
|
160
199
|
return response;
|
|
161
200
|
}
|
|
162
201
|
|
|
202
|
+
/**
|
|
203
|
+
* Creates a JSON response using the platform `Response.json()` implementation.
|
|
204
|
+
*/
|
|
163
205
|
export function json(value: unknown, init?: ResponseInit): Response {
|
|
164
206
|
return Response.json(value, init);
|
|
165
207
|
}
|
|
166
208
|
|
|
209
|
+
/**
|
|
210
|
+
* Creates an HTML response and defaults the content type to `text/html; charset=utf-8`.
|
|
211
|
+
*/
|
|
167
212
|
export function html(value: string, init: ResponseInit = {}): Response {
|
|
168
213
|
if (init.headers === undefined) {
|
|
169
214
|
return new Response(value, {
|
|
@@ -184,6 +229,9 @@ export function html(value: string, init: ResponseInit = {}): Response {
|
|
|
184
229
|
});
|
|
185
230
|
}
|
|
186
231
|
|
|
232
|
+
/**
|
|
233
|
+
* Returns the request headers object for route code that follows app-router helper naming.
|
|
234
|
+
*/
|
|
187
235
|
export function headers(request: Request): Headers {
|
|
188
236
|
return request.headers;
|
|
189
237
|
}
|
|
@@ -194,6 +242,11 @@ export interface RequestCookies {
|
|
|
194
242
|
has(name: string): boolean;
|
|
195
243
|
}
|
|
196
244
|
|
|
245
|
+
/**
|
|
246
|
+
* Parses request cookies into a small read-only helper.
|
|
247
|
+
*
|
|
248
|
+
* Values are decoded with `decodeURIComponent`; malformed encoded values are ignored rather than throwing during request handling.
|
|
249
|
+
*/
|
|
197
250
|
export function cookies(request: Request): RequestCookies {
|
|
198
251
|
let values: ReadonlyMap<string, string> | undefined;
|
|
199
252
|
const cookieValues = () => {
|