@reckona/mreact-router 0.0.11 → 0.0.12

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/dist/render.js CHANGED
@@ -27,10 +27,18 @@ const authSessionScriptId = "__mreact_auth_session";
27
27
  const serverTransformCache = new Map();
28
28
  const serverSourceFileCache = new Map();
29
29
  const routeSourceAnalysisCache = new Map();
30
+ const routeOutOfOrderBoundaryAnalysisCache = new Map();
31
+ const routeLoaderModuleCache = new Map();
32
+ const middlewareModuleCache = new Map();
33
+ const serverRouteModuleCache = new Map();
30
34
  const composedRouteMetadataCache = new Map();
31
35
  const maxServerTransformCacheEntries = 512;
32
36
  const maxServerSourceFileCacheEntries = 512;
33
37
  const maxRouteSourceAnalysisCacheEntries = 512;
38
+ const maxRouteOutOfOrderBoundaryAnalysisCacheEntries = 512;
39
+ const maxRouteLoaderModuleCacheEntries = 512;
40
+ const maxMiddlewareModuleCacheEntries = 64;
41
+ const maxServerRouteModuleCacheEntries = 512;
34
42
  const maxComposedRouteMetadataCacheEntries = 512;
35
43
  // Issue 086: per-shell prefix/suffix cache. Pure layouts (whose
36
44
  // exported component takes zero arguments and therefore cannot
@@ -62,6 +70,8 @@ async function renderAppRequestInternal(options) {
62
70
  appDir: options.appDir,
63
71
  importPolicy: options.importPolicy,
64
72
  request: options.request,
73
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
74
+ serverSourceFiles: options.serverSourceFiles,
65
75
  });
66
76
  if (middlewareResponse !== undefined) {
67
77
  const location = rewriteLocation(middlewareResponse);
@@ -113,7 +123,13 @@ async function renderAppRequestInternal(options) {
113
123
  let routeCacheContext;
114
124
  try {
115
125
  if (matched.route.kind === "server") {
116
- return await dispatchServerRoute(matched.route.file, options.request, matched.params);
126
+ return await dispatchServerRoute({
127
+ file: matched.route.file,
128
+ params: matched.params,
129
+ request: options.request,
130
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
131
+ serverSourceFiles: options.serverSourceFiles,
132
+ });
117
133
  }
118
134
  // Issue 080: page routes render HTML for GET / HEAD only. Other
119
135
  // methods (PUT, PATCH, DELETE, PROPFIND, ...) get 405 with an
@@ -142,6 +158,7 @@ async function renderAppRequestInternal(options) {
142
158
  serverModuleCacheVersion: options.serverModuleCacheVersion,
143
159
  });
144
160
  const cachePolicy = originalAnalysis.cachePolicy;
161
+ const navigationScript = options.navigationScripts?.get(matched.route.path);
145
162
  const cacheKey = routeCacheKey(options.appDir, matched.route.path, url);
146
163
  const mayUseRouteCache = cachePolicy === undefined
147
164
  ? originalAnalysis.usesRuntimeCacheControl
@@ -185,6 +202,7 @@ async function renderAppRequestInternal(options) {
185
202
  appDir: options.appDir,
186
203
  filename: matched.route.file,
187
204
  importPolicy: options.importPolicy,
205
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
188
206
  })
189
207
  : undefined;
190
208
  recoveryRoute = {
@@ -206,7 +224,13 @@ async function renderAppRequestInternal(options) {
206
224
  "content-type": "text/html; charset=utf-8",
207
225
  "x-mreact-stream": "1",
208
226
  };
209
- if (loadingFile === undefined && !mayRenderOutOfOrderBoundary(routeCode)) {
227
+ const mayRenderOutOfOrder = await mayRenderOutOfOrderBoundaryDeep({
228
+ code: routeCode,
229
+ filename: matched.route.file,
230
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
231
+ serverSourceFiles: options.serverSourceFiles,
232
+ });
233
+ if (loadingFile === undefined && !mayRenderOutOfOrder) {
210
234
  const stringOutput = transformServerModule({
211
235
  code: routeCode,
212
236
  clientBoundaryImports: clientInference.clientBoundaryImports,
@@ -282,6 +306,7 @@ async function renderAppRequestInternal(options) {
282
306
  return withOptionalActionCookie(htmlResponse(`<!DOCTYPE html>${clientNavigationHeadTags({
283
307
  assetBaseUrl: options.assetBaseUrl,
284
308
  currentScript: clientRoute ? clientScript : undefined,
309
+ currentNavigationScript: clientRoute ? undefined : navigationScript,
285
310
  routeScripts: options.clientScripts,
286
311
  })}${html}`, { headers }), preparedActions.csrfToken, preparedActions.csrfTokenIsNew === true);
287
312
  }
@@ -429,6 +454,7 @@ async function renderAppRequestInternal(options) {
429
454
  const response = withOptionalActionCookie(htmlResponse(`<!DOCTYPE html>${clientNavigationHeadTags({
430
455
  assetBaseUrl: options.assetBaseUrl,
431
456
  currentScript: clientRoute ? clientScript : undefined,
457
+ currentNavigationScript: clientRoute ? undefined : navigationScript,
432
458
  routeScripts: options.clientScripts,
433
459
  })}${html}`, {
434
460
  headers: responseHeadersForMetadata(metadata),
@@ -519,9 +545,15 @@ function modulePreloadTags(script, assetBaseUrl) {
519
545
  function clientNavigationHeadTags(options) {
520
546
  return [
521
547
  modulePreloadTags(options.currentScript, options.assetBaseUrl),
548
+ navigationRuntimeScriptTag(options.currentNavigationScript, options.assetBaseUrl),
522
549
  routePrefetchManifestScript(options.routeScripts, options.assetBaseUrl),
523
550
  ].join("");
524
551
  }
552
+ function navigationRuntimeScriptTag(script, assetBaseUrl) {
553
+ return script === undefined
554
+ ? ""
555
+ : `<script type="module" src="${escapeHtmlAttribute(assetPath(script, assetBaseUrl ?? "/_mreact/client/"))}"></script>`;
556
+ }
525
557
  function routePrefetchManifestScript(routeScripts, assetBaseUrl) {
526
558
  if (routeScripts === undefined || routeScripts.size === 0) {
527
559
  return "";
@@ -665,15 +697,15 @@ function normalizeErrorForProps(error) {
665
697
  }
666
698
  return { message: String(error) };
667
699
  }
668
- async function dispatchServerRoute(file, request, params) {
669
- const module = await importAppRouterFileModule(file);
670
- const handler = module[request.method] ?? module.ALL ?? module.default;
700
+ async function dispatchServerRoute(options) {
701
+ const module = await loadServerRouteModule(options);
702
+ const handler = module[options.request.method] ?? module.ALL ?? module.default;
671
703
  if (typeof handler !== "function") {
672
704
  return new Response("Method Not Allowed", { status: 405 });
673
705
  }
674
706
  let response;
675
707
  try {
676
- response = await handler(request, { params });
708
+ response = await handler(options.request, { params: options.params });
677
709
  }
678
710
  catch (error) {
679
711
  if (error instanceof Response) {
@@ -685,6 +717,29 @@ async function dispatchServerRoute(file, request, params) {
685
717
  ? response
686
718
  : new Response("Invalid route response", { status: 500 });
687
719
  }
720
+ async function loadServerRouteModule(options) {
721
+ if (options.serverModuleCacheVersion === undefined) {
722
+ return await importAppRouterFileModule(options.file);
723
+ }
724
+ const code = await readServerSourceFile(options.file, options.serverModuleCacheVersion, options.serverSourceFiles);
725
+ const cacheKey = `server-route\0${options.file}\0${options.serverModuleCacheVersion}\0${memoizedHashText(code)}`;
726
+ const cached = serverRouteModuleCache.get(cacheKey);
727
+ if (cached !== undefined) {
728
+ return cached;
729
+ }
730
+ const loaded = importAppRouterSourceModule({
731
+ cacheKey,
732
+ code,
733
+ label: `server-route:${options.file}`,
734
+ resolveDir: dirname(options.file),
735
+ sourcefile: options.file,
736
+ }).catch((error) => {
737
+ serverRouteModuleCache.delete(cacheKey);
738
+ throw error;
739
+ });
740
+ setBoundedCacheEntry(serverRouteModuleCache, cacheKey, loaded, maxServerRouteModuleCacheEntries);
741
+ return loaded;
742
+ }
688
743
  async function runMiddleware(options) {
689
744
  const candidates = [
690
745
  join(options.appDir, "middleware.ts"),
@@ -701,6 +756,8 @@ async function runMiddleware(options) {
701
756
  appDir: options.appDir,
702
757
  file,
703
758
  importPolicy: options.importPolicy,
759
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
760
+ serverSourceFiles: options.serverSourceFiles,
704
761
  });
705
762
  if (!middlewareMatches(module.config, new URL(options.request.url).pathname)) {
706
763
  return undefined;
@@ -729,7 +786,34 @@ async function runMiddleware(options) {
729
786
  return undefined;
730
787
  }
731
788
  async function loadMiddlewareModule(options) {
732
- const code = await readFile(options.file, "utf8");
789
+ const code = await readServerSourceFile(options.file, options.serverModuleCacheVersion, options.serverSourceFiles);
790
+ const cacheKey = options.serverModuleCacheVersion === undefined
791
+ ? undefined
792
+ : `middleware\0${options.appDir}\0${options.file}\0${options.serverModuleCacheVersion}\0${memoizedHashText(code)}\0${importPolicyCacheKey(options.importPolicy)}`;
793
+ if (cacheKey !== undefined) {
794
+ const cached = middlewareModuleCache.get(cacheKey);
795
+ if (cached !== undefined) {
796
+ return cached;
797
+ }
798
+ }
799
+ const loaded = bundleMiddlewareModule({
800
+ appDir: options.appDir,
801
+ code,
802
+ file: options.file,
803
+ importPolicy: options.importPolicy,
804
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
805
+ }).catch((error) => {
806
+ if (cacheKey !== undefined) {
807
+ middlewareModuleCache.delete(cacheKey);
808
+ }
809
+ throw error;
810
+ });
811
+ if (cacheKey !== undefined) {
812
+ setBoundedCacheEntry(middlewareModuleCache, cacheKey, loaded, maxMiddlewareModuleCacheEntries);
813
+ }
814
+ return loaded;
815
+ }
816
+ async function bundleMiddlewareModule(options) {
733
817
  const output = await bundle({
734
818
  bundle: true,
735
819
  format: "esm",
@@ -747,7 +831,7 @@ async function loadMiddlewareModule(options) {
747
831
  jsxFactory: "__mreact_jsx",
748
832
  jsxFragment: "__mreact_fragment",
749
833
  stdin: {
750
- contents: code,
834
+ contents: options.code,
751
835
  loader: "ts",
752
836
  resolveDir: dirname(options.file),
753
837
  sourcefile: options.file,
@@ -758,6 +842,11 @@ async function loadMiddlewareModule(options) {
758
842
  throw new Error(`Failed to compile middleware for ${options.file}.`);
759
843
  }
760
844
  return importAppRouterSourceModule({
845
+ ...(options.serverModuleCacheVersion === undefined
846
+ ? {}
847
+ : {
848
+ cacheKey: `middleware:${options.file}:${options.serverModuleCacheVersion}:${memoizedHashText(compiled)}`,
849
+ }),
761
850
  code: compiled,
762
851
  label: `middleware:${options.file}`,
763
852
  });
@@ -987,6 +1076,92 @@ function hasOutOfOrderBoundary(code) {
987
1076
  function mayRenderOutOfOrderBoundary(code) {
988
1077
  return (code.includes("<Await") || code.includes("Await(") || code.includes("renderOutOfOrderBoundary"));
989
1078
  }
1079
+ async function mayRenderOutOfOrderBoundaryDeep(options) {
1080
+ const seen = new Set();
1081
+ return await mayRenderOutOfOrderBoundaryDeepInner(options, seen);
1082
+ }
1083
+ async function mayRenderOutOfOrderBoundaryDeepInner(options, seen) {
1084
+ if (mayRenderOutOfOrderBoundary(options.code)) {
1085
+ return true;
1086
+ }
1087
+ if (seen.has(options.filename)) {
1088
+ return false;
1089
+ }
1090
+ seen.add(options.filename);
1091
+ const sourceHash = memoizedHashText(options.code);
1092
+ const cacheKey = `${options.serverModuleCacheVersion ?? "dev"}\0${options.filename}\0${sourceHash}`;
1093
+ const cached = routeOutOfOrderBoundaryAnalysisCache.get(cacheKey);
1094
+ if (cached !== undefined) {
1095
+ return cached;
1096
+ }
1097
+ const pending = mayRenderImportedOutOfOrderBoundary(options, seen).catch((error) => {
1098
+ routeOutOfOrderBoundaryAnalysisCache.delete(cacheKey);
1099
+ throw error;
1100
+ });
1101
+ setBoundedCacheEntry(routeOutOfOrderBoundaryAnalysisCache, cacheKey, pending, maxRouteOutOfOrderBoundaryAnalysisCacheEntries);
1102
+ return pending;
1103
+ }
1104
+ async function mayRenderImportedOutOfOrderBoundary(options, seen) {
1105
+ for (const specifier of localModuleSpecifiers(options.code)) {
1106
+ const file = await resolveLocalServerSourceImport(options.filename, specifier);
1107
+ if (file === undefined) {
1108
+ continue;
1109
+ }
1110
+ const code = await readServerSourceFile(file, options.serverModuleCacheVersion, options.serverSourceFiles);
1111
+ if (await mayRenderOutOfOrderBoundaryDeepInner({
1112
+ code,
1113
+ filename: file,
1114
+ serverModuleCacheVersion: options.serverModuleCacheVersion,
1115
+ serverSourceFiles: options.serverSourceFiles,
1116
+ }, seen)) {
1117
+ return true;
1118
+ }
1119
+ }
1120
+ return false;
1121
+ }
1122
+ function localModuleSpecifiers(code) {
1123
+ const specifiers = new Set();
1124
+ const importPattern = /\b(?:import|export)\s+(?:type\s+)?(?:[^"']*?\s+from\s*)?["'](?<source>\.{1,2}\/[^"']+)["']/g;
1125
+ for (const match of code.matchAll(importPattern)) {
1126
+ const source = match.groups?.source;
1127
+ if (source !== undefined) {
1128
+ specifiers.add(source);
1129
+ }
1130
+ }
1131
+ return Array.from(specifiers);
1132
+ }
1133
+ async function resolveLocalServerSourceImport(fromFile, specifier) {
1134
+ const base = join(dirname(fromFile), specifier);
1135
+ const candidates = localServerSourceImportCandidates(base);
1136
+ for (const candidate of candidates) {
1137
+ try {
1138
+ await access(candidate);
1139
+ return candidate;
1140
+ }
1141
+ catch {
1142
+ // Try the next TypeScript route/source extension.
1143
+ }
1144
+ }
1145
+ return undefined;
1146
+ }
1147
+ function localServerSourceImportCandidates(base) {
1148
+ const candidates = [base];
1149
+ if (base.endsWith(".js")) {
1150
+ const withoutJs = base.slice(0, -".js".length);
1151
+ candidates.push(`${withoutJs}.ts`, `${withoutJs}.tsx`, `${withoutJs}.mreact.tsx`);
1152
+ }
1153
+ else if (base.endsWith(".jsx")) {
1154
+ const withoutJsx = base.slice(0, -".jsx".length);
1155
+ candidates.push(`${withoutJsx}.tsx`, `${withoutJsx}.mreact.tsx`);
1156
+ }
1157
+ else if (base.endsWith(".mreact")) {
1158
+ candidates.push(`${base}.tsx`);
1159
+ }
1160
+ else {
1161
+ candidates.push(`${base}.ts`, `${base}.tsx`, `${base}.mreact.tsx`, join(base, "index.ts"), join(base, "index.tsx"), join(base, "index.mreact.tsx"));
1162
+ }
1163
+ return candidates;
1164
+ }
990
1165
  async function runServerStreamModuleWithLoading(code, options) {
991
1166
  const loadingProps = {
992
1167
  data: undefined,
@@ -1338,6 +1513,31 @@ async function loadRouteData(options) {
1338
1513
  if (!hasLoaderExport(options.code)) {
1339
1514
  return undefined;
1340
1515
  }
1516
+ const module = await loadRouteLoaderModule(options);
1517
+ return module.loader === undefined ? undefined : await module.loader(options.context);
1518
+ }
1519
+ async function loadRouteLoaderModule(options) {
1520
+ const cacheKey = options.serverModuleCacheVersion === undefined
1521
+ ? undefined
1522
+ : `${options.appDir}\0${options.filename}\0${options.serverModuleCacheVersion}\0${memoizedHashText(options.code)}\0${importPolicyCacheKey(options.importPolicy)}`;
1523
+ if (cacheKey !== undefined) {
1524
+ const cached = routeLoaderModuleCache.get(cacheKey);
1525
+ if (cached !== undefined) {
1526
+ return cached;
1527
+ }
1528
+ }
1529
+ const loaded = bundleRouteLoaderModule(options).catch((error) => {
1530
+ if (cacheKey !== undefined) {
1531
+ routeLoaderModuleCache.delete(cacheKey);
1532
+ }
1533
+ throw error;
1534
+ });
1535
+ if (cacheKey !== undefined) {
1536
+ setBoundedCacheEntry(routeLoaderModuleCache, cacheKey, loaded, maxRouteLoaderModuleCacheEntries);
1537
+ }
1538
+ return loaded;
1539
+ }
1540
+ async function bundleRouteLoaderModule(options) {
1341
1541
  const output = await bundle({
1342
1542
  bundle: true,
1343
1543
  format: "esm",
@@ -1365,11 +1565,25 @@ async function loadRouteData(options) {
1365
1565
  if (code === undefined) {
1366
1566
  throw new Error(`Failed to compile loader for ${options.filename}.`);
1367
1567
  }
1368
- const module = await importAppRouterSourceModule({
1568
+ return await importAppRouterSourceModule({
1569
+ ...(options.serverModuleCacheVersion === undefined
1570
+ ? {}
1571
+ : {
1572
+ cacheKey: `loader:${options.filename}:${options.serverModuleCacheVersion}:${memoizedHashText(code)}`,
1573
+ }),
1369
1574
  code,
1370
1575
  label: `loader:${options.filename}`,
1371
1576
  });
1372
- return module.loader === undefined ? undefined : await module.loader(options.context);
1577
+ }
1578
+ function importPolicyCacheKey(policy) {
1579
+ if (policy === undefined) {
1580
+ return "";
1581
+ }
1582
+ return JSON.stringify({
1583
+ allowedPackages: [...(policy.allowedPackages ?? [])].sort(),
1584
+ allowedSourceDirs: [...(policy.allowedSourceDirs ?? [])].sort(),
1585
+ projectRoot: policy.projectRoot ?? "",
1586
+ });
1373
1587
  }
1374
1588
  async function loadRouteMetadata(options) {
1375
1589
  if (!hasMetadataExport(options.code)) {