@reckona/mreact-router 0.0.181 → 0.0.183

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 (47) hide show
  1. package/dist/actions.d.ts.map +1 -1
  2. package/dist/actions.js +20 -8
  3. package/dist/actions.js.map +1 -1
  4. package/dist/build.d.ts +3 -0
  5. package/dist/build.d.ts.map +1 -1
  6. package/dist/build.js +263 -5
  7. package/dist/build.js.map +1 -1
  8. package/dist/built-assets.d.ts.map +1 -1
  9. package/dist/built-assets.js +2 -2
  10. package/dist/built-assets.js.map +1 -1
  11. package/dist/cache-config.d.ts +1 -1
  12. package/dist/cache-config.d.ts.map +1 -1
  13. package/dist/cache-config.js.map +1 -1
  14. package/dist/cache.d.ts.map +1 -1
  15. package/dist/cache.js +18 -1
  16. package/dist/cache.js.map +1 -1
  17. package/dist/client.d.ts.map +1 -1
  18. package/dist/client.js +16 -6
  19. package/dist/client.js.map +1 -1
  20. package/dist/config.d.ts +2 -0
  21. package/dist/config.d.ts.map +1 -1
  22. package/dist/config.js +6 -0
  23. package/dist/config.js.map +1 -1
  24. package/dist/csrf.js +10 -6
  25. package/dist/csrf.js.map +1 -1
  26. package/dist/navigation.d.ts.map +1 -1
  27. package/dist/navigation.js +14 -1
  28. package/dist/navigation.js.map +1 -1
  29. package/dist/render.d.ts +4 -0
  30. package/dist/render.d.ts.map +1 -1
  31. package/dist/render.js +96 -46
  32. package/dist/render.js.map +1 -1
  33. package/dist/vite.d.ts.map +1 -1
  34. package/dist/vite.js +35 -4
  35. package/dist/vite.js.map +1 -1
  36. package/package.json +11 -11
  37. package/src/actions.ts +26 -9
  38. package/src/build.ts +398 -5
  39. package/src/built-assets.ts +2 -3
  40. package/src/cache-config.ts +1 -0
  41. package/src/cache.ts +20 -1
  42. package/src/client.ts +16 -6
  43. package/src/config.ts +8 -0
  44. package/src/csrf.ts +13 -6
  45. package/src/navigation.ts +16 -1
  46. package/src/render.ts +194 -78
  47. package/src/vite.ts +41 -4
package/src/build.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { AsyncLocalStorage } from "node:async_hooks";
2
3
  import type { Dirent } from "node:fs";
3
4
  import {
4
5
  copyFile,
@@ -108,7 +109,8 @@ const nativeEscapeTransform = {
108
109
  batchImportSource: "@reckona/mreact-router/native-escape",
109
110
  } as const;
110
111
 
111
- const defaultBuildConcurrency = Math.max(1, Math.min(2, availableParallelism()));
112
+ const maxDefaultBuildConcurrency = 8;
113
+ const buildConcurrencyStorage = new AsyncLocalStorage<number>();
112
114
  const serverArtifactFilesystemConcurrency = 2;
113
115
 
114
116
  type ServerTransformOutput = ReturnType<typeof transform>;
@@ -219,6 +221,29 @@ export interface BuildAppResult {
219
221
  routes: AppRoute[];
220
222
  }
221
223
 
224
+ interface IncrementalBuildCacheManifest {
225
+ fingerprint: string;
226
+ version: 1;
227
+ }
228
+
229
+ interface IncrementalBuildServerManifestOutputs {
230
+ serverModuleFiles?: Record<string, string>;
231
+ serverModuleRenderFiles?: Record<string, string>;
232
+ serverModuleRequestFiles?: Record<string, string>;
233
+ }
234
+
235
+ interface IncrementalBuildClientManifestOutputs {
236
+ assets?: readonly string[];
237
+ publicAssets?: readonly string[];
238
+ routes: readonly {
239
+ css?: readonly string[];
240
+ imports?: readonly string[];
241
+ navigationScript?: string | undefined;
242
+ script?: string | undefined;
243
+ sourceMap?: string | undefined;
244
+ }[];
245
+ }
246
+
222
247
  /**
223
248
  * Describes the generated import policy artifact consumed by built request handlers.
224
249
  */
@@ -377,10 +402,24 @@ type StaticParams = Record<string, string | number | boolean | readonly string[]
377
402
  * 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.
378
403
  */
379
404
  export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult> {
405
+ const project = resolveAppRouterProjectOptions(options);
406
+ const buildConcurrency = resolveBuildConcurrency(
407
+ options.buildConcurrency ?? project.buildConcurrency,
408
+ );
409
+
410
+ return await buildConcurrencyStorage.run(
411
+ buildConcurrency,
412
+ async () => await buildAppWithResolvedProject(options, project),
413
+ );
414
+ }
415
+
416
+ async function buildAppWithResolvedProject(
417
+ options: BuildAppOptions,
418
+ project: ResolvedAppRouterProject,
419
+ ): Promise<BuildAppResult> {
380
420
  const timingSink = options.onBuildPhaseTiming;
381
421
  const progressSink = options.onBuildProgress;
382
422
  const shouldTrackBuildPhases = timingSink !== undefined || progressSink !== undefined;
383
- const project = resolveAppRouterProjectOptions(options);
384
423
  const buildTargets = resolveBuildTargets(options.targets ?? project.buildTargets);
385
424
  const shouldBuildCloudflare = buildTargets.includes("cloudflare");
386
425
  const shouldBuildAwsLambda = buildTargets.includes("aws-lambda");
@@ -405,6 +444,14 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
405
444
  const serverDir = join(options.outDir, "server");
406
445
  const clientDir = join(options.outDir, "client");
407
446
  const cloudflareDir = join(options.outDir, "cloudflare");
447
+ const incrementalBuildFingerprint = await createIncrementalBuildCacheFingerprint({
448
+ buildTargets,
449
+ files,
450
+ project,
451
+ routes,
452
+ viteDefine,
453
+ vitePlugins,
454
+ });
408
455
  const sourceAnalysis =
409
456
  shouldTrackBuildPhases === false
410
457
  ? await analyzeBuildRouteSources({
@@ -452,6 +499,17 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
452
499
  );
453
500
  }
454
501
 
502
+ if (
503
+ incrementalBuildFingerprint !== undefined &&
504
+ (await isIncrementalBuildCacheHit({
505
+ buildTargets,
506
+ fingerprint: incrementalBuildFingerprint,
507
+ outDir: options.outDir,
508
+ }))
509
+ ) {
510
+ return { routes };
511
+ }
512
+
455
513
  if (shouldTrackBuildPhases === false) {
456
514
  await rm(options.outDir, { force: true, recursive: true });
457
515
  await Promise.all([
@@ -783,9 +841,284 @@ export async function buildApp(options: BuildAppOptions): Promise<BuildAppResult
783
841
  });
784
842
  }
785
843
 
844
+ if (incrementalBuildFingerprint !== undefined) {
845
+ await writeIncrementalBuildCacheManifest(options.outDir, incrementalBuildFingerprint);
846
+ }
847
+
786
848
  return { routes };
787
849
  }
788
850
 
851
+ const incrementalBuildCacheFilename = "build-cache.json";
852
+ const incrementalBuildCacheVersion = 1;
853
+
854
+ async function createIncrementalBuildCacheFingerprint(options: {
855
+ buildTargets: readonly AppRouterBuildTarget[];
856
+ files: Record<string, string>;
857
+ project: ResolvedAppRouterProject;
858
+ routes: readonly AppRoute[];
859
+ viteDefine: UserConfig["define"] | undefined;
860
+ vitePlugins: readonly PluginOption[] | undefined;
861
+ }): Promise<string | undefined> {
862
+ if (options.vitePlugins !== undefined && options.vitePlugins.length > 0) {
863
+ return undefined;
864
+ }
865
+
866
+ let define: string;
867
+ try {
868
+ const serializedDefine = JSON.stringify(options.viteDefine ?? null);
869
+ if (serializedDefine === undefined) {
870
+ return undefined;
871
+ }
872
+ define = serializedDefine;
873
+ } catch {
874
+ return undefined;
875
+ }
876
+
877
+ const [publicAssets, appConventionAssets] = await Promise.all([
878
+ collectBuildInputFileHashes(options.project.publicDir, options.project.projectRoot),
879
+ collectAppConventionAssetInputHashes(options.project.routesDir, options.project.projectRoot),
880
+ ]);
881
+ const sourceFiles = Object.keys(options.files)
882
+ .sort()
883
+ .map((file) => [normalizeBuildInputPath(file), hashText(options.files[file] ?? "")] as const);
884
+ const routes = options.routes
885
+ .map((route) => ({
886
+ file: normalizeBuildInputPath(relative(options.project.projectRoot, route.file)),
887
+ kind: route.kind,
888
+ path: route.path,
889
+ }))
890
+ .sort((left, right) => left.file.localeCompare(right.file));
891
+ const payload = {
892
+ appDir: normalizeBuildInputPath(relative(options.project.projectRoot, options.project.routesDir)),
893
+ assetBaseUrl: options.project.assetBaseUrl ?? null,
894
+ clientConsolePureFunctions: options.project.clientConsolePureFunctions ?? [],
895
+ clientSourceMaps: options.project.clientSourceMaps,
896
+ define,
897
+ nodeEnv: process.env.NODE_ENV ?? null,
898
+ publicAssetBaseUrl: options.project.publicAssetBaseUrl ?? null,
899
+ publicDir: normalizeBuildInputPath(relative(options.project.projectRoot, options.project.publicDir)),
900
+ routes,
901
+ sourceDirs: options.project.allowedSourceDirs
902
+ .map((directory) => normalizeBuildInputPath(relative(options.project.projectRoot, directory)))
903
+ .sort(),
904
+ sourceFiles,
905
+ targets: [...options.buildTargets].sort(),
906
+ version: incrementalBuildCacheVersion,
907
+ publicAssets,
908
+ appConventionAssets,
909
+ };
910
+
911
+ return createHash("sha256").update(JSON.stringify(payload)).digest("hex");
912
+ }
913
+
914
+ async function isIncrementalBuildCacheHit(options: {
915
+ buildTargets: readonly AppRouterBuildTarget[];
916
+ fingerprint: string;
917
+ outDir: string;
918
+ }): Promise<boolean> {
919
+ const cache = await readJsonBuildOutput<IncrementalBuildCacheManifest>(
920
+ join(options.outDir, incrementalBuildCacheFilename),
921
+ );
922
+ if (cache === undefined) {
923
+ return false;
924
+ }
925
+
926
+ return (
927
+ cache.version === incrementalBuildCacheVersion &&
928
+ cache.fingerprint === options.fingerprint &&
929
+ (await hasRequiredIncrementalBuildOutputs(options.outDir, options.buildTargets))
930
+ );
931
+ }
932
+
933
+ async function hasRequiredIncrementalBuildOutputs(
934
+ outDir: string,
935
+ buildTargets: readonly AppRouterBuildTarget[],
936
+ ): Promise<boolean> {
937
+ const serverManifestFile = join(outDir, "server", "manifest.json");
938
+ const clientManifestFile = join(outDir, "client", "manifest.json");
939
+ const requiredFiles = [
940
+ serverManifestFile,
941
+ join(outDir, "server", "import-policy.json"),
942
+ clientManifestFile,
943
+ join(outDir, "client", ".vite", "manifest.json"),
944
+ join(outDir, "routes.d.ts"),
945
+ join(outDir, "public-assets.d.ts"),
946
+ ...(buildTargets.includes("aws-lambda")
947
+ ? [join(outDir, "aws-lambda", "mreact-handler.mjs")]
948
+ : []),
949
+ ...(buildTargets.includes("cloudflare") ? [join(outDir, "cloudflare", "worker.mjs")] : []),
950
+ ];
951
+ const [serverManifest, clientManifest] = await Promise.all([
952
+ readJsonBuildOutput<IncrementalBuildServerManifestOutputs>(serverManifestFile),
953
+ readJsonBuildOutput<IncrementalBuildClientManifestOutputs>(clientManifestFile),
954
+ ]);
955
+ if (serverManifest === undefined || clientManifest === undefined) {
956
+ return false;
957
+ }
958
+
959
+ for (const file of [
960
+ ...Object.values(serverManifest.serverModuleFiles ?? {}),
961
+ ...Object.values(serverManifest.serverModuleRenderFiles ?? {}),
962
+ ...Object.values(serverManifest.serverModuleRequestFiles ?? {}),
963
+ ]) {
964
+ requiredFiles.push(join(outDir, "server", file));
965
+ }
966
+
967
+ for (const file of collectClientManifestOutputFiles(clientManifest)) {
968
+ requiredFiles.push(join(outDir, "client", file));
969
+ }
970
+
971
+ for (const file of requiredFiles) {
972
+ if (!(await isExistingFile(file))) {
973
+ return false;
974
+ }
975
+ }
976
+
977
+ return true;
978
+ }
979
+
980
+ function collectClientManifestOutputFiles(
981
+ manifest: IncrementalBuildClientManifestOutputs,
982
+ ): string[] {
983
+ const files = new Set<string>();
984
+
985
+ for (const asset of manifest.assets ?? []) {
986
+ files.add(asset);
987
+ }
988
+ for (const asset of manifest.publicAssets ?? []) {
989
+ files.add(asset);
990
+ files.add(`public/${asset}`);
991
+ }
992
+ for (const route of manifest.routes) {
993
+ for (const file of [
994
+ route.script,
995
+ route.sourceMap,
996
+ route.navigationScript,
997
+ ...(route.css ?? []),
998
+ ...(route.imports ?? []),
999
+ ]) {
1000
+ if (file !== undefined) {
1001
+ files.add(file);
1002
+ }
1003
+ }
1004
+ }
1005
+
1006
+ return [...files];
1007
+ }
1008
+
1009
+ async function readJsonBuildOutput<T>(file: string): Promise<T | undefined> {
1010
+ try {
1011
+ return JSON.parse(await readFile(file, "utf8")) as T;
1012
+ } catch (error) {
1013
+ if (
1014
+ (isNodeError(error) && error.code === "ENOENT") ||
1015
+ error instanceof SyntaxError
1016
+ ) {
1017
+ return undefined;
1018
+ }
1019
+
1020
+ throw error;
1021
+ }
1022
+ }
1023
+
1024
+ async function writeIncrementalBuildCacheManifest(
1025
+ outDir: string,
1026
+ fingerprint: string,
1027
+ ): Promise<void> {
1028
+ const cache = {
1029
+ fingerprint,
1030
+ version: incrementalBuildCacheVersion,
1031
+ } satisfies IncrementalBuildCacheManifest;
1032
+
1033
+ await writeFile(
1034
+ join(outDir, incrementalBuildCacheFilename),
1035
+ `${JSON.stringify(cache, null, 2)}\n`,
1036
+ );
1037
+ }
1038
+
1039
+ async function collectBuildInputFileHashes(
1040
+ directory: string,
1041
+ projectRoot: string,
1042
+ ): Promise<readonly (readonly [string, string])[]> {
1043
+ if (!(await isPublicAssetDirectory(directory))) {
1044
+ return [];
1045
+ }
1046
+
1047
+ const files = await collectFiles(directory);
1048
+ const hashes = await mapWithBuildConcurrency(
1049
+ files,
1050
+ async (file) =>
1051
+ [
1052
+ normalizeBuildInputPath(relative(projectRoot, file)),
1053
+ hashBuffer(await readFile(file)),
1054
+ ] as const,
1055
+ );
1056
+
1057
+ return hashes.sort(([left], [right]) => left.localeCompare(right));
1058
+ }
1059
+
1060
+ async function collectAppConventionAssetInputHashes(
1061
+ appDir: string,
1062
+ projectRoot: string,
1063
+ ): Promise<readonly (readonly [string, string])[]> {
1064
+ const entries = await readdir(appDir, { withFileTypes: true });
1065
+ const assetFiles = entries
1066
+ .filter((entry) => entry.isFile())
1067
+ .map((entry) => join(appDir, entry.name))
1068
+ .filter((file) => isAppFileConventionAsset(file, appDir));
1069
+ const hashes = await mapWithBuildConcurrency(
1070
+ assetFiles,
1071
+ async (file) =>
1072
+ [
1073
+ normalizeBuildInputPath(relative(projectRoot, file)),
1074
+ hashBuffer(await readFile(file)),
1075
+ ] as const,
1076
+ );
1077
+
1078
+ return hashes.sort(([left], [right]) => left.localeCompare(right));
1079
+ }
1080
+
1081
+ async function isExistingFile(file: string): Promise<boolean> {
1082
+ try {
1083
+ return (await stat(file)).isFile();
1084
+ } catch (error) {
1085
+ if (isNodeError(error) && error.code === "ENOENT") {
1086
+ return false;
1087
+ }
1088
+
1089
+ throw error;
1090
+ }
1091
+ }
1092
+
1093
+ function normalizeBuildInputPath(path: string): string {
1094
+ return path.split(sep).join("/");
1095
+ }
1096
+
1097
+ function hashBuffer(buffer: Buffer): string {
1098
+ return createHash("sha256").update(buffer).digest("hex").slice(0, 16);
1099
+ }
1100
+
1101
+ function defaultBuildConcurrency(): number {
1102
+ return Math.max(1, Math.min(maxDefaultBuildConcurrency, availableParallelism()));
1103
+ }
1104
+
1105
+ function currentBuildConcurrency(): number {
1106
+ return buildConcurrencyStorage.getStore() ?? defaultBuildConcurrency();
1107
+ }
1108
+
1109
+ function resolveBuildConcurrency(
1110
+ value: number | undefined,
1111
+ cores = availableParallelism(),
1112
+ ): number {
1113
+ const resolved = value ?? Math.max(1, Math.min(maxDefaultBuildConcurrency, cores));
1114
+
1115
+ if (!Number.isSafeInteger(resolved) || resolved < 1) {
1116
+ throw new Error("mreactRouter buildConcurrency must be a positive integer.");
1117
+ }
1118
+
1119
+ return resolved;
1120
+ }
1121
+
789
1122
  function typedRoutesDeclaration(routes: readonly AppRoute[]): string {
790
1123
  const routePaths = Array.from(
791
1124
  new Set(
@@ -803,21 +1136,60 @@ function typedRoutesDeclaration(routes: readonly AppRoute[]): string {
803
1136
  return left.localeCompare(right);
804
1137
  });
805
1138
  const routeUnion = routePaths.map((routePath) => JSON.stringify(routePath)).join(" | ");
1139
+ const routeParamEntries = routePaths.map(
1140
+ (routePath) => ` readonly ${JSON.stringify(routePath)}: ${routeParamsType(routePath)};`,
1141
+ );
806
1142
  return [
807
- `import type { AppRouteLinkHref as MreactAppRouteLinkHref } from "@reckona/mreact-router";`,
1143
+ `import type { AppRouteLinkHref as MreactAppRouteLinkHref, RouteParamsFor as MreactRouteParamsFor } from "@reckona/mreact-router";`,
808
1144
  ``,
809
1145
  `export type AppRoutePath = ${routeUnion === "" ? "never" : routeUnion};`,
810
1146
  `export type AppRouteHref = MreactAppRouteLinkHref<AppRoutePath>;`,
1147
+ `export type AppRouteParams<Path extends AppRoutePath> = MreactRouteParamsFor<Path>;`,
1148
+ `export interface AppRouteParamMap {`,
1149
+ ...(routeParamEntries.length === 0 ? [` readonly [path: string]: never;`] : routeParamEntries),
1150
+ `}`,
811
1151
  ``,
812
1152
  `declare module "@reckona/mreact-router/link" {`,
813
1153
  ` interface AppRouteDeclarations {`,
814
1154
  ` readonly path: AppRoutePath;`,
1155
+ ` readonly params: AppRouteParamMap;`,
815
1156
  ` }`,
816
1157
  `}`,
817
1158
  ``,
818
1159
  ].join("\n");
819
1160
  }
820
1161
 
1162
+ function routeParamsType(routePath: string): string {
1163
+ const params = routePath
1164
+ .split("/")
1165
+ .flatMap((segment): Array<{ catchAll: boolean; name: string }> => {
1166
+ if (segment.startsWith(":...")) {
1167
+ return [{ catchAll: true, name: segment.slice(4) }];
1168
+ }
1169
+
1170
+ if (segment.startsWith(":")) {
1171
+ return [{ catchAll: false, name: segment.slice(1) }];
1172
+ }
1173
+
1174
+ return [];
1175
+ });
1176
+
1177
+ if (params.length === 0) {
1178
+ return "Record<never, never>";
1179
+ }
1180
+
1181
+ return `{ ${params
1182
+ .map(
1183
+ (param) =>
1184
+ `readonly ${propertyKeyForType(param.name)}: ${param.catchAll ? "readonly string[]" : "string"}`,
1185
+ )
1186
+ .join("; ")} }`;
1187
+ }
1188
+
1189
+ function propertyKeyForType(value: string): string {
1190
+ return /^[A-Za-z_$][\w$]*$/.test(value) ? value : JSON.stringify(value);
1191
+ }
1192
+
821
1193
  function typedPublicAssetsDeclaration(publicAssets: readonly string[]): string {
822
1194
  const assetUnion = publicAssets.map((assetPath) => JSON.stringify(assetPath)).join(" | ");
823
1195
 
@@ -861,7 +1233,7 @@ function roundBuildPhaseMs(value: number): number {
861
1233
  async function mapWithBuildConcurrency<T, R>(
862
1234
  items: readonly T[],
863
1235
  map: (item: T, index: number) => Promise<R>,
864
- concurrency = defaultBuildConcurrency,
1236
+ concurrency = currentBuildConcurrency(),
865
1237
  ): Promise<R[]> {
866
1238
  if (items.length === 0) {
867
1239
  return [];
@@ -897,6 +1269,27 @@ export async function __mapWithBuildConcurrencyForTests<T, R>(
897
1269
  return await mapWithBuildConcurrency(items, map, concurrency);
898
1270
  }
899
1271
 
1272
+ async function mapServerOutputsWithBuildConcurrency<R>(
1273
+ serverOutputs: readonly ServerOutputMode[],
1274
+ map: (serverOutput: ServerOutputMode, index: number) => Promise<R>,
1275
+ ): Promise<R[]> {
1276
+ return await mapWithBuildConcurrency(serverOutputs, map, 1);
1277
+ }
1278
+
1279
+ export async function __mapServerOutputsWithBuildConcurrencyForTests<R>(
1280
+ serverOutputs: readonly ServerOutputMode[],
1281
+ map: (serverOutput: ServerOutputMode, index: number) => Promise<R>,
1282
+ ): Promise<R[]> {
1283
+ return await mapServerOutputsWithBuildConcurrency(serverOutputs, map);
1284
+ }
1285
+
1286
+ export function __resolveBuildConcurrencyForTests(
1287
+ value: number | undefined,
1288
+ cores: number,
1289
+ ): number {
1290
+ return resolveBuildConcurrency(value, cores);
1291
+ }
1292
+
900
1293
  async function analyzeBuildRouteSources(options: {
901
1294
  clientRouteInferenceCache: ClientRouteInferenceCache;
902
1295
  files: Record<string, string>;
@@ -2479,7 +2872,7 @@ async function buildServerModuleArtifacts(options: {
2479
2872
  });
2480
2873
  }
2481
2874
 
2482
- const serverOutputArtifacts = await mapWithBuildConcurrency(
2875
+ const serverOutputArtifacts = await mapServerOutputsWithBuildConcurrency(
2483
2876
  serverOutputs,
2484
2877
  async (serverOutput) => {
2485
2878
  const output = await transformServerRouteSource({
@@ -87,9 +87,8 @@ export async function readBuiltPublicAsset(
87
87
  return undefined;
88
88
  }
89
89
 
90
- const normalized = normalize(relativePath);
91
-
92
- if (isAbsolute(normalized) || normalized === ".." || normalized.startsWith("../")) {
90
+ const normalized = safeBuiltClientAssetPath(relativePath);
91
+ if (normalized === undefined) {
93
92
  return undefined;
94
93
  }
95
94
 
@@ -2,6 +2,7 @@ export type RouterCacheLimitName =
2
2
  | "COMPOSED_ROUTE_METADATA"
3
3
  | "MIDDLEWARE_MODULE"
4
4
  | "ROUTE_LOADER_MODULE"
5
+ | "ROUTE_METADATA_MODULE"
5
6
  | "ROUTE_OUT_OF_ORDER_BOUNDARY_ANALYSIS"
6
7
  | "ROUTE_SOURCE_ANALYSIS"
7
8
  | "SERVER_ROUTE_MODULE"
package/src/cache.ts CHANGED
@@ -203,6 +203,11 @@ export function routeCachePolicyFromSource(code: string): RouteCachePolicy | und
203
203
  const seconds = match?.groups?.seconds === undefined ? undefined : Number(match.groups.seconds);
204
204
 
205
205
  if (seconds === undefined || !Number.isFinite(seconds)) {
206
+ if (/^\s*export\s+const\s+revalidate\s*=/m.test(code)) {
207
+ console.warn(
208
+ "export const revalidate must be a plain integer literal, e.g. export const revalidate = 3600",
209
+ );
210
+ }
206
211
  return undefined;
207
212
  }
208
213
 
@@ -362,8 +367,10 @@ function requestCarriesCredentials(request: Request | undefined): boolean {
362
367
  lower === "x-api-key" ||
363
368
  lower === "cf-access-jwt-assertion" ||
364
369
  lower === "proxy-authorization" ||
370
+ lower === "x-access-token" ||
365
371
  lower === "x-auth-token" ||
366
372
  lower === "x-authenticated-user" ||
373
+ lower === "x-id-token" ||
367
374
  lower === "x-forwarded-user" ||
368
375
  lower === "x-session-id" ||
369
376
  lower === "x-user-email" ||
@@ -380,7 +387,7 @@ function requestCarriesCredentials(request: Request | undefined): boolean {
380
387
  return true;
381
388
  }
382
389
 
383
- return true;
390
+ continue;
384
391
  }
385
392
 
386
393
  return false;
@@ -391,12 +398,18 @@ const PUBLIC_ROUTE_CACHE_REQUEST_HEADERS = new Set([
391
398
  "accept-encoding",
392
399
  "accept-language",
393
400
  "cache-control",
401
+ "cf-connecting-ip",
402
+ "cf-ipcountry",
403
+ "cf-ray",
394
404
  "connection",
395
405
  "dnt",
396
406
  "host",
407
+ "if-none-match",
397
408
  "pragma",
409
+ "priority",
398
410
  "purpose",
399
411
  "referer",
412
+ "save-data",
400
413
  "sec-ch-prefers-color-scheme",
401
414
  "sec-ch-prefers-reduced-motion",
402
415
  "sec-ch-ua",
@@ -408,8 +421,14 @@ const PUBLIC_ROUTE_CACHE_REQUEST_HEADERS = new Set([
408
421
  "sec-fetch-user",
409
422
  "upgrade-insecure-requests",
410
423
  "user-agent",
424
+ "via",
411
425
  "x-mreact-navigation",
412
426
  "x-mreact-navigation-cache",
427
+ "x-real-ip",
428
+ "x-request-id",
429
+ "x-forwarded-for",
430
+ "x-forwarded-host",
431
+ "x-forwarded-proto",
413
432
  ]);
414
433
 
415
434
  /**
package/src/client.ts CHANGED
@@ -3849,6 +3849,20 @@ function __mreactObserveViewportPrefetchAnchors(root) {
3849
3849
 
3850
3850
  function __mreactApplyOutOfOrderFragments(root) {
3851
3851
  const fragments = Array.from(root.querySelectorAll("template[data-mreact-oob-fragment]"));
3852
+ const completionMarkers = new Map();
3853
+ for (const marker of root.querySelectorAll("[data-mreact-oob-complete]")) {
3854
+ const id = marker.getAttribute("data-mreact-oob-complete");
3855
+ if (!completionMarkers.has(id)) {
3856
+ completionMarkers.set(id, marker);
3857
+ }
3858
+ }
3859
+ const placeholders = new Map();
3860
+ for (const placeholder of root.querySelectorAll("[data-mreact-oob-placeholder]")) {
3861
+ const id = placeholder.getAttribute("data-mreact-oob-placeholder");
3862
+ if (!placeholders.has(id)) {
3863
+ placeholders.set(id, placeholder);
3864
+ }
3865
+ }
3852
3866
 
3853
3867
  for (const fragment of fragments) {
3854
3868
  const id = fragment.getAttribute("data-mreact-oob-fragment");
@@ -3857,16 +3871,12 @@ function __mreactApplyOutOfOrderFragments(root) {
3857
3871
  continue;
3858
3872
  }
3859
3873
 
3860
- const completionMarker = Array.from(root.querySelectorAll("[data-mreact-oob-complete]"))
3861
- .find((candidate) => candidate.getAttribute("data-mreact-oob-complete") === id);
3862
-
3874
+ const completionMarker = completionMarkers.get(id);
3863
3875
  if (completionMarker === undefined) {
3864
3876
  continue;
3865
3877
  }
3866
3878
 
3867
- const placeholder = Array.from(root.querySelectorAll("[data-mreact-oob-placeholder]"))
3868
- .find((candidate) => candidate.getAttribute("data-mreact-oob-placeholder") === id);
3869
-
3879
+ const placeholder = placeholders.get(id);
3870
3880
  if (placeholder === undefined) {
3871
3881
  continue;
3872
3882
  }
package/src/config.ts CHANGED
@@ -26,6 +26,7 @@ export interface AppRouterProductionOptions {
26
26
 
27
27
  export interface AppRouterProjectOptions {
28
28
  assetBaseUrl?: string | undefined;
29
+ buildConcurrency?: number | undefined;
29
30
  buildTargets?: readonly AppRouterBuildTarget[] | undefined;
30
31
  clientSourceMaps?: AppRouterClientSourceMapOption | undefined;
31
32
  /**
@@ -48,6 +49,7 @@ export interface AppRouterProjectOptions {
48
49
  export interface ResolvedAppRouterProject {
49
50
  allowedSourceDirs: readonly string[];
50
51
  assetBaseUrl?: string | undefined;
52
+ buildConcurrency?: number | undefined;
51
53
  buildTargets: readonly AppRouterBuildTarget[];
52
54
  clientSourceMaps: AppRouterClientSourceMapMode;
53
55
  clientConsolePureFunctions?: readonly string[] | undefined;
@@ -76,6 +78,9 @@ export function resolveAppRouterProjectOptions(
76
78
  resolveProjectPath(appDir, directory, "allowedSourceDirs"),
77
79
  ),
78
80
  ...(options.assetBaseUrl === undefined ? {} : { assetBaseUrl: options.assetBaseUrl }),
81
+ ...(options.buildConcurrency === undefined
82
+ ? {}
83
+ : { buildConcurrency: options.buildConcurrency }),
79
84
  buildTargets: resolveBuildTargets(options.buildTargets),
80
85
  clientSourceMaps: resolveClientSourceMapMode(options.clientSourceMaps),
81
86
  ...(clientConsolePureFunctions === undefined ? {} : { clientConsolePureFunctions }),
@@ -98,6 +103,9 @@ export function resolveAppRouterProjectOptions(
98
103
  resolveProjectPath(projectRoot, directory, "allowedSourceDirs"),
99
104
  ),
100
105
  ...(options.assetBaseUrl === undefined ? {} : { assetBaseUrl: options.assetBaseUrl }),
106
+ ...(options.buildConcurrency === undefined
107
+ ? {}
108
+ : { buildConcurrency: options.buildConcurrency }),
101
109
  buildTargets: resolveBuildTargets(options.buildTargets),
102
110
  clientSourceMaps: resolveClientSourceMapMode(options.clientSourceMaps),
103
111
  ...(clientConsolePureFunctions === undefined ? {} : { clientConsolePureFunctions }),
package/src/csrf.ts CHANGED
@@ -8,7 +8,7 @@ const formFieldCsrf = "__mreact_csrf";
8
8
  export const formCsrfFieldName = formFieldCsrf;
9
9
 
10
10
  export function serverActionCookie(csrfToken: string): string {
11
- const production = isProductionEnvironment();
11
+ const secure = shouldUseSecureCsrfCookie();
12
12
  const parts = [
13
13
  `${currentCsrfCookieName()}=${encodeURIComponent(csrfToken)}`,
14
14
  "Path=/",
@@ -16,7 +16,7 @@ export function serverActionCookie(csrfToken: string): string {
16
16
  "HttpOnly",
17
17
  ];
18
18
 
19
- if (production) {
19
+ if (secure) {
20
20
  parts.push("Secure");
21
21
  }
22
22
 
@@ -97,16 +97,23 @@ function randomToken(): string {
97
97
  return `${hex.slice(0, 4).join("")}-${hex.slice(4, 6).join("")}-${hex.slice(6, 8).join("")}-${hex.slice(8, 10).join("")}-${hex.slice(10).join("")}`;
98
98
  }
99
99
 
100
- function isProductionEnvironment(): boolean {
101
- return typeof process !== "undefined" && process.env?.NODE_ENV === "production";
100
+ function isLocalCsrfEnvironment(): boolean {
101
+ return (
102
+ typeof process !== "undefined" &&
103
+ (process.env?.NODE_ENV === "development" || process.env?.NODE_ENV === "test")
104
+ );
105
+ }
106
+
107
+ function shouldUseSecureCsrfCookie(): boolean {
108
+ return !isLocalCsrfEnvironment();
102
109
  }
103
110
 
104
111
  function currentCsrfCookieName(): string {
105
- return isProductionEnvironment() ? csrfCookieNameProduction : csrfCookieNameDevelopment;
112
+ return shouldUseSecureCsrfCookie() ? csrfCookieNameProduction : csrfCookieNameDevelopment;
106
113
  }
107
114
 
108
115
  function csrfCookieNamesRead(): readonly string[] {
109
- return isProductionEnvironment()
116
+ return shouldUseSecureCsrfCookie()
110
117
  ? [csrfCookieNameProduction]
111
118
  : [csrfCookieNameProduction, csrfCookieNameDevelopment];
112
119
  }